跳转至

Fuzz Testing

Introduction

Fuzz的目的是驱动系统执行进入期望的状态。

Evolution

基因算法

基因算法是一种模拟自然选择的算法。将初始随机种子输入到种子池,通过选择和变异得到种子,以这个种子作为测试用例,执行程序,得到结果,根据结果判断是否是好的种子,如果是,加入到种子池,否则丢弃。

Coverage

如果fuzzer生成了一个能够扩大代码覆盖率的测试用例,那么这个测试用例就是好的种子。

覆盖分为Block Coverage(基本块覆盖)、Branch Coverage(分支覆盖)、Return Coverage(返回覆盖)、Path Coverage(路径覆盖)等。

如果我们可以获得一组覆盖程序控制流图(CFG)中每条可行路径的输入集,那么这个程序就被认为是饱和测试的。

路径覆盖是最全面的覆盖,但在实际中很难实现。在实践中,我们更多是采取分支覆盖,因为它在有效性和可行性之间取得了良好的平衡。

Loops: Trouble Maker for Branch Coverage

循环会影响分支覆盖的测试,应当使用Bounded Loop Unrolling技术。把循环变成一个一定次数的顺序执行的代码块。

Concolic execution: forced path exploration

Narrow-range constraints

对于一些约束很窄的情况,使用随机的输入很难找到满足约束的输入。但从另一方面,如果我们知道约束的范围,我们可以使用符号执行来找到满足约束的输入。

让fuzz完成大部分的状态空间探索,然后使用符号推理引擎来解决剩下的约束。


Fuzz 测试工具

AFL (American Fuzzy Lop)

AFL 是最经典的覆盖率引导模糊测试工具,由 Michal Zalewski 开发。

AFL 工作原理

