AFL(American Fuzzy Lop)是由安全研究员Michał Zalewski(@lcamtuf)开发的一款基于覆盖引导(Coverage-guided)的模糊测试工具。它通过在程序插桩以记录代码覆盖率(Code Coverage),并据此调整输入样本,从而更高效地探索程序路径,提升发现漏洞的概率。
其核心工作流程可以概括为:
- 从源码编译程序时进行插桩,以记录代码覆盖率。
- 选择一些输入文件,作为初始测试集加入输入队列(queue)。
- 将队列中的文件按一定的策略进行“突变”。
- 如果变异后的文件触发了新的代码覆盖,则将其保留并添加到队列中。
- 上述过程会一直循环进行,期间所有触发程序崩溃(crash)的文件都会被记录下来。

一、AFL安装与基础测试
1. 安装AFL
首先下载AFL源码并编译安装。对于更优的性能,可以安装llvm_mode。
下载源码:

执行编译:

安装llvm模式(可选):

最后进行安装:

2. AFL基础测试
我们用一个有缺陷的C程序来验证AFL是否工作正常。
首先,下载测试用的C文件:

使用AFL提供的编译器进行插桩编译:
afl-gcc -o test_afl test.c

准备一个初始的种子语料库,例如一个简单的文本文件:

开始模糊测试:
afl-fuzz -i in -o out -- ./test_afl @@

如果系统提示需要修改 /proc/sys/kernel/core_pattern,请按照提示执行相应命令(通常为 echo core | sudo tee /proc/sys/kernel/core_pattern)。
修改后再次运行afl-fuzz,即可看到正常的Fuzzing状态界面:

出现该界面即表示AFL安装成功。如果状态栏出现 (odd, check syntax!) 提示,通常意味着初始语料库完全无法被程序解析,需要调整或提供更有效的种子文件。
使用 Ctrl+C 中断测试后,可以在输出目录(out/)中查看测试结果,包括触发的崩溃用例、覆盖路径等信息。

3. 并行Fuzz测试
AFL的每个 afl-fuzz 进程会占用一个CPU核心。在多核主机上,我们可以启动多个实例进行并行测试,以提升测试效率。
首先,查看CPU核心数:
grep -c ^processor /proc/cpuinfo

上图显示有4个核心,意味着我们可以同时运行最多4个实例。并行时,需要指定一个主实例(-M)和若干个从实例(-S)。
- 主实例命令:
afl-fuzz -M master -i in/ -o out/ -m none -- ./target @@
- 从实例命令:
afl-fuzz -S slave1 -i in/ -o out/ -m none -- ./target @@

执行后,在输出目录 out/ 下会生成 master 和 slave1 等文件夹,分别存放各自实例的进度和发现。
如果尝试启动超过核心数的实例(例如在第5个终端启动),后续实例会因无法获取CPU资源而报错,但不影响已运行的实例。

二、实战:对libjpeg-turbo进行模糊测试
libjpeg是处理JPEG图像编解码的流行库,libjpeg-turbo 是其性能优化的分支。对其进行安全测试是检验AFL实战能力的好例子。
1. 编译插桩版本的libjpeg-turbo
首先下载 libjpeg-turbo 源码。为了进行覆盖引导的模糊测试,我们需要修改其CMakeLists.txt,在 cmake_minimum_required 命令下方添加编译器选项,强制使用AFL的编译器进行插桩编译:
# 在文件靠前位置添加,避免被覆盖
set(CMAKE_C_COMPILER /usr/local/bin/afl-gcc)
set(CMAKE_CXX_COMPILER /usr/local/bin/afl-g++)

随后,在源码目录中执行编译安装:
mkdir build && cd build
cmake ..
make
sudo make install

编译完成后,build 目录内容如下:

为了测试库是否安装成功,并作为Fuzz目标,我们编写一个简单的测试程序(例如 test_turbo.c),调用库函数对JPEG图片进行压缩处理。
理想情况下,由于我们修改了CMakeLists.txt,库文件应该已被插桩。但在实际测试时发现,动态链接的方式可能未成功传递插桩信息。通过研究源码自带的测试项目,我们找到了静态链接的编译方法:

据此,我们使用静态链接方式编译自己的测试程序:
afl-gcc -static -o turbo_test test_turbo.c `pkg-config --libs --static libturbojpeg`

编译成功后,开始对静态链接的可执行文件进行模糊测试。我们启动了4个并行实例,总执行次数超过1亿次,这体现了覆盖率引导的高效性。测试结果表明,libjpeg-turbo 2.0版本的安全性非常 robust,未发现崩溃。

