逆向工程实战手册 第 39 章

第 39 章 漏洞挖掘方法论(Fuzz / 审计 / 符号执行辅助)

前面章节讲"分析已有程序",本章讲"主动找漏洞":审计方法论、模糊测试(Fuzzing)、符号执行辅助验证。发现漏洞 → 第 18 章转 exploit 链。

📍 知识点地图 | 主题:漏洞挖掘 | 前置:第38章 | 后续:第18章 | 核心概念:审计、AFL++、符号执行辅助、去重

39.1 漏洞挖掘全景(三条路线)

路线 A: 代码审计(人工/工具辅助)
  适用: 有源码(开源)或反编译可读性高
  方法: 危险函数清单 → 追踪输入可达性 → 确认利用条件

路线 B: 模糊测试(Fuzzing)—— 自动发现崩溃
  适用: 无源码、有输入接口(文件/网络/命令行)
  方法: 生成变异输入 → 监控崩溃 → 去重 → 分析

路线 C: 符号执行/污点(智能验证)
  适用: 特定逻辑(协议/格式解析器)
  方法: 符号化输入 → 路径约束求解 → 触发深分支

实战中三者组合: 审计找可疑点 → fuzz 批量触发 → 符号执行验证

39.2 代码审计方法论(有源码/反编译视角)

危险函数清单(第一遍扫描)

类别 函数 漏洞类型
内存拷贝 strcpy/strcat/sprintf/gets 栈溢出
长度缺失 memcpy/strncpy(长度错误) 堆/栈溢出
格式化 printf(user_input) 格式化字符串
整数 atoi/strtol → 用前不校验 整数溢出 → 绕过
命令 system/popen/exec 命令注入
文件 open/fopen + 路径拼接 路径穿越
反序列化 pickle/unserialize/readObject 反序列化 RCE
释放 free/delete/close UAF/double-free

审计流程(架构)

1. 找输入面: 程序从哪里读数据?(argv/文件/网络/环境变量)
2. 找危险函数调用点(grep/IDA 搜索)
3. 追溯输入可达性: 用户输入 → 是否到达危险函数?
4. 检查长度/类型/释放管理
5. 确认利用条件(ASLR/保护开启情况)
6. 产出: 可疑点清单(优先级: 可达+可控+可利用)

反编译审计的加分项

□ IDA: 搜索 strcpy|sprintf|gets 的 xref(35 章模式)
□ 用数据流追踪确认输入可控(idapro_trace_data_flow)
□ 栈帧大小 → 判断溢出空间(stack_frame)
□ 危险函数在哪个权限上下文执行(提权潜力)

39.3 模糊测试(Fuzzing)—— 主力自动发现

概念与工具

原理: 提供大量变异输入 → 程序崩溃 = 疑似漏洞
覆盖率: 覆盖率引导(coverage-guided)是主流(记录执行路径,优先探索新路径)

工具选型:
  AFL++             最主流(覆盖率引导,支持 QEMU/Unicorn 模式)
  libFuzzer         单文件/库内 fuzz(Clang 内置)
  honggfuzz         硬件计数器辅助
  radamsa           快速文件变异(简单粗暴)
  Peach / Boofuzz   协议 fuzz(状态感知)
  syzkaller         内核 syscall fuzz

AFL++ 实战流程

# 1. 编译插桩(有源码)
afl-clang-fast -g -o target target.c

# 2. 无源码 → QEMU 模式
afl-fuzz -Q -i seeds/ -o out -- ./target @@

# 3. 种子输入(种子质量决定覆盖率!)
#    提供多样化的最小有效输入(正常文件/边界值/格式变体)

# 4. 运行监控
afl-fuzz -i seeds/ -o out -m none -- ./target @@
# out/crashes/  崩溃样本
# out/hangs/    超时样本
# 界面: execs/sec(执行速度)、paths(路径数)、crash count

# 5. 崩溃去重 + 复现
afl-tmin -i crash -o minimized -- ./target @@     # 最小化
afl-cmin -i crashes -o unique -- ./target @@      # 去重
gdb ./target < crash_input                          # 复现分析

提高 fuzz 效果的关键知识

□ 种子质量 > 数量: 10 个高质量种子远胜 1000 个垃圾
□ 字典: 提供格式关键字(-x dict)→ 深度探索结构化输入
□ 白名单模式: 只 fuzz 目标函数(cmplog/自定义 harness)
□ 多核并行: -M master / -S slave
□ 覆盖率停滞 → 换种子/换模式(QEMU↔源码)/加字典
□ 内存限制: -m none 避免 OOM 误判

39.4 符号执行辅助验证(补 fuzz 的盲区)

fuzz 的盲区: 深分支/特定 magic 值(如校验头 0xDEADBEEF 拦路)
符号执行: 符号化输入 → 自动构造满足深条件的输入

工具: angr(第 7 章)/ KLEE(有源码)/ Triton

场景:
  1. fuzz 卡在魔法值 → 符号执行跳过
  2. 验证"假设的漏洞点"是否可达(约束求解一条路径)
  3. 生成 PoC 输入(让程序走特定分支)

39.5 常见漏洞类的 fuzz/审计特征

漏洞类 审计信号 fuzz 信号
栈溢出 大拷贝进小缓冲 长输入崩溃(栈被破坏)
堆溢出 长度不受控的堆分配拷贝 崩溃在 free/分配器
UAF 释放后仍引用 时序敏感崩溃
整数溢出 计算后无校验 超大输入值崩溃
命令注入 字符串拼接 + system 特殊字符输入
逻辑漏洞 状态/权限检查缺失 异常状态序列

39.6 内核 fuzz(进阶)

syzkaller: 内核 syscall fuzz(Google)
  - 生成随机 syscall 序列 → 触发内核路径
  - 崩溃 = 内核漏洞候选(配合第 27 章分析)

AFL++ 内核模式: QEMU + 内核镜像
范围: 驱动 ioctl(构造随机 ioctl 参数)、文件系统、网络栈

39.7 漏洞确认与分级(发现之后)

1. 复现: 确定性复现(相同输入相同崩溃)
2. 分类: 溢出/UAF/逻辑/注入?
3. 可控性: 崩溃点是否受输入控制(RIP 可控?)
4. 可达性: 攻击面(远程?本地?认证后?)
5. 保护绕过评估: ASLR/NX/Canary 对利用的影响
6. 产出: PoC + 利用路径评估 → 转第 18 章

动手练习

  1. 写一个含栈溢出的程序(gets 读入),用 AFL++ fuzz 它,找到崩溃输入并用 GDB 复现。
  2. 对一个 C 程序做审计扫描:列出所有危险函数调用点,评估输入可达性。
  3. 给一个 fuzz 目标准备高质量种子集(10 个),对比只用 1 个种子的覆盖率差异。
  4. 用 angr 构造一个输入,让程序进入"魔法值校验"深分支(fuzz 达不到的路径)。

深入阅读

  • 仓库:skills/firmware-pentest/references/emulation-and-fuzz.md(AFL++ 固件 fuzz)
  • 仓库:skills/pwn-chain/SKILL.md(漏洞点到 exploit 的衔接)
  • 仓库:skills/pwn-chain/references/heap-pwn.md(堆漏洞利用)
  • 仓库:skills/reverse-engineering/tools-dynamic.md(angr 符号执行辅助)
  • 仓库:CTF-Sandbox-Orchestrator/competition-reverse-pwn/(逆向+Pwn CTF)