第 18 章 从逆向到利用(Pwn)
逆向解决"程序在干什么",Pwn 解决"怎么让程序按我说的做"。本章覆盖从漏洞点写出稳定 exploit 的全流程(栈/堆/内核),强调本地通 → 远程稳定打的工程差距。
📍 知识点地图 | 主题:Pwn利用链 | 前置:第16章 | 后续:第79章 | 核心概念:栈/堆/内核、ROP、pwntools、checksec
18.1 分工与前提
前置: 你已经知道"哪里炸了"(溢出/UAF/double-free 点已定位)
本阶段: 把漏洞变成可执行攻击
上游: 漏洞发现(fuzz/审计/逆向)
下游: 拿到 shell 后整合进攻击链
18.2 核心工作流(六步)
Step 1: 确认漏洞类型 + 保护机制
checksec ./vuln # NX/Canary/PIE/RELRO/Fortify
file ./vuln; readelf -d ./vuln
→ 分类: 栈溢出/格式化字符串/堆(UAF/DF/OF)/整数/竞态/内核
Step 2: 选择利用策略
NX 关 + 无 ASLR → shellcode
NX 开 + 给 libc → ret2libc / one_gadget
NX 开 + 不给 libc → leak 后 libc-database 反查
堆 → 按 glibc 版本对应技术 (tcache/fastbin/unsorted/large)
内核 → commit_creds / modprobe_path / core_pattern
Step 3: 准备 libc + gadget
libc-database: ./find puts 0x6f0
ROPgadget --binary ./libc.so.6 --only "pop|ret"
one_gadget ./libc.so.6
libc base = leak_addr - libc.sym['puts']
Step 4: 写 pwntools 模板(本地)
context.binary = ELF('./vuln')
p = process('./vuln')
payload = cyclic(N) + p64(ret) + ...
p.interactive()
Step 5: 本地调通
反复 attach + 看寄存器 + 调 offset
pwndbg: vmmap / heap / bins / telescope
Step 6: 远程稳定化(关键!)
libc: 用 leak 反查 libc-database,不要拍脑袋
栈对齐: 16 字节不对齐 → movaps 崩 → 加 ret gadget
远程延迟: recvuntil 精确锚字符串,禁用模糊 sleep
远程缓冲: sendlineafter 比 sendline 稳
堆喷: 放大 spray 数量 + 留 padding chunk
验证: 连续跑 20+ 次成功率 ≥ 95%
18.3 栈利用(stack-pwn)
经典 ret2libc(64 位)
from pwn import *
context.binary = ELF("./vuln")
libc = ELF("./libc.so.6")
p = process("./vuln")
# 1. leak canary(如果开了)
# 2. leak libc 函数地址: puts@got
payload = b"A"*offset + p64(pop_rdi) + p64(libc.sym["puts"]) + p64(main)
# 3. 算 libc base
# 4. 构造最终 payload
payload = b"A"*offset + p64(canary) + p64(0) + p64(ret) + p64(pop_rdi) + p64(bin_sh) + p64(system)
p.sendline(payload)
p.interactive()
栈对齐(64 位高频坑)
movaps 要求 rsp 16 字节对齐
不对齐 → 崩溃在 system 内部
解法: 链首加一个 ret gadget(空 ret)
常用姿势
| 技巧 | 说明 |
|---|---|
| ret2csu | 无 pop rdi 时用 libc_csu_init 的通用 gadget |
| one_gadget | 一个地址拿 shell(约束要满足) |
| ret2dlresolve | 无 libc 时手工构造重定位 |
| SROP | sigreturn 伪造(SigreturnOrientedProgramming) |
| 格式化字符串 | %n 任意写 / %p 泄露 |
18.4 堆利用(heap-pwn)
glibc 版本 → 技术映射(最重要的决策表)
< 2.26 无 tcache, 老 fastbin/unsorted 技术
2.27+ tcache 引入 → tcache dup/poison 大放异彩
2.29 tcache 增加 key 字段(double-free 检查)→ tcache stash
2.32+ safe-linking(堆地址异或)→ 需要泄露堆地址
2.34 hooks 移除 → 转向 FILE struct / IO 利用
核心技术清单
tcache dup / tcache poisoning
fastbin dup / fastbin attack
unsorted bin attack(写 main_arena)
house of 系列(Einherjar/Roman/Botcake...)
large bin attack
FSOP / _IO_FILE 利用
off-by-null
UAF: double free / 重叠 chunk
18.5 内核 pwn(kernel-pwn)
环境: vmlinux + bzImage + initramfs.cpio.gz + vuln.ko
保护: SMEP / SMAP / KASLR / KPTI
策略:
1. 改 init 脚本拿 root shell(CTF)或先 leak KASLR(真实)
2. leak 内核基址(/proc/kallsyms 限权时用未初始化堆喷)
3. 在 slab 喷 tty_struct / msg_msg / pipe_buffer
4. SMEP 开 → 不能直接跳用户态 → stack pivot + 内核 ROP
5. ROP 链: prepare_kernel_cred(0) → commit_creds → swapgs+iretq
6. 或覆盖 modprobe_path 为 /tmp/x → 触发 modprobe 机制
关键确认:QEMU 启动参数里有没有 +smep +smap +pku,直接决定 ROP 链写法。
18.6 工程纪律(本地 → 远程的坑)
| 坑 | 解法 |
|---|---|
| libc 版本错配 | leak 反查 libc-database,不假设版本 |
| 栈对齐 | 加空 ret gadget |
| 网络延迟 | recvuntil 精确锚点,不用 sleep |
| 远程缓冲 | sendlineafter |
| 堆喷成功率 | 放大 spray + padding chunk 防合并 |
| 一次 leak 不够 | KASLR 一次 leak 所有偏移,别重复 |
| 本地通远程崩 | 20+ 次循环验证成功率 |
动手练习
- 用 pwn.college 或本地起一个 ret2libc 练习环境,完成:leak → 反查 libc → ROP → shell。
- 练习栈对齐:故意构造 16 字节未对齐 payload,观察 movaps 崩溃,加 ret 修复。
- 用 pwndbg 的
bins/heap观察一次 tcache poisoning 全过程。 - 准备一个内核 pwn 环境(qemu),验证 modprobe_path 覆盖到 root。
深入阅读
- 仓库:
skills/pwn-chain/SKILL.md(六步工作流 + 典型场景模板) - 仓库:
skills/pwn-chain/references/stack-pwn.md(栈利用全量) - 仓库:
skills/pwn-chain/references/heap-pwn.md(堆利用全量 + glibc 版本映射) - 仓库:
skills/pwn-chain/references/kernel-pwn.md(内核 pwn:kROP/SMEP/SMAP/modprobe_path) - 实战案例:
skills/field-journal/seed-010_ctf-pwn-rop-x64.md - 仓库:
CTF-Sandbox-Orchestrator/competition-reverse-pwn/