AFL++ 是做什么的?
AFL++ 是原始 AFL 模糊测试器的一个分支,在保持稳定性的同时,提供了更优的模糊测试性能和更高级的特性。相比于 libFuzzer,AFL++ 的主要优势在于对多核模糊测试活动具有稳定的支持,使其成为大规模模糊测试工作的理想选择。
何时使用
| 模糊测试器 | 最佳适用场景 | 复杂度 |
|---|---|---|
| AFL++ | 多核模糊测试、多样化变异、成熟项目 | 中等 |
| libFuzzer | 快速设置、单线程、简单测试桩 | 低 |
| LibAFL | 自定义模糊测试器、研究、高级用例 | 高 |
在以下情况选择 AFL++:
- 你需要多核模糊测试来最大化吞吐量
- 你的项目可以使用 Clang 或 GCC 编译
- 你想要多样化的变异策略和成熟的工具链
- libFuzzer 已经达到瓶颈,你需要更多覆盖率
- 你正在模糊测试受益于并行执行的生产级代码库
快速开始
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
// 使用模糊测试器提供的数据调用你的代码
check_buf((char*)data, size);
return 0;
}
编译并运行:
# 先设置 AFL++ 封装脚本(参见安装章节)
./afl++ docker afl-clang-fast++ -DNO_MAIN=1 -O2 -fsanitize=fuzzer harness.cc main.cc -o fuzz
mkdir seeds && echo "aaaa" > seeds/minimal_seed
./afl++ docker afl-fuzz -i seeds -o out -- ./fuzz
安装
AFL++ 有许多依赖项,包括 LLVM、Python 和 Rust。我们建议使用当前的 Debian 或 Ubuntu 发行版进行 AFL++ 模糊测试。
| 方法 | 何时使用 | 支持的编译器 |
|---|---|---|
| Ubuntu/Debian 仓库 | 较新的 Ubuntu,仅基本功能 | Ubuntu 23.10: Clang 14 & GCC 13Debian 12: Clang 14 & GCC 12 |
| Docker(从 Docker Hub) | 特定 AFL++ 版本,Apple Silicon 支持 | 截至 4.35c: Clang 19 & GCC 11 |
| Docker(从源码) | 测试未发布功能,应用补丁 | 可在 Dockerfile 中配置 |
| 从源码 | 避免 Docker,需要特定补丁 | 可通过 LLVM_CONFIG 环境变量调整 |
Ubuntu/Debian
在安装 afl++ 之前,使用 apt-cache show afl++ 检查包的 clang 版本依赖,并安装匹配的 lld 版本(例如 lld-17)。
apt install afl++ lld-17
Docker(从 Docker Hub)
docker pull aflplusplus/aflplusplus:stable
Docker(从源码)
git clone --depth 1 --branch stable https://github.com/AFLplusplus/AFLplusplus
cd AFLplusplus
docker build -t aflplusplus .
从源码编译
关于 Ubuntu 版本要求及依赖项,请参考 Dockerfile。设置 LLVM_CONFIG 来指定 Clang 版本(例如 llvm-config-18)。
封装脚本设置
创建一个封装脚本,用于在主机或 Docker 上运行 AFL++:
cat <<'EOF' > ./afl++
#!/bin/sh
AFL_VERSION="${AFL_VERSION:-"stable"}"
case "$1" in
host)
shift
bash -c "$*"
;;
docker)
shift
/usr/bin/env docker run -ti \
--privileged \
-v ./:/src \
--rm \
--name afl_fuzzing \
"aflplusplus/aflplusplus:$AFL_VERSION" \
bash -c "cd /src && bash -c \"$*\""
;;
*)
echo "Usage: $0 {host|docker}"
exit 1
;;
esac
EOF
chmod +x ./afl++
安全警告: afl-system-config 和 afl-persistent-config 脚本需要 root 权限,并且会禁用操作系统的安全特性。不要在生成系统或开发环境中进行模糊测试。请使用专用的虚拟机。
系统配置
每次重启后运行,可使每秒执行次数提升最多 15%:
./afl++ <host/docker> afl-system-config
为获得最大性能,禁用内核安全缓解措施(需要 grub 引导加载程序,Docker 中不支持):
./afl++ host afl-persistent-config
update-grub
reboot
./afl++ <host/docker> afl-system-config
使用 cat /proc/cmdline 验证——输出应包含 mitigations=off。
编写测试桩
测试桩结构
AFL++ 支持 libFuzzer 风格的测试桩:
#include <stdint.h>
#include <stddef.h>
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
// 1. 必要时验证输入大小
if (size < MIN_SIZE || size > MAX_SIZE) return 0;
// 2. 使用模糊测试数据调用目标函数
target_function(data, size);
// 3. 返回 0(非零值保留给将来使用)
return 0;
}
测试桩规则
| 应该做 | 不应该做 |
|---|---|
| 在每次运行之间重置全局状态 | 依赖上一次运行的状态 |
| 优雅处理边界情况 | 在无效输入时退出 |
| 保持测试桩确定性 | 使用随机数生成器 |
| 释放分配的内存 | 造成内存泄漏 |
| 验证输入大小 | 处理无界输入 |
另请参阅: 关于编写测试桩的详细技术、处理复杂输入的模式以及高级策略,请参见 fuzz-harness-writing 技术技能。
编译
AFL++ 提供了多种编译模式,各有不同的权衡。
编译模式决策树
选择你的编译模式:
- LTO 模式 (
afl-clang-lto):最佳性能和插桩。首先尝试此模式。 - LLVM 模式 (
afl-clang-fast):如果 LTO 编译失败,则回退到此模式。 - GCC 插件 (
afl-gcc-fast):适用于需要使用 GCC 的项目。
基本编译(LLVM 模式)
./afl++ <host/docker> afl-clang-fast++ -DNO_MAIN=1 -O2 -fsanitize=fuzzer harness.cc main.cc -o fuzz
GCC 编译
./afl++ <host/docker> afl-g++-fast -DNO_MAIN=1 -O2 -fsanitize=fuzzer harness.cc main.cc -o fuzz
重要提示: GCC 版本必须与编译 AFL++ GCC 插件时使用的版本一致。
使用 Sanitizer
./afl++ <host/docker> AFL_USE_ASAN=1 afl-clang-fast++ -DNO_MAIN=1 -O2 -fsanitize=fuzzer harness.cc main.cc -o fuzz
另请参阅: 关于详细的 sanitizer 配置、常见问题和高级标志,请参见 address-sanitizer 和 undefined-behavior-sanitizer 技术技能。
构建标志
注意 -g 不是必需的,AFL++ 编译器默认会添加。
| 标志 | 用途 |
|---|---|
-DNO_MAIN=1 | 使用 libFuzzer 测试桩时跳过 main 函数 |
-O2 | 生产优化级别(建议用于模糊测试) |
-fsanitize=fuzzer | 启用 libFuzzer 兼容模式,并在链接可执行文件时添加模糊测试运行时 |
-fsanitize=fuzzer-no-link | 进行插桩但不链接模糊测试运行时(用于静态库和目标文件) |
语料库管理
创建初始语料库
AFL++ 至少需要一个非空的种子文件:
mkdir seeds
echo "aaaa" > seeds/minimal_seed
对于真实项目,收集有代表性的输入:
- 下载你正在模糊测试的格式的示例文件
- 从项目的测试套件中提取测试用例
- 为你的文件格式使用最小的有效输入
语料库最小化
在模糊测试活动之后,最小化语料库以只保留唯一的覆盖率:
./afl++ <host/docker> afl-cmin -i out/default/queue -o minimized_corpus -- ./fuzz
另请参阅: 关于语料库创建策略、字典和种子选择,请参见 fuzzing-corpus 技术技能。
运行模糊测试活动
基本运行
./afl++ <host/docker> afl-fuzz -i seeds -o out -- ./fuzz
设置环境变量
./afl++ <host/docker> AFL_FAST_CAL=1 afl-fuzz -i seeds -o out -- ./fuzz
解释输出
AFL++ 用户界面显示实时模糊测试统计数据:
| 输出 | 含义 |
|---|---|
| execs/sec | 执行速度——越高越好 |
| cycles done | 完成的队列遍历次数 |
| corpus count | 队列中的唯一测试用例数 |
| saved crashes | 发现的唯一崩溃数 |
| stability | 稳定边的百分比(应接近 100%) |
输出目录结构
out/default/
├── cmdline # 被测系统是如何被调用的?
├── crashes/ # 使被测系统崩溃的输入
│ └── id:000000,sig:06,src:000002,time:286,execs:13105,op:havoc,rep:4
├── hangs/ # 使被测系统挂起的输入
├── queue/ # 重现最终模糊测试器状态的测试用例
│ ├── id:000000,time:0,execs:0,orig:minimal_seed
│ └── id:000001,src:000000,time:0,execs:8,op:havoc,rep:6,+cov
├── fuzzer_stats # 模糊测试活动统计数据
└── plot_data # 用于绘图的数据
分析结果
查看实时模糊测试活动统计数据:
./afl++ <host/docker> afl-whatsup out
创建覆盖率图:
apt install gnuplot
./afl++ <host/docker> afl-plot out/default out_graph/
重新执行测试用例
./afl++ <host/docker> ./fuzz out/default/crashes/<test_case>
模糊测试器选项
| 选项 | 用途 |
|---|---|
-G 4000 | 最大测试输入长度(默认:1048576 字节) |
-t 1000 | 每个测试用例的超时时间,单位毫秒(默认:1000ms) |
-m 1000 | 内存限制,单位兆字节(默认:0 = 无限制) |
-x ./dict.dict | 使用字典文件来指导变异 |
多核模糊测试
AFL++ 在多核模糊测试方面表现出色,有两个主要优势:
- 每秒更多的执行次数(与物理核心成线性比例)
- 非对称模糊测试(例如,一个 ASan 作业,其余不带 sanitizer)
启动一次模糊测试活动
启动主模糊测试器(在后台):
./afl++ <host/docker> afl-fuzz -M primary -i seeds -o state -- ./fuzz 1>primary.log 2>primary.error &
启动从模糊测试器(你有多少个核心就启动多少个):
./afl++ <host/docker> afl-fuzz -S secondary01 -i seeds -o state -- ./fuzz 1>secondary01.log 2>secondary01.error &
./afl++ <host/docker> afl-fuzz -S secondary02 -i seeds -o state -- ./fuzz 1>secondary02.log 2>secondary02.error &
监控多核模糊测试活动
列出所有正在运行的作业:
jobs
查看实时统计数据(每秒更新一次):
./afl++ <host/docker> watch -n1 --color afl-whatsup state/
停止所有模糊测试器
kill $(jobs -p)
覆盖率分析
AFL++ 通过边插桩自动跟踪覆盖率。覆盖率信息存储在 fuzzer_stats 和 plot_data 中。
测量覆盖率
使用 afl-plot 可视化覆盖率随时间的变化:
./afl++ <host/docker> afl-plot out/default out_graph/
提升覆盖率
- 使用字典进行格式感知的模糊测试
- 运行更长时间的模糊测试活动(
cycles_wo_finds表示平台期) - 通过多核模糊测试尝试不同的变异策略
- 分析覆盖率缺口并添加针对性的种子输入
另请参阅: 关于详细的覆盖率分析技术、识别覆盖率缺口和系统性覆盖率提升,请参见 coverage-analysis 技术技能。
CMPLOG
CMPLOG/RedQueen 是所有模糊测试器中可用的最佳路径约束求解机制。 要启用它,需要对模糊测试目标进行相应插桩。 在构建模糊测试目标之前设置环境变量:
./afl++ <host/docker> AFL_LLVM_CMPLOG=1 make
编译和链接测试桩不需要特殊操作。
要运行带有 CMPLOG 插桩模糊测试目标的模糊测试器实例,请在命令行参数中添加 -c0:
./afl++ <host/docker> afl-fuzz -c0 -S cmplog -i seeds -o state -- ./fuzz 1>secondary02.log 2>secondary02.error &
Sanitizer 集成
Sanitizer 对于发现不会立即导致崩溃的内存损坏错误至关重要。
AddressSanitizer (ASan)
./afl++ <host/docker> AFL_USE_ASAN=1 afl-clang-fast++ -DNO_MAIN=1 -O2 -fsanitize=fuzzer harness.cc main.cc -o fuzz
注意: 由于 ASan 保留了 20TB 的虚拟内存,因此不支持内存限制 (-m) 选项。
UndefinedBehaviorSanitizer (UBSan)
./afl++ <host/docker> AFL_USE_UBSAN=1 afl-clang-fast++ -DNO_MAIN=1 -O2 -fsanitize=fuzzer,undefined harness.cc main.cc -o fuzz
常见 Sanitizer 问题
| 问题 | 解决方案 |
|---|---|
| ASan 降低模糊测试速度 | 在多核设置中仅使用 1 个 ASan 作业 |
| 栈耗尽 | 使用 ASAN_OPTIONS=stack_size=... 增加栈大小 |
| GCC 版本不匹配 | 确保系统 GCC 与 AFL++ 插件版本匹配 |
另请参阅: 关于全面的 sanitizer 配置和故障排除,请参见 address-sanitizer 技术技能。
高级用法
技巧和窍门
| 技巧 | 为何有帮助 |
|---|---|
| 尽可能使用 LLVMFuzzerTestOneInput 测试桩 | 如果模糊测试活动至少有 85% 的稳定性,这是最高效的模糊测试风格。如果不是,则尝试标准输入或文件输入模糊测试 |
| 使用字典 | 帮助模糊测试器发现特定格式的关键字和 magic 字节 |
| 设置合理的超时时间 | 防止系统负载导致的误报 |
| 限制输入大小 | 更大的输入不一定探索更多空间 |
| 监控稳定性 | 低稳定性表明确定性行为不佳 |
标准输入模糊测试
AFL++ 可以在没有 libFuzzer 测试桩的情况下模糊测试从 stdin 读取的程序:
./afl++ <host/docker> afl-clang-fast++ -O2 main_stdin.c -o fuzz_stdin
./afl++ <host/docker> afl-fuzz -i seeds -o out -- ./fuzz_stdin
这比持久化模式慢,但不需要任何测试桩代码。
文件输入模糊测试
对于读取文件的程序,使用 @@ 占位符:
./afl++ <host/docker> afl-clang-fast++ -O2 main_file.c -o fuzz_file
./afl++ <host/docker> afl-fuzz -i seeds -o out -- ./fuzz_file @@
为了获得更好的性能,使用 fmemopen 从内存创建文件描述符。
参数模糊测试
使用 argv-fuzz-inl.h 模糊测试命令行参数:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifdef __AFL_COMPILER
#include "argv-fuzz-inl.h"
#endif
void check_buf(char *buf, size_t buf_len) {
if(buf_len > 0 && buf[0] == 'a') {
if(buf_len > 1 && buf[1] == 'b') {
if(buf_len > 2 && buf[2] == 'c') {
abort();
}
…
[为翻译而截断]