GitHub Security Lab 发布基于 Taskflow Agent 的 AI 模糊测试流水线
AI-powered fuzzing with the GitHub Security Lab Taskflow Agent
GitHub Security Lab 发布 Fuzzing Taskflow,一个面向 C/C++ 项目的自主模糊测试流水线,基于其 Taskflow Agent 框架构建。
GitHub Security Lab 把模糊测试的 harness 编写、覆盖率追踪与崩溃分诊交给 LLM 智能体,读者可据此判断自动化安全测试的边界。
在这篇博客文章中,我将解释如何使用基于 GitHub Security Lab Taskflow Agent AI 框架的新模糊测试任务流。
2026年9月24日
|
10 分钟
- 分享:
如果你是模糊测试新手,想先学习基础知识,请查看我们的 Fuzzing 101 课程:gh.io/fuzzing101。
持续模糊测试并不是能解决你所有问题的魔法方案。即使是已经加入 OSS-Fuzz 多年的项目,仍然可能隐藏着严重漏洞,而原因几乎总是相同的:需要有人持续关注覆盖率,为无人触及的代码编写新的测试框架,并对另一端产生的崩溃进行分诊。换句话说,模糊测试仍然需要人在环路中。
所以我不断问自己的一个自然问题是:这些人工工作中有多少可以真正交给 LLM 智能体?
这促使我构建了 Fuzzing Taskflow,一个面向 C/C++ 项目的自主模糊测试流水线。你只需将它指向一个 GitHub 仓库,剩下的它都会完成:识别合适的入口点、分析构建系统、编写测试框架、运行 AFL++、读取覆盖率报告、改进测试框架、对每个崩溃进行分诊,并为每个唯一漏洞编写漏洞报告,全程无需人工照看。
Fuzzing Taskflow 构建在 GitHub Security Lab Taskflow Agent 之上,这是我们用于编写 LLM 驱动的安全自动化的框架,因此该流水线被表达为一组任务流,由智能体端到端运行。
在这篇文章中,我将带你了解它的工作原理及其背后的设计决策。让我们开始吧!
如何运行
最简单的运行方式就是前往 https://github.com/GitHubSecurityLab/seclab-taskflows-fuzzing 并启动一个 codespace。
然后,像这样运行脚本:
./scripts/fuzzing/run_fuzzing.sh PROJECT例如:
./scripts/fuzzing/run_fuzzing.sh tukaani-project/xz就是这样。参数只是一个 GitHub owner/repo slug。然后,智能体会自行处理所有前期步骤:
- 安装 AFL 等软件
- 克隆仓库
- 识别代码中最相关的函数
- 为这些函数创建模糊测试目标
如果你只想在投入长期测试活动之前做个快速冒烟测试,就把它指向一个小项目:
./scripts/fuzzing/run_fuzzing.sh DaveGamble/cJSON运行前提醒一句:此任务流会直接在主机上运行 afl-fuzz、clang 以及 由 LLM 选择的任意构建命令,中间没有容器隔离。被提示注入的智能体原则上可以做你的用户能做的任何事。因此请仅在一次性环境(例如 Codespace 或一次性 VM)中运行它,且不要使用提升的权限。

模型选择
一些前沿模型会对其输出施加安全护栏。对于模糊测试任务流,我们默认使用 Claude Sonnet 5,因为它通过了我们所有的内部测试且没有出现问题。你可以通过修改以下文件来选择其他模型:src/seclab_taskflows_fuzzing/configs/model_config.yaml。
一分钟了解架构
在进入有趣的部分之前,先了解各部分如何组合在一起会有所帮助。共有三层:
- 一个shell 驱动程序(run_fuzzing.sh),将流水线各阶段串联起来。
- 一组任务流 YAML,每个阶段一个,本质上就是告诉 LLM 智能体在每个步骤该做什么的提示词。
- 一组MCP 工具,智能体调用它们来实际完成工作:运行 AFL、编译测试框架、存储崩溃、读取覆盖率报告等等。
我最关心的设计准则是清晰的职责分离:LLM agent 负责决策,MCP 工具负责执行。agent 决定要 fuzz 什么、要写什么 harness、接下来要追哪个覆盖率缺口。工具只暴露像 run_afl_for 或 compile_harness 这样的原语。agent 从不直接调用 AFL 或 clang;它用这些构建块组合出流水线。所有状态都存放在一个 SQLite 数据库(fuzz_context.db)中,因此各个阶段之间从不通过内存传递数据,只通过数据库。
一个微小但重要的细节:每个 harness 都会被构建两次。AFL 的边缘插桩非常适合引导 fuzzer,但对人类可读的覆盖率报告毫无用处。因此每个 harness 都会变成两个二进制文件:一个 .afl 二进制(用 afl-clang-lto -fsanitize=address,undefined 构建)和一个 .cov 二进制(用 clang -fprofile-instr-generate -fcoverage-mapping 构建)。.afl 二进制负责 fuzzing;.cov 二进制随后重放 AFL 的队列,以生成真实的源码行和分支覆盖率。
覆盖率反馈循环
这是整个流水线的核心,也是最直接地自动化我在开头描述的手动工作流的部分。
如果你曾经尝试手动提高 fuzzing 覆盖率,你就会知道这是一个迭代过程,看起来像这样:

