栈迁移
栈迁移的原理
栈迁移是因为栈溢出造成的漏洞,而溢出的又不多,通常刚好溢出到ebp和返回地址,无法构造参数,所以有时候不能直接利用,所以我们通过改变esp和ebp在我们可以改写的地方构造一个新的栈
我们在栈迁移中最常用的就是leave_ret这两个命令,下面解释一下这两个命令
1 | leave: |
leave指令即为mov esp ebp;pop ebp先将ebp赋给esp,此时esp与ebp位于了一个地址,你可以现在把它们指向的那个地址,即当成栈顶又可以当成是栈底。然后pop ebp,将栈顶的内容弹入ebp(此时栈顶的内容也就是ebp的内容,也就是说现在把ebp的内容赋给了ebp)。因为esp要时刻指向栈顶,既然栈顶的内容都弹走了,那么esp自然要往下挪一个内存单元。
ret指令为pop eip,这个指令就是把栈顶的内容弹进了eip(就是下一条指令执行的地址)
栈迁移的核心在于,将当前的 EBP 覆盖为我们想要将栈迁移过去的地址,然后将返回地址覆盖为 leave; ret 的地址


什么是 BSS 段?
BSS 段(Block Started by Symbol,以符号开始的块)是程序可执行文件 / 内存布局中的核心分段之一,专门用于存储「未初始化的全局变量、未初始化的静态变量」,以及「初始值为 0 的全局 / 静态变量」—— 本质是操作系统在程序启动时,一次性将该段内存全部清零的区域。
简单示例
1 |
|

当我们没办法覆盖返回地址的时候,就要去寻找ESP指针去构造ROP去找到EIP控制返回地址
对比32位和64位程序的不同传参方式
1 | # 64位(复杂,需要gadget): |
1 | # 64位(寄存器传参): |
基于64位程序的rdi指令
pop rdi; ret
1 | pop rdi ; mov rdi, [rsp] ; 把rsp指向的内存内容复制到rdi = target_addr+0x20 (/bin/sh地址) |
步骤2:执行 ret (栈对齐)
1 | ret ; pop eip = [ESP] = target_addr+0x18 (system地址) |
执行 ****system
1 | system: ; 此时RDI已经包含/bin/sh地址 |
chanllenge
栈迁移练习1)[BUU]ciscn_2019_es_2 //x32

仔细看一下read的范围,30u刚好可以覆盖到rbp,所以我们可以选择直接泄露rbp地址
read读入到s的这个地方,距离ebp只有0x28个字节,但是两个read都可以写入0x30个字节的内容,
也就是说可以溢出覆盖ebp和返回地址,我们不足以直接构造pop链但是可以构造栈迁移。
先寻找我们需要的gadget抽取出地址
我们通过IDA可知,read在ebp-28h处,我们可以通过第一次read去寻找ebp的地址(payload1)
之后我们gdb调试进入到程序vul函数当中,在第二次输入中输入aaaa来确定第二次payload2的初始地址
这里我们可以通过第二张图片的地址差值去确定偏移得到ebp-esp=0x38,地址即为ebp-0x38

要完成栈迁移的攻击结构,就要覆盖原栈上 ret为 leave_ret gadget的地址,本题中可覆盖为 0x080484b8;要将esp劫持到 old_ebp -0x38处,就要将原ebp中的 old_ebp 覆盖为old_ebp -0x38,其中 old_ebp 可通过第一次 read & printf 泄露得到。此时栈迁移payload的框架如下图所示。
在上图中的Payload中, vuln 函数正常执行到leave指令时, ebp 寄存器将被赋予 old_ebp -0x38,而之后执行 ret(即第二个 leave ret)时, esp 将随之被覆盖为该值,因此该payload已然能实现将 esp 劫持至 old_ebp -0x38处的栈迁移效果了。
接下来则要向该框架中填充执行 system 的shellcode 以完成对 eip 与执行流的篡改。此处与传统的栈溢出攻击类似,下面直接给出payload结构。
上图中,栈迁移的最后一个 pop eip 执行结束后, esp 将指向 aaaa 后的内容开始执行,故此处要填上 system 函数地址,那么后面则应为一个 fake ebp 来维持栈操作的完整性。再往后则是 system 的函数参数,即 /bin/sh 的地址。而 /bin/sh 本身我们也可由 read 函数输入到该区域内,因此其地址恰好也在栈上。
下面可以构造exp了
exp 1)
1 | from pwn import * |
exp 1)解析
1 | from pwn import * |

栈迁移练习 2)[BUU] gyctf_2020_borrowstack1


栈迁移练习 3)[NSS][HDCTF 2023]KEEP ON //x64

这直接printf(s)了,很明显的一个格式化字符串漏洞哈,基于这个我们可以去练习栈迁移
我们首先是想要拿到rbp的地址的,但是read只有48u没有办法覆盖到rbp/s-[rbp-50h]

使用GDB调试,在第一个read处输入以下字符串内容,之后单步到printf处看一看栈上的内容,找一找rbp的地址去确定rbp的偏移,发现偏移是16,那就是%16$p
1 | aaaaaaaa_%p_%p_%p_%p_%p_%p_%p_%p_%p_%p_%p_%p_%p_%p_%p_%p_%p_%p_%p_%p |


寻找一下第一次read输入的地址和rbp的距离,因为我们需要栈迁移到这里,距离0x60


1 | from pwn import * |
target 是栈迁移后的栈基址(rsp 最终指向的位置),/bin/sh 是我们构造在 payload 里的字符串,要让 rdi 寄存器指向这个字符串的地址,就必须精准计算:/bin/sh 在 target 这个基址下的偏移量。
先明确栈迁移后的 “地址映射关系”
栈迁移的核心是:执行 leave 后,rsp = target(栈顶直接指向 target 地址)。此时,payload 里的每一个字节,都会对应到 target 开始的连续内存地址中。
我们先把 payload 的结构和对应的地址偏移列出来(以target为基址,单位:字节):
栈情况
| 地址偏移(target+N) | payload 内容 | 字节数 | 累计偏移 |
|---|---|---|---|
| target+0 | b’aaaaaaaa’(填充) | 8 | 8 |
| target+8 | p64(ret_addr) | 8 | 16 |
| target+16 | p64(pop_rdi_ret) | 8 | 24 |
| target+24 | p64(target+40) | 8 | 32 |
| target+32 | p64(sys_addr) | 8 | 40 |
| target+40 | b’/bin/sh’ | 8 | 48 |
| … | 填充 \x00 到 0x50(80) | - | 80 |
| target+80 | p64(target) | 8 | 88 |
| target+88 | p64(leave_ret) | 8 | 96 |
更新: 2026-04-23 11:34:52
原文: https://www.yuque.com/idcm/wnemg9/yxuzemozfzorix1g