AFL 使用 编译时插桩(compile-time instrumentation)来获取代码覆盖率信息:

  1. 在编译时注入 instrumentation 代码(通过 afl-gccafl-clang
  2. 维护一个 共享内存 bitmap(64KB),记录边覆盖率(edge coverage)
  3. 每个分支用 (prev_block << 16) | cur_block 哈希作为 bitmap 索引
  4. 如果发现新的 bitmap 命中,将该输入加入种子队列
flowchart TD
    A["种子队列"] --> B["选择种子"]
    B --> C["变异输入"]
    C --> D["执行目标程序(插桩)"]
    D --> E{"新覆盖率?"}
    E -->|是| F["加入种子队列"]
    E -->|否| G["丢弃"]
    F --> A
    G --> A
  • Bit Flip:翻转 1/2/4/8 个比特
  • Byte Flip:翻转 1/2/4/8 个字节
  • Arithmetic:对整数进行加减运算(+1, -1, +2, -2 等小值)
  • Dictionary:替换为已知的特殊值(如 0, 0xFF, 0xFFFF
  • Havoc:随机组合多种变异,适合探索复杂输入
  • Splicing:将两个种子拼接
  • Fork Server 模式:避免每次执行都 fork 整个进程
  • 共享内存 bitmap:高效的覆盖率记录
  • 自动字典推断:从二进制中提取常量字符串
  • 持久模式(persistent mode):在进程内循环执行,大幅提高速度

LibFuzzer

LibFuzzer 是 LLVM 项目内置的进程内模糊测试引擎。

LibFuzzer 与 AFL 的区别
特性 AFL LibFuzzer
执行方式 进程级(fork) 进程内(in-process)
插桩方式 编译时插桩 Sanitizer 插桩(ASan/MSan/UBSan)
目标格式 独立可执行文件 需要编写 harness 函数
速度 较快(fork server) 极快(无 fork 开销)
内存检测 需要配合 ASan 内置 Sanitizer 支持
适用场景 黑盒/灰盒测试 库函数/API 测试

LibFuzzer 的 harness 函数签名:

C
1
2
3
4
5
6
7
8
// LLVMFuzzerTestOneInput 是 LibFuzzer 的入口函数
// data: 模糊器生成的输入数据
// size: 输入数据的长度
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
    // 调用被测函数
    MyFunction(data, size);
    return 0;  // 返回值必须为 0
}

Honggfuzz

Honggfuzz 是 Google 开发的另一个覆盖率引导模糊测试工具,支持 硬件辅助覆盖率收集

Honggfuzz 的独特之处
  • 支持硬件性能计数器(Intel PT、Branch)作为覆盖率反馈
  • 支持 POSIX 信号处理,可以测试多线程程序
  • 支持持久模式和外部模式
  • 内置 Intel PT 解码器,无需内核模块

覆盖率引导原理详解

覆盖率引导是现代 fuzzer 的核心思想。其基本假设是:能够触发新代码路径的输入更有可能发现 bug

覆盖率收集机制
  • AFL 使用 (prev_block_id << 16) | cur_block_id 作为边标识
  • 边覆盖率比块覆盖率更细粒度,能区分 A->BB->A
  • 使用哈希表(bitmap)存储,碰撞概率可接受
  • 记录完整的执行路径(所有分支的组合)
  • 理论上最精确,但状态空间爆炸
  • 实践中通常用边覆盖率近似
  • 考虑调用栈信息,区分不同调用路径到达的同一分支
  • 能发现更多深层 bug,但开销更大
  • 代表:AFLGo(定向模糊测试)
覆盖率引导的局限性
  • Magic Number 问题:难以通过随机变异跳过 if (x == 0xDEADBEEF) 这样的检查
  • 校验和问题:CRC、MD5 等校验和会阻断变异
  • 复杂数据结构:JSON、XML 等结构化输入难以随机变异
  • 解决方案:字典辅助结构感知变异符号执行辅助

变异策略详解

变异策略是 fuzzer 生成新测试用例的核心。好的变异策略应该在 探索性利用性 之间取得平衡。

常见变异策略
  • 按固定规则逐一变异输入的每个字节/比特
  • 优点:可重复,适合小输入
  • 缺点:输入较大时组合爆炸
  • 例子:bit flip、byte flip、arithmetic
  • 随机选择多个变异操作并组合
  • 优点:探索范围广,能发现意外路径
  • 缺点:盲目性较大
  • AFL 的 havoc 阶段会随机组合 2~8 次变异
  • 使用预定义的 token 列表替换或插入输入
  • 优点:能跳过 magic number 检查
  • 缺点:需要人工或自动推断字典
  • AFL++ 支持从二进制中自动提取字典
  • 理解输入的语法结构,只在合法位置变异
  • 优点:能绕过解析器的格式检查,深入测试业务逻辑
  • 缺点:需要为每种格式编写变异器
  • 代表:libprotobuf-mutator(Protocol Buffer 格式)

实际案例

CVE-2014-0160 (Heartbleed)
  • OpenSSL 的 TLS Heartbeat 扩展存在缓冲区越界读取漏洞
  • Fuzzer 可以通过变异 heartbeat 包的 payload_length 字段触发
  • 覆盖率引导:正常的 heartbeat 包不会触发新路径,但篡改长度字段会进入异常处理分支
  • 教训:fuzzer 需要字典辅助 才能生成有效的 TLS 协议消息
ImageTragick (CVE-2016-3714)
  • ImageMagick 的图像处理库存在多个漏洞(命令注入、文件读写)
  • Fuzzer 通过变异图像文件格式触发
  • 覆盖率引导:不同的图像格式(PNG、JPEG、SVG)会触发不同的解析路径
  • 教训:结构感知变异 对于格式解析库至关重要
Chrome 渲染引擎漏洞
  • Google 使用 libFuzzer + AddressSanitizer 测试 Chrome 的渲染引擎
  • 通过编写 harness 函数测试 HTML/CSS/JS 解析器
  • 发现了大量内存安全漏洞(越界读写、use-after-free)
  • 教训:Sanitizer 组合(ASan + MSan + UBSan)能大幅提高漏洞检出率

Fuzz 测试最佳实践

编写高质量的 Fuzz Harness
  1. 保持 harness 简单:只调用一个或少数几个函数
  2. 使用 Sanitizer:编译时启用 ASan、MSan、UBSan
  3. 提供种子语料库:用真实的输入文件作为初始种子
  4. 限制执行时间:设置超时避免无限循环
  5. 清理状态:每次执行后释放所有分配的内存
  6. 避免全局状态:确保每次执行是独立的
提高 Fuzz 效率
  1. 使用字典:为协议/格式提供关键词字典
  2. 覆盖率去重:只保存触发新覆盖率的输入
  3. 并行执行:多实例并行 fuzz,共享种子队列
  4. 定向变异:针对特定代码路径进行定向 fuzz(如 AFLGo)
  5. 持续集成:将 fuzz 测试集成到 CI/CD 流程中