Post

(CodeGate2014) nuclear - libpthead(send, recv, system)

아이디어 자체는 어렵지 않았으나 libc가 아닌 다른 library의 함수가 호출될거라는 생각을 못했기 때문에 꽤 헤맸다.
recv buffer 초기화에 별로 신경을 안썼더니, send의 결과로 받은 값에 이전 send 결과로 받은 데이터가 포함되어 몇 byte 씩 틀어져 있거나 처음에 303031이 들어가 시간을 꽤 소모함.

fork를 사용하는 standalone 방식의 서버 프로그램이다.

leak

\_\_isoc99_sscanf() 실행 이후 stack context

1
2
3
4
5
6
7
8
9
10
11
12
13
0000| 0xffffd050 --> 0xffffd070 ("1/1\n")
0004| 0xffffd054 --> 0x8049877 ("%f/%f")
0008| 0xffffd058 --> 0xffffd274 --> 0x3f800000
0012| 0xffffd05c --> 0xffffd270 --> 0x3f800000

  

gdb-peda$ x/16x 0xffffd270
0xffffd270:     0x3f800000      0x3f800000      0x53534150      0x45444f43
0xffffd280:     0x00000a7e      0x00000000      0x00000000      0x00000000
0xffffd290:     0x00000000      0x00000000      0x00000004      0x0804c008
0xffffd2a0:     0xf7fa7000      0x00000001      0xffffd318      0x080490d9

* 부동소수점이라 다르게 박힌다. 검사를 통과하기 위해서는 1. passcode를 leak하거나, 2. passcode에 저장되어있는 값을 null byte 등으로 변경해야 한다. passcode에 있는 값을 변경하려면 그 위에 위치한 buf, longtitude, latitude에서 overflow가 발생해야 하는데 별 다른 overflow 지점이 보이지는 않는다. * latitude나 longtitude의 타입이 다르기는 하지만, 어차피 %f로 읽어오기 때문에 4byte가 넘어가지는 않는다.

latitude, longtitude 4byte를 모두 채워 passcode와 이어지게 하는 것은 가능한데, passcode를 어떻게 출력할 것인가가 문제다.

1
2
3
4
5
6
7
8
9
10
0000| 0xffffd050 --> 0xffffd070 ("12345678/12345678\n")
0004| 0xffffd054 --> 0x8049877 ("%f/%f")
0008| 0xffffd058 --> 0xffffd274 ("Na<KPASSCODE~\n")
0012| 0xffffd05c --> 0xffffd270 ("Na<KNa<KPASSCODE~\n")

  

gdb-peda$ x/s 0xffffd270
0xffffd270:     "Na<KNa<KPASSCODE~\n"

이런 식으로 PASSCODE가 노출될 수는 있으나 latitude나 longtitude를 대상으로 하는 출력 코드가 따로 없다.

그러나, 아무 command나 입력하면 [!] Unknown command : asdfds 가 발생하면서 buf 가 출력되므로 buf, latitude, longtitude, passcode 를 모두 잇는다면 passcode가 leak된다.

exploit

launch 메뉴에서 passcode를 입력하면 thread(t1)을 생성하며, t1이 다시 t2를 생성한다. t1은 100초가 지나면 종료되고 t2는 그냥 buf에서 0x512만큼 recv한다.

buf의 크기는 0x512 가 아니라 그냥 512(0x200) 이기 때문에 여기서 overflow가 발생한다.

처음 t1, t2가 생성되고 나서는 buf가 비어있기 때문에, t2는 일단 recv에서 blocking 되어 있는 상태라 데이터를 전송해 overflow를 일으킬 수 있다.

두 가지 방법으로 해결할 수 있는데, 둘 모두 PIE가 안걸려 있기 때문에 가능한 방법이다.

1-1 libpthread RTL - system + nc

PIE가 안걸려 있으므로 send(GOT)로 libc_base를 구해 system() 등의 주소를 알아낼 수 있으며 ret과 param위치를 조작할 수 있으니 RTL을 사용할 수 있다.

send@got / recv@got를 leak해서 libc_base를 구하려고 했는데, 자꾸 안되길래 확인해보니

1
2
0xf7faf840  0xf7fbc467  Yes (\*)     /lib/i386-linux-gnu/libpthread.so.0
0xf7e13490  0xf7f4499e  Yes (\*)     /lib/i386-linux-gnu/libc.so.6

libpthread.so.0도 매핑되어 있다.

1
2
gdb-peda$ print recv
$2 = {<text variable, no debug info>} 0xf7fb90a0 <recv>

recv()

libc.so.6 이 아니라,libpthread.so.0 에 위치한다. (Ubuntu 14.04.5 LTS)

추가로 send() / system()libpthread.so.0에 위치한다. * 그래서 system()을 사용한 exploit은 send(), recv()를 기준으로 구한 libpt_base를 사용했을 때 제대로 동작한다.

해서, 이를 이용해 base를 구한다면 이는 libpt_base 가 된다.

문제는 libpthread-db같은게 존재하지 않아서 서버에서 사용하는 libpthread 버전을 알아내는 것이 어렵고, 따라서 base를 구하기가 어렵다는 점이다.

그냥 서버에 있는 libpthread를 대상으로 objdump로 base와 offset을 계산했다.

https://github.com/umbum/pwn/blob/master/exploit/cg_nuclear.py#L21-L67

system()을 사용하면 recv()/bin/sh | nc를 넘긴 다음 이를 인자로 사용할 수 있기 때문에, stdio를 연결해 줄 필요가 없어 수월하다.

1-2 libc RTL - dup2 + exec*

system()은 libpt에 있기 때문에 사용할 수 없다. 따라서 libc에 있는 exec\*()를 사용해야 한다. xinetd가 아니라 standalone이기 때문에 표준입출력이 소켓과 연결되어있지 않아 nc를 사용하는 것이 편한데, exec\*('/bin/sh')를 사용한다면 nc를 사용할 수 없어 STDIOsockfd를 연결해주어야 한다.

libc_base는 send(), recv()말고 다른 함수를 기준으로 구해야 한다. 여기서는 fclose@got를 사용했고 libc-database를 사용했다. one shot을 쓰는게 간단한데, 모두 동작하지 않아 그냥 RTL했다.

https://github.com/umbum/pwn/blob/master/exploit/cg_nuclear.py#L72-L115

2 Shellcode

recv()를 사용할 수 있으니, ROP chain을 구성해 recv로 buf 위치를 지정해 적당한 곳에 쉘코드를 쓴 다음 buf로 리턴할 수 있다. 근데 mprotect()도 호출해줘야 해서 좀 번거롭다. 귀찮아서 이렇게는 안풀었다.

?
1
2
snprintf(&s, 0x80u, "%c[2J%c[0;0H", 27, 27);   // shell output clear
snprintf(&s, 0x80u, "%c[%d;%dH", 27, 5, 5);    // shell cursor return to top
This post is licensed under CC BY 4.0 by the author.