栈迁移
栈迁移 (Stack Migration),又称为 Stack Pivoting (栈枢纽/栈反转),是二进制漏洞利用中一种非常重要且优雅的技巧。
它的核心目的是:当栈溢出可用的空间非常有限(不足以存放完整的 ROP 链或 Shellcode)时,利用特定的汇编指令,将栈指针(ESP/RSP)“迁移”到攻击者可以控制的、空间更大的内存区域(如 BSS 段、堆、或其他栈位置),从而继续执行攻击。
1. 为什么需要栈迁移?
在标准的栈溢出攻击中,我们通常构造如下 Payload:
[ Padding ] + [ ROP Chain / Shellcode ]
但现实中常遇到这种情况:
- 溢出空间太小:例如程序只有 20 字节的溢出空间,而一个
execve("/bin/sh")的 ROP 链可能需要 60 字节以上。 - 开启了 NX 保护:无法直接在栈上执行 Shellcode,必须用 ROP,而 ROP 链通常很长。
此时,我们无法在当前的栈帧中塞入攻击代码。但是,如果我们能欺骗 CPU,让它把栈指针 (ESP/RSP) 指向我们提前布置好数据的另一个地方,就可以解除空间限制。
2. 核心原理:leave; ret 指令
栈迁移最常用的利用手段是利用 leave; ret 指令序列。这是大多数函数(特别是 x86/x64 架构下未开启优化时)的标准函数尾声 (Epilogue)。
指令分解
leave 指令等价于以下两条指令:
mov esp, ebp ; 1. 将栈指针 ESP 恢复到 EBP 的位置 (收缩栈帧)
pop ebp ; 2. 将栈顶的数据弹出给 EBP (恢复上层函数的栈基址)
ret 指令等价于:
pop eip ; 3. 将栈顶的数据弹出给 EIP (跳转执行)
攻击思路
利用 leave 指令的特性,我们可以发现:EBP 寄存器控制了 ESP 的去向。
- 如果我们在溢出时,覆盖了栈上的 Saved EBP,将其改为目标内存地址 (Fake Stack)。
- 然后覆盖 Return Address,让程序跳转执行
leave; ret指令(通常利用程序原本结尾的那条指令即可)。 - 当程序执行两次
leave操作(一次是函数自身的退出,一次是我们 gadget 的执行)或者巧妙配合时,ESP 就会指向我们设定的 Fake Stack。
3. 详细攻击流程 (以 32 位为例)
假设我们要把栈迁移到内存地址 0x0804A000 (BSS 段,可读写且固定)。
阶段 1:内存布局与 Payload 构造
首先,我们需要在那个“宽敞”的地方(0x0804A000)写入我们的 ROP 链。
如果无法提前写入,我们可能需要利用 read 函数先往那里写数据。
当前受限的栈溢出 Payload 结构如下:
[ Padding (填充至 EBP 前) ]
[ Fake EBP ] <-- 覆盖 Saved EBP,填入 target_addr (如 0x0804A000)
[ Return Addr] <-- 覆盖返回地址,填入 'leave; ret' gadget 的地址
阶段 2:执行过程推演
让我们一步步看 CPU 发生了什么:
1. 函数执行完毕,准备退出
此时栈指针 ESP 指向栈帧底部。
EBP 指向 Saved EBP 的位置。
2. 执行函数原本的 leave
mov esp, ebp:ESP指向了Saved EBP。pop ebp:- 栈顶的值(我们覆盖的
0x0804A000)被弹出放入EBP寄存器。 - 此时 EBP = 0x0804A000。
ESP向下移动,指向了Return Address。
- 栈顶的值(我们覆盖的
3. 执行函数原本的 ret
pop eip: 栈顶的值(我们覆盖的leave; ret的地址)被赋给EIP。- 程序跳转去执行这个
leave; retgadget。
4. 执行 Gadget 中的 leave (迁移发生的关键瞬间)
mov esp, ebp:- 此时
EBP是0x0804A000。 - CPU 将 ESP 更新为 0x0804A000!
- 栈迁移成功! 现在栈顶指针已经跑到了 BSS 段。
- 此时
pop ebp:- 从新的栈顶 (
0x0804A000) 弹出一个值给EBP(通常我们在 fake stack 开头放 4 字节垃圾数据来抵消这一步)。 ESP变为0x0804A004。
- 从新的栈顶 (
5. 执行 Gadget 中的 ret
pop eip: 从0x0804A004取出地址执行。这通常就是我们 ROP 链的第一条指令。
4. 栈迁移的变体
A. 64位系统的区别 (Ret2csu / Off-by-one)
在 64 位系统中,操作的是 rbp 和 rsp,原理完全一样。但需要注意:
- 地址是 8 字节。
- 如果只能修改
Saved RBP的最低字节(Off-by-one 漏洞),也可以控制栈在小范围内迁移。
B. 利用 pop rsp
如果在 64 位程序的 Gadget 库中能找到 pop rsp; ret,那更加直接:
- 直接覆盖返回地址为
pop rsp; ret的地址。 - 紧接着放入
Fake Stack Address。 ret跳转到 gadget ->pop rsp读取下一个值直接修改栈指针。
C. Heap Pivoting
如果我们将 ROP 链放在堆(Heap)上,通过上述方法将 ESP/RSP 指向堆地址,就叫 Heap Pivoting。这在浏览器漏洞利用中非常常见。
5. 一个具体的利用场景示例
场景:
- 函数
read(0, buf, 0x40),但buf到ebp的距离是 0x40。 - 这意味着我们只能覆盖
Saved EBP和Return Address,没有多余空间放 ROP。
利用步骤:
-
第一次 Payload (写入 ROP): 利用这微小的溢出,先调用
read(0, bss_addr, 100)。把真正的长 ROP 链读入 BSS 段。- Payload:
[Padding] + [Fake EBP] + [read_plt] + [leave_ret_addr] + [0, bss, 100](这里利用栈帧结构可能比较复杂,通常用两次迁移)。
- Payload:
-
第二次 Payload (迁移): 再次触发溢出。
- Payload:
[Padding] + [bss_addr (Fake EBP)] + [leave_ret_gadget]
- Payload:
-
结果: 程序执行
leave; ret后,ESP 飞到了bss_addr,开始执行之前写入在那里的 ROP 链。
总结
栈迁移是空间换时间/空间换空间的经典战术。
-
前提:
- 存在栈溢出,能覆盖 EBP 和 EIP。
- 有一块已知地址、且可控内容的内存区域(BSS、Heap等)。
- 程序中有
leave; ret指令(或者能修改 SP 的 gadget)。
-
本质:利用
mov esp, ebp这一机制,通过劫持ebp来间接劫持esp,从而重新定义”栈”的位置。