2. 结合内存错误检查工具(ASAN)
AddressSanitizer (ASAN) 是一种快速内存错误检测器。与AFL结合,可以在Fuzzing过程中更精准地发现内存漏洞。
首先,了解ASAN的基本用法。编译测试程序时添加 -fsanitize=address 选项:
g++ -fsanitize=address -fno-omit-frame-pointer -o test_asan vuln_demo.cpp
运行程序,ASAN会清晰地报告如 use-after-free、stack-buffer-overflow 等错误及其位置。

要在AFL中启用ASAN,需要在编译目标程序时设置环境变量:
AFL_USE_ASAN=1 make

注意,后续使用AFL测试该程序时,也需要在 afl-fuzz 命令中设置相应的ASAN环境变量(如 AFL_PRELOAD 指向ASAN运行时库),否则可能会报错。

3. 使用与构建自定义字典
AFL内置了字典,但为特定文件格式(如JPEG)构建自定义字典,能指导变异过程更快速地生成有效语法结构,提升效率。
AFL自带的JPEG字典示例:

我们可以分析JPEG文件格式,提取常见且关键的魔术字、标记符,构建自己的字典文件 jpeg.dict:
FFD8 “SOI”
FFE0 “APP0”
FFDB “DQT”
FFC0 “SOF0”
FFC4 “DHT”
FFDA “SOS”
FFD9 “EOI”

在 afl-fuzz 命令中使用 -x 参数指定字典文件:
afl-fuzz -i in -o out -x ./jpeg.dict -- ./turbo_test @@

4. 语料库蒸馏与精简
随着Fuzzing进行,语料库可能变得冗余。AFL提供了工具对其进行优化。
-
afl-cmin(语料库最小化):从大量输入文件中筛选出一个能保持相同代码覆盖率的最小集合。

-
afl-tmin(单个文件精简):在不影响覆盖路径的前提下,尽力减小单个输入文件的大小。默认是instrumented模式。有时精简策略可能导致文件变为0字节,这在某些情况下是正常的。
对于包含特殊字符的文件名或目录,可能需要编写脚本配合处理。
使用 -x 参数可切换至crash模式,该模式会直接剔除导致程序异常退出的输入。
5. 持久模式(Persistent Mode)
当Fuzzing的目标是大型程序中的一个独立函数时,持久模式可以避免重复执行程序初始化、解析输入等开销,极大提升测试速度。
其原理是在一个进程内多次循环调用目标函数。我们需要修改源码,在目标函数周围添加AFL的持久循环宏 AFL_PERSISTENT_LOOP。

同时,在编译时需要定义 AFL_PERSISTENT_MODE 宏。

重新编译库和测试程序后运行 afl-fuzz,可以发现执行速度相比普通模式有显著提升(有时可达数倍)。

6. 使用afl-cov进行代码覆盖率分析
afl-cov 能方便地整合 lcov/gcov,可视化展示AFL测试用例所覆盖的代码行和分支。
- GCOV:GCC套件的一部分,用于生成代码覆盖率数据。
- LCOV:GCOV的图形前端,生成HTML格式的覆盖率报告。
首先安装afl-cov(建议从源码安装):

要生成覆盖率数据,必须在编译目标程序时加上GCC覆盖率选项 -fprofile-arcs -ftest-coverage。

重新编译后,我们可以让afl-cov实时监控一个正在进行的 afl-fuzz 会话(--live 参数):
/home/user/Desktop/afl-cov/afl-cov -d out --live --enable-branch-coverage -c . -e "cat AFL_FILE | ./turbo_test" --overwrite

然后启动 afl-fuzz。当 afl-fuzz 停止后,afl-cov会生成详细的HTML报告。

报告首页展示了总体覆盖率统计:

点击具体源文件,可以查看每行代码被测试用例执行的次数,这对于查漏补缺、优化测试方向非常有帮助。

7. 使用后处理库(afl_postprocess)
后处理库允许我们自定义AFL生成的测试用例的格式。例如,可以强制所有变异出的文件都以特定的文件头开始。
参考官方示例,一个后处理库函数可能形如:
u8* afl_postprocess(const u8* in_buf, u32* len) {
static u8 header[] = "GIF89a";
// ... 处理逻辑,将header拼接到in_buf前 ...
return out_buf;
}

将其编译为动态库:
gcc -shared -Wall -O3 post_library.so.c -o post_library.so
AFL通过环境变量 AFL_POST_LIBRARY 来加载后处理库。设置环境变量后启动 afl-fuzz,AFL会显示库加载成功的提示。
export AFL_POST_LIBRARY=/path/to/post_library.so

之后,生成的测试用例就会经过后处理函数的格式化。使用此功能时,需确保目标程序能够正确处理这种格式化后的输入。