(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
를 사용할 수 없어 STDIO
와 sockfd
를 연결해주어야 한다.
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