Pwn讲解—栈溢出高级利用技术与ROP链构造
栈溢出的进阶视角
基础栈溢出(ret2text、ret2shellcode、ret2libc)是 Pwn 的入门内容。高级栈溢出关注的是:在严格的保护机制(NX、ASLR、PIE、RELRO、Canary)下,如何构造复杂的利用链;以及各种高级 ROP 技术(ret2csu、SROP、ret2reg、stack pivot)的原理和应用。
本文假设读者已经掌握基础栈溢出原理,重点讲解高级利用技术。
保护机制与绕过策略
| 保护机制 | 作用 | 绕过方法 |
|---|---|---|
| NX/DEP | 栈不可执行 | ROP、ret2libc |
| ASLR | 地址随机化 | 泄露地址(puts/printf 泄露 GOT) |
| PIE | 程序基址随机化 | 泄露程序地址(partial overwrite、格式化字符串) |
| Stack Canary | 栈溢出检测 | 泄露 canary、格式化字符串、tls 劫持 |
| RELRO | GOT 表只读 | ret2dlresolve、largebin attack(堆) |
| Full RELRO | GOT 完全只读 | 只能用 ROP,不能改 GOT |
高级栈溢出题通常同时开启多种保护,需要组合多种技术。
地址泄露技术
ASLR 和 PIE 开启时,需要先泄露地址才能构造 ROP 链。
1. puts/printf 泄露 GOT
最基础的泄露方法:调用 puts(got_addr) 打印 GOT 表中函数的真实地址,然后计算 libc 基址。
1 | from pwn import * |
2. 格式化字符串泄露
如果程序有格式化字符串漏洞(printf(user_input)),可以泄露栈上的任意数据,包括 canary、返回地址、libc 地址等。
1 | # 泄露 canary(假设 canary 在栈上的偏移是 %15$p) |
3. Partial Overwrite(部分覆盖)
当 PIE 开启时,程序基址的低 12 位(页内偏移)是固定的。可以只覆盖返回地址的低 2-3 字节,跳转到程序中的某个 gadget,不需要泄露完整地址。
1 | # 只覆盖返回地址的低 2 字节,跳转到程序中的 win 函数 |
Partial overwrite 的成功率取决于地址的低字节是否固定。ASLR 的粒度是页(4KB),所以低 12 位固定,低 13-16 位有 1/16 的概率。
Canary 绕过技术
1. 泄露 Canary
最直接的方法:用格式化字符串或逐字节爆破泄露 canary。
1 | # 逐字节爆破 canary(64 位 canary 是 8 字节,最低字节固定为 0x00) |
2. TLS 劫持(Canary 绕过的高级方法)
Canary 的值存储在 TLS(Thread Local Storage)中,通过 fs:0x28(64 位)或 gs:0x14(32 位)访问。如果可以任意写,可以修改 TLS 中的 canary 值,让检查通过。
1 | # 用格式化字符串任意写,修改 TLS 中的 canary |
3. 覆盖 __stack_chk_fail 的 GOT
如果开启了 Partial RELRO(GOT 可写),可以覆盖 __stack_chk_fail 的 GOT 为一个 gadget,让 canary 检查失败时跳转到我们控制的地址。
1 | # 用格式化字符串或任意写,把 __stack_chk_fail@got 改成 pop rdi; ret |
高级 ROP 技术
1. ret2csu(通用 gadget)
64 位程序中,__libc_csu_init 函数中包含一组通用的 gadget,可以用来设置任意寄存器(rdi、rsi、rdx),这在没有足够 gadget 时非常有用。
__libc_csu_init 中的关键 gadget:
1 | # gadget 1(csu_gadget_1): |
利用 ret2csu 调用任意函数(设置 rdi、rsi、rdx 三个参数):
1 | def ret2csu(csu_gadget_1, csu_gadget_2, rdi, rsi, rdx, func_addr): |
ret2csu 的限制:
- 只能设置 rdi、rsi、rdx(前三个参数)
- 调用的函数地址必须在 r12 指向的内存中(通常是 GOT 表中的函数)
- 调用后会继续执行 csu_gadget_1,需要填充栈
2. SROP(Sigreturn Oriented Programming)
SROP 利用 sigreturn 系统调用来设置所有寄存器。当程序中有 syscall; ret gadget 时,可以构造一个 sigreturn 帧,设置所有寄存器(包括 rip、rsp、rax、rbx、rcx、rdx、rsi、rdi、r8-r15 等)。
SROP 的优势:
- 只需要一个
syscall; retgadget - 可以一次设置所有寄存器
- 可以直接设置 rip 到任意地址
1 | from pwn import * |
SROP 的限制:
- 需要
syscall; retgadget - 需要知道
/bin/sh的地址(或用 read 写入) - sigreturn 帧较大(248 字节),需要足够的溢出空间
3. ret2reg(返回到寄存器)
当寄存器中存储了我们控制的数据的地址时,可以直接跳转到寄存器指向的代码。常见的 ret2reg:
- ret2rax:跳转到 rax 指向的地址(rax 通常是函数返回值)
- ret2rsp:跳转到 rsp 指向的地址(栈顶)
- ret2rdi/rsi/rdx:跳转到参数寄存器指向的地址
1 | # ret2rsp:jmp rsp gadget |
4. Stack Pivot(栈迁移)
当溢出空间不足以构造完整的 ROP 链时,可以把栈迁移到另一个我们控制的区域(如 bss 段、堆、可读可写的内存)。
Stack pivot 的核心 gadget:leave; ret
1 | leave: |
利用 leave; ret 进行栈迁移:
1 | # 假设我们在 bss 段构造了一个假栈 |
在 fake_stack 处预先构造完整的 ROP 链,leave; ret 后就会执行这个 ROP 链。
Stack pivot 常用于:
- 溢出空间不足(只能覆盖 rbp 和返回地址)
- 需要构造复杂的 ROP 链(ret2csu、SROP)
- 格式化字符串漏洞中迁移栈
5. ret2dlresolve(延迟绑定绕过)
当 Full RELRO 关闭时,可以利用动态链接器的延迟绑定机制,让程序解析一个不存在的函数名,从而执行任意函数。不需要泄露 libc 地址。
原理:
- PLT 中的代码会跳转到
_dl_runtime_resolve _dl_runtime_resolve根据 reloc 索引和符号索引解析函数地址- 我们可以伪造 reloc 和符号结构,让解析器解析我们指定的函数(如 system)
ret2dlresolve 比较复杂,通常用 pwntools 的 Ret2dlresolve 类自动构造:
1 | from pwn import * |
64 位 vs 32 位的差异
64 位(x86-64)
- 参数通过寄存器传递(rdi, rsi, rdx, rcx, r8, r9)
- 需要 pop rdi 等 gadget 来设置参数
- ret2csu 是设置多参数的常用方法
- 栈对齐:调用 system 前需要 ret gadget 对齐 16 字节
- 地址 8 字节,溢出空间需求更大
32 位(x86)
- 参数通过栈传递,直接在返回地址后布置参数
- 不需要 pop 寄存器的 gadget
- 利用更简单:
payload = padding + system_addr + fake_return + arg1 + arg2 - 地址 4 字节,溢出空间需求小
实战:Full RELRO + PIE + Canary 的栈溢出
这是一个典型的高级栈溢出场景,所有保护都开启。
利用思路
- 泄露 Canary:用格式化字符串或逐字节爆破
- 泄露 PIE 基址:用格式化字符串泄露栈上的返回地址,计算程序基址
- 泄露 libc 基址:用 printf/puts 泄露 GOT 表(Full RELRO 下 GOT 只读,但可以读)
- 构造 ROP 链:用 ret2csu 或已知 gadget 调用 system(“/bin/sh”)
完整 exploit 框架
1 | from pwn import * |
调试技巧
pwndbg 高级命令
1 | # 查看 canary |
动态泄露地址
1 | # 在 gdb 中查看 libc 基址 |
常见坑
- 栈对齐:64 位下调用 system 前需要
retgadget 做 16 字节栈对齐,否则会段错误(movaps 指令) - Canary 最低字节:Canary 的最低字节固定为 0x00(防止字符串函数泄露),爆破时从第 2 字节开始
- ret2csu 的 rbp 设置:rbp 必须设为 1,否则
cmp rbp, rbx; jnz会跳回,导致无限循环 - SROP 的 sigreturn 帧:必须设置正确的
__reserved字段,否则 sigreturn 会失败 - Full RELRO 下不能改 GOT:只能用 ROP,不能用 ret2dlresolve(需要 Partial RELRO)
- PIE 下 gadget 地址:所有 gadget 地址都要加上程序基址,泄露基址后才能计算
总结
高级栈溢出的核心是在多种保护机制的组合下,构造复杂的利用链。从基础的地址泄露,到 Canary 绕过,再到 ret2csu、SROP、Stack Pivot、ret2dlresolve 等高级 ROP 技术,每一种技术都是在特定限制下的解决方案。
掌握高级栈溢出需要:
- 深入理解 x86 汇编和调用约定:寄存器、栈布局、系统调用
- 熟练使用 ROPgadget 和 pwntools:快速找到和构造 gadget
- 理解各种保护机制的原理:知道每种保护限制了什么,才能找到绕过方法
- 耐心调试:高级利用链通常需要多次调试才能成功
- 积累经验:不同的题目有不同的限制,经验丰富才能快速选择合适的技术
栈溢出是 Pwn 的基石,掌握高级栈溢出技术后,再去学堆利用、内核 pwn、浏览器 pwn 等更复杂的方向时,思路会清晰很多。









