逆向工程实战手册 第 18 章

第 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+ 次循环验证成功率

动手练习

  1. 用 pwn.college 或本地起一个 ret2libc 练习环境,完成:leak → 反查 libc → ROP → shell。
  2. 练习栈对齐:故意构造 16 字节未对齐 payload,观察 movaps 崩溃,加 ret 修复。
  3. 用 pwndbg 的 bins/heap 观察一次 tcache poisoning 全过程。
  4. 准备一个内核 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/