第二十六讲:模糊测试基础

模糊测试,说白了就是「拿一堆乱七八糟的数据去砸你的程序,看它会不会崩」。我刚开始接触安全测试时,总觉得这方法太粗暴了——哪有这么测的?后来真遇到一个隐藏了三年的缓冲区溢出漏洞,就是被模糊测试揪出来的。嗯,从那以后我再也不敢小看它了。

模糊测试的原理

模糊测试的核心思想很简单:用非预期的输入触发程序异常。你想想看,程序员写代码时总会假设输入是「合理」的——长度不超过100字节啦,数值在0到255之间啦。但攻击者偏偏不按套路出牌。

模糊测试的工作流程大致分三步:

  1. 生成测试用例——构造各种畸形、随机、边界数据
  2. 执行目标程序——把用例喂给被测程序
  3. 监控异常——检测崩溃、断言失败、内存错误等

我在项目中遇到过最典型的案例:一个网络协议解析库,处理报文头部的长度字段时没有做校验。模糊测试器随机生成了一个长度为0xFFFF的报文,结果解析函数直接越界写入了堆内存。这种漏洞,人工代码审查很难发现,但模糊测试跑一晚上就能逮到。

关键认知:模糊测试不是「乱测」,而是有策略的「乱测」。好的模糊测试器会利用覆盖率反馈来指导变异方向,而不是盲目生成随机数据。

LibFuzzer 的使用

LibFuzzer 是 LLVM 项目自带的模糊测试库。它跟被测代码链接在一起,直接调用你写的测试入口函数。我个人习惯用它来测那些有明确输入接口的函数——比如解析器、解码器、校验函数。

先看一个最简单的例子。假设我们要测试一个解析 CSV 行的函数:

// csv_parser.c
#include <stdint.h>
#include <stddef.h>

extern int parse_csv_line(const char *data, size_t len);

int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) {
    // 把模糊测试数据直接传给被测函数
    parse_csv_line((const char *)Data, Size);
    return 0;
}

编译时加上 LibFuzzer 的 flag:

clang -fsanitize=fuzzer,address csv_parser.c -o csv_fuzzer

然后直接运行:

./csv_fuzzer -max_len=1024 -runs=1000000

这里有几个参数我建议你记住:

参数 作用 我的建议值
-max_len 限制输入最大长度 根据接口合理设置,别太大
-runs 指定测试轮数 先跑100万轮看看效果
-timeout 单次执行超时 5秒,防止死循环
-jobs 并行任务数 CPU核心数减1

小技巧:LibFuzzer 默认会无限跑下去,直到你按 Ctrl+C。我一般先设一个合理的 -runs 值,看看覆盖率增长曲线。如果曲线还在上升,就继续加轮数。

AFL(American Fuzzy Lop)的使用

AFL 是另一款经典的模糊测试工具。跟 LibFuzzer 不同,AFL 是独立运行的——它启动被测程序,通过 stdin 或文件传递输入。我记得第一次用 AFL 时,被它的「仪表盘」界面惊艳到了——实时显示执行速度、路径数、崩溃数,非常直观。

AFL 的使用步骤:

  1. 用 afl-gcc 或 afl-clang 编译被测程序
  2. 准备初始种子输入(哪怕只有一个文件也行)
  3. 运行 afl-fuzz 开始测试

编译示例:

afl-clang -fsanitize=address csv_parser.c -o csv_parser_afl

启动测试:

afl-fuzz -i seeds/ -o findings/ ./csv_parser_afl @@

这里的 @@ 表示 AFL 会把生成的测试用例文件名替换进去。如果被测程序从 stdin 读数据,就不用加 @@。

注意:AFL 对初始种子质量很敏感。我曾经拿一个空文件做种子,结果 AFL 跑了三天都没发现新路径。后来换成几个典型的合法输入,覆盖率立刻飙升。种子文件别太「干净」,稍微带点变化效果更好。

测试用例生成策略

测试用例怎么生成?这里我总结了几种常用策略:

  • 随机变异——对合法输入做位翻转、字节替换、块删除/复制。最简单,但效率一般。
  • 基于语法——如果你知道输入格式(比如 JSON、XML),可以构造语法正确的畸形数据。比如 JSON 里把数字改成字符串,或者嵌套深度设到100层。
  • 基于覆盖率——这是 LibFuzzer 和 AFL 的核心策略。它们会记录哪些代码路径被执行过,然后优先变异那些能触发新路径的用例。
  • 基于污点分析——跟踪输入数据在程序中的传播路径,精准定位哪些字节影响了分支判断。这个比较高级,我一般在挖漏洞时才用。

我个人最常用的是「覆盖率引导 + 随机变异」的组合。说白了,让工具自己探索新路径,同时保留一定的随机性来突破边界。

知识体系总览

下面这张图梳理了模糊测试的核心逻辑,从输入生成到异常检测的完整链路:

模糊测试核心流程 测试用例生成 随机变异 / 语法生成 执行目标程序 LibFuzzer / AFL 异常监控 崩溃 / 断言 / 内存错误 覆盖率反馈(指导变异方向) 关键策略 • 初始种子:准备3-5个典型合法输入 • 变异策略:位翻转 → 字节替换 → 块操作 → 语法感知 • 并行加速:多核同时跑,共享发现的新用例 • 结合 Sanitizer:AddressSanitizer / UBSan 捕获更多异常 图:模糊测试核心流程与关键策略

避坑指南

做模糊测试这些年,我踩过不少坑。挑几个典型的说说:

  • 别用 Debug 版本做模糊测试。我曾经拿一个带大量 assert 的 Debug 版跑了整晚,结果崩溃报告全是 assert 触发——这些在 Release 版里根本不存在。一定要用 Release 版 + Sanitizer 的组合。
  • 注意磁盘空间。AFL 会把所有测试用例都保存下来。我见过有人跑了一周,磁盘被几十万个用例撑爆。建议定期清理,或者用 -c 参数压缩存储。
  • 别忽略超时。有些漏洞不会直接崩溃,而是让程序陷入死循环。LibFuzzer 的 -timeout 参数一定要设,我习惯设5秒。
  • 种子文件别太大。一个10MB的种子文件,变异起来效率极低。我一般把种子控制在1KB以内,必要时拆分成多个小文件。

我的经验:模糊测试不是「跑就完事」的工具。你得学会看覆盖率报告,分析哪些代码路径还没覆盖到,然后针对性地补充种子或调整变异策略。说白了,模糊测试是「半自动化」——工具帮你跑,但策略得你来定。

好了,这一讲的内容就到这里。模糊测试是个实践性很强的领域,建议你拿自己的项目试试手——先跑 LibFuzzer,再试试 AFL,对比一下效果。你会发现,那些你以为「不可能出问题」的代码,往往藏着最意想不到的惊喜。


公众号:蓝海资料掘金营,微信deep3321