afl-fuzz 使用方法
AFL++ 是一款暴力模糊测试工具,它结合了一种极其简单但非常可靠的基于插桩技术完成覆盖率引导的遗传算法。AFL++ 采用一种改进的边覆盖率(edge coverage)度量,能够轻松捕捉到程序控制流中细微的、局部性的变化。
注:如果您想深入了解 AFL++ 的最新工作原理,我们推荐您阅读这篇博客文章:https://blog.ritsec.club/posts/afl-under-hood/
简而言之,afl-fuzz 的核心算法流程可概括为:
-
将用户提供的初始测试用例加载到输入队列中
-
从队列中取出下一个种子文件
-
尝试将测试用例裁剪至不影响程序测量行为的最小尺寸
-
使用一套均衡且经过充分研究的传统变异策略反复对种子文件进行变异
-
若生成的变异样本触发了插桩记录到新状态转换(即覆盖率提高),则将该变异样本作为新条目加入种子队列
-
go to 2
已发现的测试用例会定期进行筛选,剔除那些被更新、更高覆盖率样本所淘汰的冗余用例,同时还会经历若干其他基于插桩技术的优化步骤,以减少冗余工作。
作为模糊测试过程的附带结果,该工具会生成一套精简且自包含的优质测试用例集。这些测试用例对于驱动其他依赖大量人力或计算资源的测试流程极具价值——例如用于浏览器、办公软件、图形套件或闭源工具的压力测试。
afl-fuzz 经过充分验证,在开箱即用的性能表现上远超传统的盲模糊测试(blind fuzzing)或仅基于覆盖率的工具。
理解状态界面
本节提供了状态界面的概述,以及针对用户界面(UI)中显示的任何警告信息和红色提示文字的故障排除提示。
如需要通用使用手册,请参阅 README.md。
关于颜色显示的说明
状态屏幕和错误消息使用颜色来保持可读性,并帮助您快速定位关键信息。例如,红色文字通常表示“请参阅本文档”:-)
但需注意,只有当您的终端使用传统的 Unix 配色方案(黑底白字)或类似配色方案时,UI 才能正确渲染颜色。
如果您启用了反色显示(黑底白字),建议更改终端设置,例如:
对于 GNOME 终端,请转到“编辑 Edit”>“配置文件 Profile ”
首选项,选择“颜色 colors ”选项卡,然后从内置方案列表中选择“白底黑字 white on black ”。
对于 MacOS X 的终端应用,通过“Shell”>“新建窗口 New Window ”
菜单使用“ Pro ”配色方案打开一个新窗口(或将“ Pro ”设置为默认方案)。
若您坚持保留当前配色方案,您可以通过编辑 config.h 文件,将 USE_COLORS
注释掉,然后执行 make clean all
重新编译。
目前我们还没找到既简单又不会产生副作用的替代方案,对此我们深表歉意。
言归正传,让我们来谈谈状态界面上的具体内容……
状态栏
american fuzzy lop ++3.01a (default) [fast] {0}
状态栏第一行显示 afl-fuzz 当前运行模式(正常模式为 “ american fuzzy lop ”,崩溃探索模式为 “ peruvian rabbit mode ”)以及 AFL++ 的版本号。版本号旁边设有横幅标志——如果没有通过 -T
参数手动设置,则默认显示被测程序二进制文件名称,或者在并行模糊测试中则会显示 -M / -S
(主/次节点)名称。倒数第二项显示当前采用的功耗调度模式(默认为“ fast ”模式)。最后一项是 CPU ID.
进程计时
+----------------------------------------------------+
| run time : 0 days, 8 hrs, 32 min, 43 sec |
| last new find : 0 days, 0 hrs, 6 min, 40 sec |
| last uniq crash : none seen yet |
| last uniq hang : 0 days, 1 hrs, 24 min, 32 sec |
+----------------------------------------------------+
这一部分相当直观:它告诉您模糊测试工具已运行的总时长,以及自上次发现新结果(“路径 paths ” / 崩溃 / 挂起)以来的耗时统计。这里的“ paths ”是简写,指那些触发了全新执行路径的测试用例。
关于运行时间:没有严格规定,但大多数模糊测试任务都需要持续运行数天甚至数周。事实上,对于一个中等复杂度的项目,首轮测试通常就需要耗时一天左右。某些情况下,我们甚至会允许测试任务持续运行数月之久。
需要特别注意的情况:如果测试开始后几分钟内都没有发现新路径,可能存在以下问题:
-
目标二进制文件调用方式不正确,导致程序根本无法解析输入文件
-
默认内存限制(
-m
参数)设置过严,程序因无法分配缓冲区而提前退出 -
输入文件明显无效,总是无法通过基础文件头检查
如果长时间没有新路径出现,本区域最终也会显示醒目的红色警告提示。:-)
汇总结果
+-----------------------+
| cycles done : 0 |
| total paths : 2095 |
| uniq crashes : 0 |
| uniq hangs : 19 |
+-----------------------+
本区域首行显示当前已完成队列遍历轮次(queue passes)的计数——即模糊测试工具迄今为止对所有已发现的值得关注的测试用例进行完整测试(包括变异、执行、循环回到起点)的循环次数。每个模糊测试会话至少应完成一轮完整遍历,理想情况下应持续多个周期。
如前所述,首轮测试通常需要耗时一天以上,请耐心等待。
为了帮助判断何时按下 Ctrl-C
(中断测试),系统采用颜色编码的循环计数器。首轮遍历期间显示为洋红色;后续轮次若持续发现新路径则变为黄色;当新发现停止时转为蓝色;若长时间未检测到任何活动变化最终变为绿色。
该区域其余指标一目了然:目前已发现的测试用例(“ paths ”)总数、唯一性故障(崩溃 / 挂起)数量。
您可通过实时查看输出目录中的文件来检查测试用例、崩溃和挂起情况,具体请参阅 #interpreting-output.
轮次内过程
+-------------------------------------+
| now processing : 1296 (61.86%) |
| paths timed out : 0 (0.00%) |
+-------------------------------------+
本区域显示模糊测试工具在当前队列循环中的实时进度,它显示了当前正在处理的测试用例的 ID ,以及因持续超时而被自动丢弃的输入样本数量。
若首行末尾出现“ * ”标记,则表示当前处理的执行路径未被标记为“优先路径 favored ”(该特性将在后续章节详细说明)。
图覆盖率
+--------------------------------------+
| map density : 10.15% / 29.07% |
| count coverage : 4.03 bits/tuple |
+--------------------------------------+
本节介绍目标二进制文件中插桩代码观测到的覆盖率统计信息。
状态框首行显示当前输入样本触发的分支元组命中数与位图容量的比例关系。左侧数值表示当前测试用例的覆盖率,右侧数值反映整个测试语料库的总体覆盖率。
需要注意的极端情况:
-
绝对值低于 200 通常意味着三种可能:程序本身极其简单;插桩过程存在问题(例如链接了未插桩的目标库版本);测试用例导致程序提前退出(模糊测试器会以粉色高亮标注此异常)。
-
百分比超过 70%(极罕见情况):通常只发生在大量使用模板生成代码的复杂程序上。高位图密度会影响新状态识别,建议通过
AFL_INST_RATIO=10
或类似的设置重新编译测试并再次尝试(详见 env_variables.md )。模糊测试工具会以红色标记高危百分比。除非测试极端复杂软件(如 v8、perl、ffmpeg ),否则通常不会出现。
第二行数值反映分支元组命中次数的离散程度:
-
如果每条已执行分支在所有输入下都被执行完全相同的次数,该值就是
1.00
. -
当我们能让某些分支出现不同命中次数时,数值会逐渐向
8.00
(8 位图每个 bit 都被置位)靠近,但实际测试中几乎不会达到这个理论极限值。
总的来说,这两项指标可用于对比不同模糊测试任务在相同插桩二进制文件上的覆盖率差异。
阶段性进展
+-------------------------------------+
| now trying : interest 32/8 |
| stage execs : 3996/34.4k (11.62%) |
| total execs : 27.4M |
| exec speed : 891.7/sec |
+-------------------------------------+
这一部分深入展示了模糊测试工具当前的工作阶段,包含以下可能状态:
-
校准( calibration ):预处理( pre-fuzzing )阶段,用于检查执行路径以检测异常、建立基线执行速度等。在每次发现新路径时短暂执行。
-
修剪 L/S( trim L/S ):另一预处理阶段,将测试用例缩减为能触发相同执行路径的最短形式。修剪长度( L )和步进值( S )根据文件大小动态调整。
-
位翻转 L/S( bitflip L/S ):确定性位翻转。每次翻转 L 个比特,以 S 位为步进遍历整个输入。当前支持的 L/S 组合:
1/1
、2/1
、4/1
、8/8
、16/8
、32/8
。 -
算术 L/8( arith L/8 ):确定性算术运算。模糊测试工具尝试对 8 位、16 位或 32 位宽的值加减小整数,步进始终是 8 位。
-
应关注值 L/8( interest L/8 ):确定性值覆盖。模糊测试工具用一组已知的“值得关注的” 8 位、 16 位或 32 位的数值覆盖原始数据,步进是 8 位。
-
额外项( extras ):字典词条的确定性注入。显示为“ user ”(用户字典)或“ auto ”(自动生成)两种模式,取决于模糊测试工具使用的是用户字典(
-x
)还是自动生成的字典。同时也显示“ over ”(覆盖)或“ insert ”(插入)来表示操作方式,取决于字典单词是覆盖现有数据还是通过偏移剩余数据来插入以适应其长度。 -
混乱( havoc ):叠加随机变换的固定长度循环。此阶段尝试的变换操作包括比特翻转、随机/“值得关注的”数值覆盖、块删除、块复制以及字典相关操作(若提供字典)。
-
拼接( splice ):首轮队列循环后若未发现新路径,则启用此最后手段。先随机选两个队列输入在任意中点拼接,再执行拼接操作。
-
同步( sync ):仅在使用
-M
或-S
参数时激活(参见 fuzzing_in_depth.md:3c) Using multiple cores )。不涉及实际的模糊测试,但工具会扫描其他模糊测试工具的输出,并根据需要导入测试用例。首次同步可能耗时数分钟。
剩余字段相对直观:当前阶段的执行计数进度指示、全局执行计数器和当前程序的执行速度基准。执行速度会因测试用例不同而波动,但理想情况下,基准在大部分时间应该超过 500 exec/sec ——若长期低于 100 exec/sec,那么测试工作将非常漫长。
模糊测试工具也会明确警告关于慢速测试目标的问题。如果发生这种情况,请参阅 best_practices.md#improving-speed 以获取提速建议。
深入发现
+--------------------------------------+
| favored paths : 879 (41.96%) |
| new edges on : 423 (20.19%) |
| total crashes : 0 (0 unique) |
| total tmouts : 24 (19 unique) |
+--------------------------------------+
本区域展示了几项主要面向深度技术用户的进阶指标。该部分包括模糊测试工具基于内置最小化算法评估筛选出的”高价值路径”数量(这些路径将获得更多测试时间),真正实现边覆盖率提升的测试用例数量(区别于仅增加分支命中计数的无效用例),关于崩溃和超时的更详细、更具体的计数器。
请注意,超时计数器与挂起计数器存在重要差异:超时计数包含所有超过预设时限的测试用例,挂起计数仅统计那些超时幅度达到”程序挂起”判定标准的案例(即部分超时用例可能被计入前者但未被后者捕获)。
模糊测试策略效能分析
+-----------------------------------------------------+
| bit flips : 57/289k, 18/289k, 18/288k |
| byte flips : 0/36.2k, 4/35.7k, 7/34.6k |
| arithmetics : 53/2.54M, 0/537k, 0/55.2k |
| known ints : 8/322k, 12/1.32M, 10/1.70M |
| dictionary : 9/52k, 1/53k, 1/24k |
|havoc/splice : 1903/20.0M, 0/0 |
|py/custom/rq : unused, 53/2.54M, unused |
| trim/eff : 20.31%/9201, 17.05% |
+-----------------------------------------------------+
本节专为深度技术用户设计,展示各模糊测试策略(参见前文所述)的路径产出率与执行次数的比例关系,用于验证不同测试方法的有效性假设。
本部分中裁剪策略( trim )统计数据与其他策略存在差异。首个数值表示从输入文件中成功移除的字节比例,第二个数值显示达成该修剪效果所需的执行次数,第三个数值反映虽无法移除但被判定为”无效字节”的比例(这些字节会被排除在高成本确定性测试步骤之外)。
请注意,当确定性变异模式处于关闭状态时(默认禁用,因效率较低),前五行将显示“ disabled (default, enable with -D )”,即“已禁用(默认,使用-D
启用)”。
仅激活的策略才会显示对应的计数器数据。
路径拓扑分析
+---------------------+
| levels : 5 |
| pending : 1570 |
| pend fav : 583 |
| own finds : 0 |
| imported : 0 |
| stability : 100.00% |
+---------------------+
本节第一个字段记录通过引导式模糊测试已到达的“路径深度”。简而言之:用户提供的初始测试用例被视为“层级 1 ”;通过常规模糊测试衍生的用例为“层级 2 ”;以这些衍生用例作为后续模糊测试轮次的输入而得出的测试用例为”层级 3 “,以此类推。因此,最大深度是直观反映 afl-fuzz 插桩引导策略的有效性收益的指标。
下一个字段显示尚未经过任何模糊测试的原始输入数量。同时还给出当前队列周期中真正想要处理的“受青睐( favored )”的条目数(非受青睐的条目可能需要等待多个周期才能被测试)。
接下来是本阶段发现的新路径数量,在进行并行模糊测试时从其他模糊测试工具实例导入的路径数量,以及相同输入却偶尔产生不同行为的变异程度,即 trace 一致性得分。
最后一项相当有趣:它衡量的是观测到的执行轨迹的一致性。
-
理想状态( 100% 评分):程序对相同输入始终产生相同行为
-
评分较低但仍显示为紫色:通常不影响模糊测试有效性
-
评分显示为红色:AFL++ 可能无法区分“有意义”的变异与”幻象效应”(即不受输入影响而是程序自身产生的随机效果)
目前,大多数测试目标都能保持 100% 的评分,若看到较低评分,可排查以下原因:
-
被测二进制在用到尚未初始化的内存时,又恰好依赖某些内建随机源(比如栈地址、时间戳等),会导致执行路径不稳定。这对 AFL 本身无害,但可能是潜在的安全隐患。
-
程序如果尝试操作持久化资源,如残留的临时文件或共享内存对象,得分也可能下降。多数情况下无害,但最好确认一下程序是否因此提前退出。磁盘空间不足、SHM 句柄耗尽等全局资源耗尽也会触发类似现象。
-
触发了本身设计为随机行为的功能,这通常是无害的。例如,在模糊测试 sqlite 时,像
select random();
这样的输入将触发可变的执行路径。 -
多线程并发导致的半随机调度。当“稳定性 stability ”指标仍保持在 90% 以上时,这通常安全;低于此值就可能成问题。可尝试以下办法:
-
换用来自 instrumentation 目录的 afl-clang-fast 做插桩,它采用线程局部( thread-local )计数模型,对并发更友好
-
重新编译或运行目标时关闭多线程支持。常见的
./configure
选项有--without-threads
、--disable-pthreads
或--disable-openmp
-
使用 GNU Pth ( https://www.gnu.org/software/pth/ ) 替换 pthreads,GNU Pth 提供确定性调度器,可消除线程调度带来的不确定性
-
-
在 persistent 模式里,轻微下降属于正常现象——并非所有代码在重入时都表现一致;但如果大幅下降,说明
__AFL_LOOP()
里的代码在后续迭代中可能没正确重置状态(例如清理或再初始化不完整),导致大部分测试资源浪费。
CPU 负载
[cpu: 25%]
这个微型状态栏显示的是本地系统的显式 CPU 利用率。其计算原理是统计处于“可运行”状态的进程数量,并将其与系统逻辑 CPU 数量进行对比得出。
若数值以绿色显示,说明您当前使用的 CPU 核心少于系统可用核心数,此时可通过并行化处理来提升性能,具体优化建议请参阅 fuzzing_in_depth.md:3c) Using multiple cores。
若数值呈红色,说明您的 CPU 可能已处于过载状态,继续增加 fuzzer 实例可能无法带来性能提升。
当然,这个基准测试非常简单:它只能反映准备就绪的进程数量,却无法衡量这些进程的资源消耗强度。同时该指标也未区分物理核心、逻辑核心和虚拟化 CPU 的差异,而这三者的性能特征存在显著区别。
如果您想要更精确的测量,可在命令行运行 afl-gotcpu
工具进行检测。
理解输出
请参见 #understanding-the-status-screen 以获取有关如何解读显示的统计信息及监控进程健康状况的信息。如果界面中有任何元素以红色高亮显示,务必查阅该文档。
模糊测试将持续运行,直到您按下 Ctrl-C 终止。至少,应让 fuzzer 完成一个完整的队列循环(queue cycle)且不再发现新的路径覆盖——这一过程可能需要数小时到一周不等,具体取决于目标程序的复杂度。
在输出目录中,fuzzer 会创建并实时更新三个子目录:
-
queue/
:存放所有独特执行路径对应的测试用例,以及用户最初提供的初始文件。该目录构成了合成语料库(synthesized corpus)。在将此语料库用于其他用途前,可使用 afl-cmin 工具对其进行精简,生成一个更小的文件子集,同时保持相同的边覆盖率(edge coverage)
-
crashes/
:存放导致被测程序收到致命信号(如 SIGSEGV、SIGILL、SIGABRT 等)的唯一测试用例,并按触发信号类型分类存储。 -
hangs/
:存放导致被测程序超时的唯一测试用例。默认超时阈值为 1 秒 或-t
参数设定的值(取较大者)。可通过环境变量AFL_HANG_TMOUT
手动调整该值,但通常无需修改。
如果相关的执行路径涉及未在先前记录的故障中出现的任何状态转换,则崩溃和挂起被视为“独特的”。如果一个单一的错误可以通过多种方式触发,早期过程中的计数可能会膨胀,但这应该很快会减小。 如果某次故障(崩溃或挂起)对应的执行路径涉及未在先前记录的故障中出现的任何状态转换,则会被视为“新发现”的 crash 或 hang。若同一漏洞可通过多种路径触发,初期可能会出现重复计数,但随着测试深入,这种情况会迅速减少。
崩溃和挂起用例的文件名会与它们对应的非故障父级 queue 条目保持关联,便于后续调试定位问题根源。
可视化
若系统已安装 gnuplot,您还可以使用 afl-plot 为当前运行的模糊测试任务生成直观的统计图表。示例效果可参考 https://lcamtuf.coredump.cx/afl/plot/。
如需更直观的图形界面监控,可手动编译安装 afl-plot-ui——这是一个基于 GTK 的辅助工具,能够以窗口形式展示 afl-plot 生成的图表。安装步骤如下:
sudo apt install libgtk-3-0 libgtk-3-dev pkg-config
cd utils/plot_ui
make
cd ../../
sudo make install
如果想要了解更多有关使用 StatsD 实现远程监控和指标可视化的内容,参见 rpc_statsd.md。
附录:状态和图表文件
如需无人值守运行,部分关键状态信息会以机器可读的格式记录在输出目录下的 fuzzer_stats 文件中。该文件包含以下核心数据:
- start_time - 表示 afl-fuzz 启动时间的 Unix 时间戳
- last_update - 本文件最后一次更新的 Unix 时间戳
- run_time - 自启动以来的累计运行时间(以秒为单位)
- fuzzer_pid - 模糊测试工具进程的 PID
- cycles_done - 已完成的队列循环次数
- cycles_wo_finds - 未发现新路径的循环次数
- time_wo_finds - 距离上次发现新路径的最长间隔时间(以秒为单位)
- execs_done - 累计执行的 execve() 调用次数
- execs_per_sec - 每秒执行的测试用例数
- corpus_count - 队列中的总测试用例数
- corpus_favored - 被标记为 favored 的队列条目数
- corpus_found - 通过本地模糊测试发现的新条目数
- corpus_imported - 从其他实例导入的条目数
- max_depth - 生成数据集的最大层级深度
- cur_item - 当前正在处理的队列条目编号
- pending_favs - 尚待 fuzz 的 favored 条目数
- pending_total - 尚待 fuzz 的所有条目数
- corpus_variable - 表现出非确定性行为的测试用例数
- stability - 位图字节的一致性百分比(稳定性指标)
- bitmap_cvg - 已覆盖的边覆盖率百分比
- saved_crashes - 记录的唯一崩溃数量
- saved_hangs - 遇到的唯一挂起数量
- last_find - 距离上次发现新路径的时间(以秒为单位)
- last_crash - 距离上次发现崩溃的时间(以秒为单位)
- last_hang - 距离上次发现挂起的时间(以秒为单位)
- execs_since_crash - 上次崩溃后的执行次数
- exec_timeout - 命令行
-t
参数设定超时阈值 - slowest_exec_ms - 单次执行的最长耗时(以毫秒为单位)
- peak_rss_mb - 峰值内存占用(以 MB 为单位)
- edges_found - 已发现的边总数
- var_byte_count - 非确定性边的数量
- afl_banner - 目标程序标识(如二进制文件名)
- afl_version - 使用的 AFL++ 版本号
- target_mode - 运行模式(default/persistent/qemu/unicorn/non-instrumented)
- command_line - 本次 fuzzing 会话的完整命令行
这些字段大多与状态屏幕(UI)显示的指标直接对应。
此外,文件还包含 plot_data
字段,记录了上述多数指标的历史变化数据。若已安装 gnuplot,可使用配套的 afl-plot
工具将其转换为可视化进度报告。
附录:使用 StatsD 自动发送指标
在持续集成(CI)环境或同时运行多个模糊测试工具时,手动加载每个实例或部署脚本读取统计信息会变得十分繁琐。通过设置 AFL_STATSD
环境变量(以及相关配套变量 AFL_STATSD_HOST
、AFL_STATSD_PORT
、AFL_STATSD_TAGS_FLAVOR
),您可以将指标数据自动发送至指定的 StatsD 服务器。根据您的 StatsD 服务配置,您可以实现以下功能:
-
实时监控关键指标
-
触发异常告警或基于指标数据执行自动化操作(如新构建版本的执行速度低于预期值、崩溃率超过设定阈值等)
-
(例如:当 execs_per_sec 或 time_since_last_crash 超过时自动告警)
将能够基于这些指标进行监控、触发警报或执行操作(例如,对新构建的慢执行次数进行警报,当距离上次发现崩溃的时间超过崩溃阈值时告警等)。
所选指标是状态文件和 plot 数据中所有指标的子集,具体包括:cycle_done
、cycles_wo_finds
、execs_done
、execs_per_sec
、corpus_count
、corpus_favored
、corpus_found
、corpus_imported
、max_depth
、cur_item
、pending_favs
、pending_total
、corpus_variable
、saved_crashes
、saved_hangs
、total_crashes
、slowest_exec_ms
、edges_found
、var_byte_count
、havoc_expansion
。它们的定义可在上文附录中找到。
若同时运行多个 fuzzer 实例,强烈建议通过 AFL_STATSD_TAGS_FLAVOR
设置与您 StatsD 服务器匹配的 tag 风格。这将允许您区分不同实例的性能数据、快速定位异常实例、追踪各策略的独立进度等。
译自: https://github.com/AFLplusplus/AFLplusplus/blob/stable/docs/afl-fuzz_approach.md
选题: Souls-R 译者: Hornos3 校对: SheepFifteen
本文由 HCTT 翻译团队 原创翻译,华中科技大学开放原子开源俱乐部荣誉推出。