作者:Antonio Morales
排版:Alan Wang
在这篇博客文章中,我将介绍如何使用基于 GitHub Security Lab Taskflow Agent AI 框架构建的新型模糊测试工作流。

如果你刚开始接触模糊测试,并希望先学习基础知识,可以参加我们的 Fuzzing 101 课程:gh.io/fuzzing101。
持续模糊测试并不是能够解决所有问题的灵丹妙药。即使是已经加入 OSS-Fuzz 多年的项目,仍然可能隐藏着严重的漏洞,而原因几乎总是相同的:需要有人持续关注代码覆盖率,为那些尚未被触及的代码编写新的测试驱动程序,并对最终产生的崩溃进行分类分析。换句话说,模糊测试仍然需要人工参与。
因此,我一直在问自己一个自然而然的问题:我们究竟能将多少这类人工工作交给大语言模型智能体来完成?
这正是我开发 Fuzzing Taskflow 的原因。这是一条面向 C/C++ 项目的自主模糊测试流水线。你只需要指定一个 GitHub 仓库,其余工作都可以交给它:识别合适的入口点、分析构建系统、编写测试驱动程序、运行 AFL++、读取覆盖率报告、改进测试驱动程序、对每次崩溃进行分类分析,并为每个独立漏洞编写漏洞报告,整个过程无需人工持续监督。
Fuzzing Taskflow 构建于我们的 GitHub Security Lab Taskflow Agent 之上。这是一个用于编写由大语言模型驱动的安全自动化程序的框架,因此,整条流水线被组织为一组任务流,由智能体端到端地执行。
在这篇文章中,我将介绍它的工作原理以及背后的设计决策。让我们开始吧!
如何运行
运行它最简单的方法,就是访问 https://github.com/GitHubSecurityLab/seclab-taskflows-fuzzing 并启动一个 Codespace。
然后,运行以下脚本:
Plain
./scripts/fuzzing/run_fuzzing.sh PROJECT
例如:
Plain
./scripts/fuzzing/run_fuzzing.sh tukaani-project/xz
就是这么简单。参数只需要填写 GitHub 仓库的 owner/repo 标识。随后,智能体会自行完成所有准备工作:
-
安装 AFL 等软件
-
克隆仓库
-
识别代码中最相关的函数
-
为这些函数创建模糊测试目标
如果你只想在投入一场耗时较长的测试活动之前快速进行一次冒烟测试,可以选择一个较小的项目:
Plain
./scripts/fuzzing/run_fuzzing.sh DaveGamble/cJSON
运行之前需要提醒一点:这个任务流会直接在宿主机上运行 afl-fuzz、clang 以及由大语言模型自主选择的任意构建命令 ,中间没有容器提供隔离。理论上,一个遭到提示词注入的智能体可以执行当前用户有权限执行的任何操作。因此,请仅在可随时销毁的环境中运行它,例如 Codespace 或一次性虚拟机,并且不要赋予它提升后的权限。

模型选择
一些前沿模型会对输出内容施加安全防护限制。对于这条模糊测试任务流,我们默认使用 Claude Sonnet 5,因为它通过了我们的所有内部测试,没有出现问题。如果你想选择其他模型,可以修改以下文件:
src/seclab_taskflows_fuzzing/configs/model_config.yaml
一分钟了解整体架构
在深入了解有趣的部分之前,先了解各个组件如何协同工作会很有帮助。整个系统分为三个层次:
-
一个Shell 驱动脚本(
run_fuzzing.sh),负责将流水线的各个阶段串联起来。 -
一组 YAML 任务流文件,每个阶段对应一个文件,本质上是告诉大语言模型智能体在每一步应该做什么的提示词。
-
一组 MCP 工具,由智能体调用来执行实际操作,例如运行 AFL、编译测试驱动程序、保存崩溃信息、读取覆盖率报告等。
我最重视的一项设计原则是明确划分职责:大语言模型智能体负责决策,MCP 工具负责执行 。智能体决定要测试哪些代码、编写什么样的测试驱动程序,以及下一步应该追踪哪个覆盖率缺口。工具只负责提供诸如 run_afl_for 或 compile_harness 这样的基础操作接口。智能体不会直接调用 AFL 或 clang,而是通过组合这些基础模块来构建整个流水线。所有状态都存储在 SQLite 数据库(fuzz_context.db)中,因此,各个阶段不会通过内存直接传递数据,而是通过数据库共享数据。
还有一个虽小却很重要的细节:每个测试驱动程序都会被构建两次。AFL 的边覆盖率插桩非常适合引导模糊测试器,但无法用于生成便于人类阅读的覆盖率报告。因此,每个测试驱动程序都会生成两种二进制文件:一种是使用 afl-clang-lto -fsanitize=address,undefined 构建的 .afl 文件,另一种是使用 clang -fprofile-instr-generate -fcoverage-mapping 构建的 .cov 文件。.afl 文件负责执行模糊测试;之后,.cov 文件会重新执行 AFL 的输入队列,生成真实的源代码行覆盖率和分支覆盖率报告。
覆盖率反馈循环
这是整条流水线的核心,也是最直接实现前面所说的人工工作自动化的部分。
如果你曾经手动提高模糊测试的代码覆盖率,就会知道这是一个反复迭代的过程,大致如下:

过去,"检查覆盖率"这一步需要由我手动完成:阅读 LCOV 报告,寻找尚未覆盖的分支。"提高覆盖率"这一步也需要我亲自完成,这次的工作是编写新的测试驱动程序或构造新的输入。现在,Fuzzing Taskflow 将这两个步骤都交给了智能体。
在每次迭代中,对于每个测试驱动程序,智能体都会先在限定的时间内运行 AFL,再使用 .cov 二进制文件重新执行输入队列,以生成真实的覆盖率报告,然后读取未覆盖分支的列表。根据分析结果,它会从以下几种操作中选择一种:
-
添加一个专门用于触达未覆盖分支的新种子输入。
-
修改测试驱动程序的源代码,调用额外的 API。
-
自动扩充 AFL 字典,将代码中的保护条件所比较的特殊常量加入其中。
-
如果覆盖率缺口属于不常见的错误处理路径,或者属于不值得继续追踪的第三方代码,则直接跳过。
每次迭代的时间预算都会翻倍增长:
30 秒 → 60 秒 → 120 秒 → 240 秒 → 480 秒 → 960 秒(每个测试目标约 32 分钟)
这样设计的目的是:在早期,先进行成本较低、耗时较短的测试,因为此时通常有很多容易获得的覆盖率增量;到了后期,当模糊测试器需要更多时间才能突破复杂的检查条件时,再延长测试时间。
与我手动执行测试时一样,还需要回答一个问题:什么时候应该停止? 这里采用的是平台期检测机制:如果连续两次迭代的覆盖率增量都低于可配置的阈值(默认值为绝对行覆盖率的 1 个百分点),循环就会判断收益正在递减,并转向下一个目标。这样可以避免智能体为了再提高最后零点几个百分点的覆盖率,而持续消耗数小时的计算资源。
感知结构的模糊测试
AFL 默认的字节级变异器,例如位翻转、算术运算和数据块拼接,在处理二进制格式时表现出色,但在处理具有结构的文本输入时往往力不从心。传统解决方案是针对每种格式手动编写自定义变异器,但这是一项繁琐的工作。这次,我希望让流水线替我完成这些工作,因此系统提供了四种互为补充的机制,用于生成能够感知输入结构的测试数据。
-
针对不同格式的字典和自定义变异器 。对于能够识别输入格式的测试目标,例如 JSON、XML、正则表达式、PNG,以及带长度前缀的二进制 TLV 格式,任务流会提供预先构建的 AFL 字典和
LLVMFuzzerCustomMutatorC 源文件。JSON 变异器会执行 Token 拼接和成对括号复制;XML 变异器能够识别标签、实体以及用于触发 Billion Laughs 攻击的特殊内容;正则表达式变异器则包含真实的正则表达式拒绝服务(ReDoS)模式。每个变异器都会将一半的变异操作交还给 AFL 默认的字节级变异器,从而保留引擎本身的随机化能力,而不是与其对抗。 -
基于源代码的字典 。对于流水线无法识别的格式,系统会扫描目标项目自身的
.c和.h文件,动态生成自定义变异器。它会提取字符串字面量和 32 位数值常量,包括来自#define、case和enum的常量,过滤掉无关内容,再将这些内容用作拼接 Token。其基本思路很简单:解析器需要检查的特殊值,通常都会在它自己的源代码中有所体现。 -
动态生成 AFL 字典,并根据覆盖率进行扩充 。在第一次迭代之前,系统也会将同一组源代码 Token 生成为标准 AFL 字典。其中,数值常量会同时以大端和小端字节序写入,这样无论宿主机采用哪种字节序,模糊测试器都能尝试满足针对 4 字节特殊标识值的
memcmp比较。此后,每次完成覆盖率分析,流水线都会检查未覆盖代码行附近的保护条件,例如strncmp、memcmp、case 0xN和== 'X',并将发现的新 Token 追加到字典中。这个字典会不断扩充,逐渐向模糊测试器尚未触达的代码靠近。 -
语料库拼接操作符。智能变异器还可以从语料库目录中加载文件,并将其中随机选取的片段拼接到当前输入中。这是一种基于重新组合的变异操作,而 AFL 自带的 havoc 变异策略并不擅长完成这类工作。
不断演进的语料库
有一个问题会在不知不觉中降低模糊测试的效率,那就是丢弃已有的进展。如果每次运行都从最初的种子输入开始,就必须反复付出代价,重新发现那些已经探索过的代码路径。
为了避免这种情况,每个测试驱动程序都会拥有一个稳定的语料库目录,在不同迭代之间以及整个测试活动之间持续保留:
Plain
<workspace>/corpus/harness_<id>/
每次迭代结束时,AFL 的输入队列都会合并到这个目录中,然后通过 afl-cmin 进行精简,以控制语料库的规模。这样一来,昨天发现的有价值输入就能带入今天的测试,上周测试活动中发现的输入也能继续用于本周的测试。即使你停止并重新启动测试活动,也不会丢失已有成果。
分类分析与漏洞报告
发现崩溃只是工作的一半。任何做过根因分析的人都知道,崩溃分类分析往往是整个过程中最繁琐的环节之一。这也是智能体能够发挥重要作用的另一个地方。
模糊测试循环结束后,系统会自动执行三个阶段。首先,每个崩溃都会使用 afl-tmin 进行最小化处理,然后在 ASan 下重新执行,以捕获堆栈跟踪。之后,系统会根据栈顶哈希值进行去重。该哈希值由经过规范化处理的顶部堆栈帧生成,过程中会剔除模板、内联命名空间和 LTO 后缀,从而将语义上相同的崩溃归并到一起。其次,系统会使用当前版本的二进制文件重新执行此前已知的崩溃,检查上游代码的修复是否已经解决了这些问题。第三,智能体会读取测试驱动程序的源代码和发生崩溃的函数,从公共 API 出发沿调用链向上追踪,并为每次崩溃编写一份 Markdown 报告。
每份报告都会给出以下判定之一:
-
vulnerability:漏洞 -
library_hardening:库的健壮性改进 -
harness_bug:测试驱动程序缺陷 -
OOM:内存耗尽 -
timeout:超时 -
assertion_failure:断言失败 -
duplicate:重复问题
区分真正的漏洞(可以通过公共 API 触达并利用)与普通的测试驱动程序缺陷(问题出在我们自己的测试驱动程序,而不是库本身),正是过去需要我坐下来手动追踪代码才能作出的判断。每份报告都包含根因分析及对应的 文件:行号 引用、可触达性论证、可利用性评估、以统一差异格式提供的建议修复方案,以及回归测试的初步设计。
这里还需要特别说明:建议补丁被标记为"需要审核"是有原因的。智能体的分析能力受限于模型对目标代码的理解,它确实会犯错。因此,应当将这些判定视为为人工审核精心准备的起点,而不是最终结论。
实时仪表板
运行自主测试活动时,如果无法了解系统正在做什么,会让人感到不安。因此,这条流水线会将所有信息发布到一个实时 HTML 仪表板上。启动测试活动后,仪表板会自动在后台运行,监听端口 8765。在 Codespace 中,该端口会自动转发,因此你可以在任意浏览器中打开仪表板,实时查看测试活动的进展。

页面展示的内容包括:
-
每个测试驱动程序的运行状态指示
-
带有内嵌迷你趋势图的覆盖率趋势表
-
崩溃热力图
-
迭代时间线
总结
我启动这个项目,是因为我看到了每一位安全研究人员都熟悉的局限:模糊测试确实有效,但如果没有人工持续关注,就很难扩大规模,而人工投入恰恰是整个流程的瓶颈。Fuzzing Taskflow 是我尝试突破这一瓶颈的成果。通过将编写测试驱动程序、阅读覆盖率报告、追踪覆盖率缺口和分析崩溃等重复性工作交给大语言模型智能体,同时明确区分智能体的判断职责与实际执行工作的工具,我希望让整个流程具备更强的自动化能力。
如果你是某个 C/C++ 项目的维护者,欢迎试用这个工具。如果你的项目此前从未进行过模糊测试,这个工具可以帮助你快速入门。如果你的项目已经进行过模糊测试,它也可能通过提高代码覆盖率,帮助你发现新的漏洞。
项目源代码已开源。如果你遇到任何问题,欢迎提交 Issue。我们也欢迎大家参与贡献!