“检查覆盖率”这一步过去由我完成,手动阅读 LCOV 报告,寻找未覆盖的分支。“提高覆盖率”这一步也由我完成,这次是编写新的 harness 或构造新的输入。Fuzzing Taskflow 把这两步都交给了 agent。
每次迭代,对于每个 harness,agent 会在一个时间预算内运行 AFL,针对 .cov 二进制重放队列以获得真实的覆盖率报告,然后读取未覆盖分支的列表。根据发现的结果,它会从少数几个动作中选择一个:
- 添加一个为到达未覆盖分支而构造的新种子
- 编辑 harness 源码以调用额外的 API
- 用某个 guard 正在比较的魔术常量自动丰富 AFL 字典
- 如果这是一个冷错误路径或不值得追的供应商代码,就直接跳过这个缺口
时间预算每次迭代翻倍:
30s → 60s → 120s → 240s → 480s → 960s(约 32 分钟/目标)
其思路是早期进行廉价、短时间的轮次(此时有大量唾手可得的覆盖率),后期进行更长的轮次(此时 fuzzer 需要更多时间才能突破一个困难的 guard)。
而且就像在我的手动工作流中一样,我需要回答一个问题:我们什么时候停止? 在这里,循环使用平台期检测:一旦连续两次迭代的增益都低于可配置的阈值(默认是 1% 的绝对行覆盖率),循环就判定已进入收益递减,并继续前进。这可以防止 agent 耗费数小时算力去榨取最后百分之零点几。
结构感知 fuzzing
AFL 默认的字节级变异器(位翻转、算术、块拼接)在二进制格式上表现出色,但在结构化的、基于文本的输入上却举步维艰。经典的解决方案是为每种格式手写自定义变异器,这是一项繁琐的工作。这次我希望流水线替我完成这项工作,因此它提供了四种互补机制来生成结构感知的输入。
1. 按格式定制的字典与自定义变异器。对于输入格式可识别的目标(JSON、XML、正则表达式、PNG、长度前缀二进制 TLV),taskflow 会附带预构建的 AFL 字典和 LLVMFuzzerCustomMutator C 文件。JSON 变异器执行 token 拼接和括号平衡复制;XML 变异器了解标签、实体和 billion-laughs token;正则表达式变异器携带真实的 ReDoS 模式。每个变异器将其一半的变异委托回 AFL 的默认字节变异器,因此我们保留引擎的随机性,而不是与之对抗。
2. 源码级字典。对于流水线无法识别的格式,它会通过扫描目标自身的 .c/.h 文件动态生成自定义变异器。它提取字符串字面量和 32 位数字常量(来自 #define、case 和 enum),过滤掉噪声,并将它们用作拼接 token。直觉很简单:解析器检查的最有趣的魔法值通常写在其自身的源码中。
3. 动态生成的 AFL 字典,带覆盖率驱动的增强。同一组源码 token 也会在第 1 次迭代前作为 AFL 经典字典输出(数字常量以两种字节序输出,这样无论主机字节序如何,模糊器都能满足对 4 字节魔数的 memcmp)。然后,在每次覆盖率步骤之后,流水线会查看未覆盖行附近的守卫(strncmp、memcmp、case 0xN、== ‘X’),并追加它发现的任何新 token。字典确实朝着模糊器尚无法到达的代码增长。
4. 语料拼接算子。智能变异器还可以从语料目录加载文件,并将它们的随机子区域拼接到输入中,这是一种重组风格的算子,AFL 的常规 havoc 做得不好。
演化语料库
悄悄扼杀模糊测试效率的事情之一是丢弃进展。如果每次运行都从原始种子开始,你就会一遍又一遍地重新付出重新发现相同路径的代价。
为了避免这种情况,每个测试框架都会获得一个稳定的语料目录,它能在多次迭代和整个活动周期中持续存在:
<workspace>/corpus/harness_<id>/在每次迭代结束时,AFL 的队列会被合并到这个目录中,并通过 afl-cmin 运行以保持其大小有界。效果是昨天的有趣输入会延续到今天的运行中,而上周活动中发现的输入会延续到这一次。如果你停止并重新启动一个活动,你不会丢失任何东西。
分类与漏洞报告
发现崩溃只是工作的一半。任何做过根因分析的人都知道,分类通常是整个过程中最乏味的部分。这是智能体大放异彩的另一个地方。
模糊测试循环结束后,会自动运行三个阶段。首先,每个崩溃都用 afl-tmin 最小化,在 ASan 下重放以捕获堆栈跟踪,并通过栈顶哈希去重(顶部规范化帧,剥离模板、内联命名空间和 LTO 后缀,以便语义相同的崩溃合并在一起)。其次,之前已知的崩溃会在当前二进制文件上重放,以查看上游修复是否已解决它们。第三,智能体读取测试框架源码和崩溃函数,从公共 API 回溯调用链,并编写每个崩溃的 markdown 报告。
每份报告都会给出以下判定之一:
- 漏洞
- library_hardening
- harness_bug
- OOM
- timeout
- assertion_failure
- duplicate
真正的漏洞(可通过公开 API 触达并利用)与单纯的 harness_bug(bug 出在我们自己的 harness 中,而非库本身)之间的区分,正是那种过去需要我坐下来手工追踪代码才能做出的判断。每份报告都包含带 file:line 引用的根因分析、可达性论证、可利用性评估、以 unified diff 形式给出的建议修复以及回归测试草案。
需要澄清一下:建议的补丁被标记为“需要审查”是有原因的。智能体的分析受限于模型对目标代码的理解,它确实会出错。请把这些结论视为一份为人类准备得非常充分的起点,而不是最终结果。
实时仪表盘
运行自主 campaign 却看不到它在做什么会让人不安,因此该流水线会把所有内容发布到一个实时 HTML 仪表盘上。一旦你启动 campaign,它就会在后台自动启动,监听端口 8765。在 Codespace 中该端口会自动转发,因此你可以在任意浏览器中打开它,并在仪表盘上实时查看 campaign 进度。

该页面展示的内容包括:
- 每个 harness 的“运行中”脉冲
- 带内联迷你走势图的覆盖率趋势表
- 崩溃热力图
- 迭代时间线
结论
我启动这个项目的动机,源于任何安全研究员都熟知的局限:模糊测试有效,但若没有人的关注就无法扩展,而人的关注正是瓶颈。Fuzzing Taskflow 是我尝试把这一瓶颈向后推的成果——将重复性的部分(编写 harness、阅读覆盖率、追踪缺口、分类崩溃)交给 LLM 智能体,同时在智能体的判断与真正干活的工具之间保持清晰的分离。
如果你是 C/C++ 项目的维护者,那么请试一试。如果你的项目以前从未做过模糊测试,那么这个工具将帮助你快速上手。或者,如果你的项目曾经做过模糊测试,那么这个工具可能通过提高你的模糊测试覆盖率来帮助发现新的 bug。
源代码是开源的,所以如果你遇到任何 bug,请创建 issue。也欢迎贡献!
作者:
相关文章
秘密保护必须与软件同步扩展
开发者并非变得越来越粗心;他们是被甩在了后面。那些让开发者能够创造更多软件的工具,也应当承担起更多保护软件的工作。
我们也做新闻通讯
在我们的双周开发者专属新闻通讯中,发现技巧、技术指南和最佳实践。
来源:GitHub Blog · AI & ML · github.blog