AFL++
AFL++ 是原始 AFL 模糊测试器的一个分支,在保持稳定性的前提下,提供了更优的模糊测试性能和更多高级特性。相比于 libFuzzer,其一个主要优势在于对多核模糊测试战役的稳定支持,使其成为大规模模糊测试工作的理想选择。
何时使用
| 模糊测试器 | 最佳适用场景 | 复杂度 |
|---|---|---|
| AFL++ | 多核模糊测试、多样化变异、成熟项目 | 中 |
| libFuzzer | 快速搭建、单线程、简单的 harness | 低 |
| 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 .
从源码编译
参考 Dockerfile 了解 Ubuntu 版本要求和依赖项。设置 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 -i \
--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++
下面的示例使用 docker 模式,但系统配置命令除外(它们需要访问宿主机内核)。将 docker 替换为 host 即可在本地安装了 AFL++ 的机器上运行任意命令。包装脚本将模式参数之后的所有内容连接成一个 shell 字符串,因此引号不会保留:包含空格的参数(如 -x "my dict.dict")会被分词处理。给此类文件重命名以消除空格,或为此次运行编辑包装脚本。
缺少 -t 是有意为之。当标准输入不是终端时,docker run -ti 会因 the input device is not a TTY 而中止,这包括 CI 任务和任何由代理或脚本驱动的情况。afl-fuzz 会注意到没有终端,并打印纯文本状态行来代替全屏用户界面。而需要终端才能运行的程序,例如 watch,则必须在包装脚本的宿主机端运行。$$ 会展开为包装脚本的 PID,这样并行实例会获得不同的容器名称,而不会在单个 afl_fuzzing 上发生冲突。docker ps 会截断 COMMAND 列,因此每一行看起来都一样;在停止某个实例之前,请使用 docker ps --no-trunc 来区分它们。
安全警告: afl-system-config 和 afl-persistent-config 脚本需要 root 权限,并且会禁用操作系统安全特性。请勿在生产系统或开发环境中进行模糊测试。请使用专用的虚拟机。
系统配置
每次重启后运行,可提高最多 15% 的每秒执行次数:
./afl++ host afl-system-config
afl-system-config 会调整其运行所在的内核,因此请在其主机上运行。./afl++ docker afl-system-config 通过包装脚本的 --privileged 容器来达到相同的设置,这是仅通过 Docker 安装 AFL++ 时的唯一途径。
为了获得最大性能,请禁用内核安全缓解措施(需要 grub 引导加载程序,Docker 中不支持):
./afl++ host afl-persistent-config
update-grub
reboot
./afl++ host afl-system-config
通过 cat /proc/cmdline 验证——输出应包含 mitigations=off。
编写 Harness
Harness 结构
AFL++ 支持类似 libFuzzer 的 harness:
#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;
}
Harness 规则
| 要做 | 不要做 |
|---|---|
| 在运行之间重置全局状态 | 依赖之前运行的状态 |
| 优雅地处理边界情况 | 在无效输入时退出 |
| 保持 harness 确定性 | 使用随机数生成器 |
| 释放已分配的内存 | 造成内存泄漏 |
| 验证输入大小 | 处理无界输入 |
另请参阅: 关于详细的 harness 编写技术、处理复杂输入的模式以及高级策略,请参阅 fuzz-harness-writing 技术技能。
编译
AFL++ 提供了多种编译模式,各有不同的权衡。
编译模式决策树
选择你的编译模式:
- LTO 模式 (
afl-clang-lto):最佳性能和插桩。首选此模式。 - LLVM 模式 (
afl-clang-fast):如果 LTO 编译失败,则回退到此模式。 - GCC 插件 (
afl-gcc-fast):适用于需要 GCC 的项目。
基本编译(LLVM 模式)
./afl++ docker afl-clang-fast++ -DNO_MAIN=1 -O2 -fsanitize=fuzzer harness.cc main.cc -o fuzz
GCC 编译
./afl++ docker afl-g++-fast -DNO_MAIN=1 -O2 -fsanitize=fuzzer harness.cc main.cc -o fuzz
重要提示: GCC 版本必须与用于编译 AFL++ GCC 插件的版本匹配。
使用 Sanitizers
./afl++ 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 harness 时跳过 main 函数 |
-O2 | 生产优化级别(推荐用于模糊测试) |
-fsanitize=fuzzer | 启用 libFuzzer 兼容模式,并在链接可执行文件时添加模糊器运行时 |
-fsanitize=fuzzer-no-link | 进行插桩但不链接模糊器运行时(用于静态库和目标文件) |
语料库管理
创建初始语料库
AFL++ 至少需要一个非空的种子文件:
mkdir seeds
echo "aaaa" > seeds/minimal_seed
对于真实项目,收集有代表性的输入:
- 为你正在模糊测试的格式下载示例文件
- 从项目的测试套件中提取测试用例
- 为你的文件格式使用最小的有效输入
语料库最小化
在战役结束后,最小化语料库以仅保留唯一的覆盖:
./afl++ docker afl-cmin -i out/default/queue -o minimized_corpus -- ./fuzz
另请参阅: 关于语料库创建策略、字典和种子选择,请参阅 fuzzing-corpus 技术技能。
运行战役
基本运行
./afl++ docker afl-fuzz -i seeds -o out -- ./fuzz
设置环境变量
./afl++ docker AFL_FAST_CAL=1 afl-fuzz -i seeds -o out -- ./fuzz
解读输出
AFL++ 总是报告这些统计信息,但读取方式取决于模式。包装脚本的 docker run -i 没有为容器提供 TTY,因此 afl-fuzz 会放弃全屏用户界面,而是将纯文本状态行写入日志——以下字段会出现在日志中,以及 state/<instance>/fuzzer_stats 文件中。要获得交互式用户界面,请在终端中以 host 模式运行,或者在你手动监控运行时,向包装脚本添加 -t。
| 输出 | 含义 |
|---|---|
| execs/sec | 执行速度——越高越好 |
| cycles done | 完成的队列遍历次数 |
| corpus count | 队列中唯一测试用例的数量 |
| saved crashes | 找到的唯一崩溃的数量 |
| stability | 稳定边的百分比(应接近 100%) |
输出目录结构
out/default/
├── cmdline # SUT 是如何被调用的?
├── crashes/ # 使 SUT 崩溃的输入
│ └── id:000000,sig:06,src:000002,time:286,execs:13105,op:havoc,rep:4
├── hangs/ # 使 SUT 挂起的输入
├── 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++ docker afl-whatsup out
创建覆盖图。aflplusplus 镜像已经包含了 gnuplot-nox;在宿主机模式下,请先使用 apt install gnuplot 安装 gnuplot。
./afl++ docker afl-plot out/default out_graph/
重新执行测试用例
传递来自 out/default/crashes/ 的文件名之一:
./afl++ docker ./fuzz out/default/crashes/id:000000,sig:06,src:000002,time:286,execs:13105,op:havoc,rep:4
模糊器选项
| 选项 | 用途 |
|---|---|
-G 4000 | 最大测试输入长度(默认:1048576 字节) |
-t 1000 | 每个测试用例的超时时间(毫秒)(默认:1000ms) |
-m 1000 | 内存限制(兆字节)(默认:0 = 无限制) |
-x ./dict.dict | 使用字典文件来指导变异 |
重要的环境变量
AFL++ 有许多环境变量,但大多数是专业用途的。以下是实践中比较重要的那些。
始终设置这些
# 每个战役都应使用 tmpfs —— SSD 会感谢你,而且速度更快
AFL_TMPDIR=/dev/shm
AFL_TMPDIR 是一个免费的性能提升,没有任何副作用——不设置它会磨损你的 SSD 并减慢模糊测试速度。
慢速目标
# 将校准加速约 2.5 倍 —— 当目标较慢时使用(例如 >10 ms/exec)
AFL_FAST_CAL=1
AFL_FAST_CAL 以减少可忽略的精度损失为代价,减少了校准时间。特别推荐用于校准耗时较长的慢速目标。
多核战役
# 仅在主实例 (-M) 上设置 —— 需要用于 afl-cmin,而不是为了模糊测试本身
AFL_FINAL_SYNC=1
# 在所有实例上设置 —— 在内存中缓存测试用例(默认:50 MB,良好范围:50-250 MB)
AFL_TESTCACHE_SIZE=100
AFL_FINAL_SYNC 告诉主实例在停止时从所有次级实例执行最终的导入。这不会影响模糊测试过程本身——它只在你稍后运行 afl-cmin 进行语料库最小化时才重要,确保主实例的队列拥有完整的合并语料库。AFL_TESTCACHE_SIZE 将测试用例缓存在内存中以减少磁盘 I/O;默认为 50 MB,50-250 MB 之间的值对大多数战役效果良好。
CI/自动化模糊测试
# 如果模糊测试没有发现任何东西,则快速失败
AFL_EXIT_ON_TIME=3600 # 1 小时内没有新路径 = 停止
# 或者运行直到“完成”(所有队列条目已被处理)
AFL_EXIT_WHEN_DONE=1
# 无头环境
AFL_NO_UI=1
在 CI 中进行无界模糊测试会浪费资源。设置时间限制或使用退出条件。
应避免的变量
| 变量 | 为什么跳过 |
|---|---|
AFL_NO_ARITH | 可能损害对二进制格式的覆盖,但对基于文本的目标可能有用 |
AFL_SHUFFLE_QUEUE | 仅用于特殊设置,通常有害 |
AFL_DISABLE_TRIM | 剪裁很有价值,没有理由不要禁用 |
多核模糊测试
AFL++ 在多核模糊测试方面表现出色,有两个主要优势:
- 更高的每秒执行次数(随物理核心数线性扩展)
- 非对称模糊测试(例如,一个 ASan 任务,其余不带 sanitizer)
启动战役
启动主模糊器(后台):
./afl++ docker afl-fuzz -M primary -i seeds -o state -- ./fuzz 1>primary.log 2>primary.error </dev/null &
启动次级模糊器(数量与核心数相同):
./afl++ docker afl-fuzz -S secondary01 -i seeds -o state -- ./fuzz 1>secondary01.log 2>secondary01.error </dev/null &
./afl++ docker afl-fuzz -S secondary02 -i seeds -o state -- ./fuzz 1>secondary02.log 2>secondary02.error </dev/null &
</dev/null 是必需的,并非装饰。docker run -i 保持客户端读取自己的标准输入,而一个后台进程如果读取终端,则会在尝试时被发送 SIGTTOU,并因 I/O error 而失败。
监控战役
使用 afl-whatsup 查看所有实例的状态:
./afl++ docker afl-whatsup state
要从特定实例获取实时统计信息:
cat state/primary/fuzzer_stats
停止战役
docker ps --no-trunc # 查找正在运行的容器名称
# 一次停止一个实例,或全部停止
docker ps -q --filter name="afl_fuzzing*" | xargs docker stop
注意 docker ps --no-trunc。包装脚本生成的每个容器名称都是唯一的,带有 $$,而 docker ps 默认会截断名称,使得它们看起来相同。使用 --no-trunc 可以显示完整的容器名称,从而可以精确停止特定实例。
崩溃分类
模糊测试战役会发现很多崩溃。afl-crash-analyzer 能帮助你理解它们。它重新运行每个输入,设置各种信号处理程序,并记录崩溃位置和类型。
# Basic usage
./afl++ docker afl-crash-analyzer -i out/default/crashes/ -- ./fuzz
# Whole directory
./afl++ docker afl-crash-analyzer -i out/default/crashes/ -- ./fuzz
另请参阅: 关于使用工具(如 GDB、LLDB)进行深入崩溃调试和重现的技术,请参阅 debugging 技术技能。
高级持久模式
持久模式避免了为每个测试用例重新创建进程的开销。它需要一个循环来调用目标,并且 LLVMFuzzerTestOneInput 可能返回以控制继续。
LLVM 模式配置
# 使用 `afl-clang-fast`(或 `afl-clang-lto`),并链接 `AFLDriver`:
# (这启用了 AFL++ 的持久模式运行时)
./afl++ docker afl-clang-fast++ -DNO_MAIN=1 -O2 -fsanitize=fuzzer harness.cc -o fuzz_persistent
# 在没有 -N 的情况下运行,以启用持久模式
./afl++ docker afl-fuzz -i seeds -o persistent_out -- ./fuzz_persistent
持久模式提供了显著的加速(根据用例,通常为 2x-10x),因为你可以避免 fork 和 exec 开销。
装饰器
__AFL_FUZZ_INIT() 在调用任何代码之前直接在后端分配缓冲区。标准 fork 服务器模型在每次执行后退出。在持久模式下,while 循环包装器利用 __AFL_LOOP() 在 max_count 次迭代中重新使用进程。
// 示例:持久模式循环
#include "what_you_want.h"
__AFL_FUZZ_INIT();
int main(int argc, char **argv) {
#ifdef __AFL_HAVE_MANUAL_CONTROL
__AFL_INIT();
#endif
uint8_t *buf = __AFL_FUZZ_TESTCASE_BUF;
while (__AFL_LOOP(10000)) {
int len = __AFL_FUZZ_TESTCASE_LEN;
// 调用你的目标
target_function(buf, len);
}
return 0;
}
故障排除
| 症状 | 可能的原因 | 解决方案 |
|---|---|---|
| "Hmm, your system is configured to send core dump notifications..." | 核心转储处理程序干扰 | afl-system-config 会自动修复这个问题 |
| "Fatal error: fork() failed..." | 内存限制 (ulimit -v) | 使用 -m none 运行或增加限制 |
| 执行速度慢 | 未配置 AFL_TMPDIR | 设置 AFL_TMPDIR=/dev/shm(参见环境变量部分) |
| 编译失败(LTO 模式) | 编译器版本不匹配 | 回退到 afl-clang-fast(LLVM 模式) |
| Docker 错误 | 容器名称冲突 | 等待上一个容器停止,或使用不同的名称 |
| 检测到不稳定 | 全局状态污染 | 在 LLVMFuzzerTestOneInput 开始时重置全局状态 |