跳到正文
GitHub Blog · AI & ML· Antonio Morales·· 2026-09-25 精选AI 评分62

GitHub Security Lab 发布 Fuzzing Taskflow 智能体流水线

AI-powered fuzzing with the GitHub Security Lab Taskflow Agent

AI 导读

GitHub Security Lab 发布 Fuzzing Taskflow,一个面向 C/C++ 项目的自主模糊测试流水线,只需指向一个 GitHub 仓库,它就会识别入口点、分析构建系统、编写 harness、运行 AFL++、读取覆盖率报告并改进 harness,最后对每个崩溃做去重与漏洞报告。

摘要与补充说明

原文完整披露了自主模糊测试流水线的架构与覆盖反馈循环,读者可据此判断 LLM 智能体在安全自动化中的分工边界。

正文· AI 翻译

如果你是模糊测试新手,想先学习基础知识,请查看我们的 Fuzzing 101 课程:gh.io/fuzzing101。

持续模糊测试并不是能解决所有问题的魔法方案。即使是已经加入 OSS-Fuzz 多年的项目,仍然可能隐藏着严重缺陷,而原因几乎总是相同的:需要有人持续关注覆盖率、为无人触及的代码编写新的 harness,并对最终产生的崩溃进行分诊。换句话说,模糊测试仍然需要人在环路中。

所以我不断问自己的一个自然问题是:这些人工工作中有多少可以真正交给 LLM agent?

这就是我构建 Fuzzing Taskflow 的起因——一个面向 C/C++ 项目的自主模糊测试流水线。你只需将它指向一个 GitHub 仓库,剩下的它都会完成:识别合适的入口点、分析构建系统、编写 harness、运行 AFL++、读取覆盖率报告、改进 harness、对每个崩溃进行分诊,并为每个唯一缺陷编写漏洞报告,全程无需人工照看。

Fuzzing Taskflow 构建在 GitHub Security Lab Taskflow Agent 之上,后者是我们用于编写 LLM 驱动的安全自动化的框架,因此该流水线被表达为一组由 agent 端到端运行的任务流。

在本文中,我将带你了解它的工作原理及其背后的设计决策。我们开始吧!

如何运行

最简单的运行方式就是前往 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。随后,agent 会自行处理所有前期步骤:

  • 安装 AFL 等软件
  • 克隆仓库
  • 识别代码中最相关的函数
  • 为这些函数创建模糊测试目标

如果你只想在投入长时间测试活动之前做个快速冒烟测试,可以指向一个小项目:

./scripts/fuzzing/run_fuzzing.sh DaveGamble/cJSON

运行前提醒一句:此任务流会直接在宿主机上运行 afl-fuzz、clang 以及 由 LLM 选择的任意构建命令,中间没有容器隔离。被提示注入的 agent 原则上可以执行你的用户所能执行的任何操作。因此请仅在一次性环境(例如 Codespace 或一次性 VM)中运行,且不要赋予提升的权限。

Screenshot of Seclab Taskflows Fuzzing.

模型选择

一些前沿模型会对其输出施加安全护栏。对于模糊测试任务流,我们默认使用 Claude Sonnet 5,因为它顺利通过了我们所有的内部测试。你可以通过修改以下文件来选择其他模型:src/seclab_taskflows_fuzzing/configs/model_config.yaml。

一分钟了解架构

在深入有趣的部分之前,先了解各部分如何组合会很有帮助。共有三层:

  • 一个shell 驱动脚本(run_fuzzing.sh),将流水线各阶段串联起来。
  • 一组任务流 YAML,每个阶段一个,本质上就是告诉 LLM agent 在每个步骤该做什么的提示词。
  • 一组MCP 工具,agent 调用它们来实际完成工作:运行 AFL、编译 harness、存储崩溃、读取覆盖率报告等等。

我最关心的设计原则是职责的清晰分离: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 覆盖率,你就会知道这是一个迭代过程,大致如下:

Three bubbles that say: Run the fuzzers, Check the coverage, and Improve the fuzzers. They are connected by three arrows in a circular flow.

“检查覆盖率”这一步过去由我完成,手动阅读 LCOV 报告,寻找未覆盖的分支。“提升覆盖率”这一步同样由我完成,这一次是编写新的 harness 或构造新的输入。Fuzzing Taskflow 把这两步都交给了 agent。

每一轮迭代中,对于每个 harness,agent 会在一个时间预算内运行 AFL,将队列重放到 .cov 二进制上以获得真实的覆盖率报告,然后读取未覆盖分支的列表。根据发现的结果,它会从若干动作中选择一个:

  • 添加一个专门构造的种子,以到达某个未覆盖的分支
  • 编辑 harness 源码,以调用额外的 API
  • 用某个守卫正在比较的魔法常量自动丰富 AFL 字典
  • 如果缺口是冷错误路径或供应商代码、不值得追查,就直接跳过

时间预算每轮翻倍:

30s → 60s → 120s → 240s → 480s → 960s(约 32 分钟/目标)

其思路是早期(当有大量容易获取的覆盖率时)花便宜、短的时间轮次,后期(当 fuzzer 需要更多时间突破某个困难守卫时)花更长的时间轮次。

而且就像在我的手动工作流中一样,我需要回答一个问题:我们什么时候停止? 在这里,循环使用平台期检测:一旦连续两轮迭代各自的增益都低于可配置的阈值(默认是 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 做得并不好。

不断演进的语料库

悄悄扼杀模糊测试效率的事情之一就是丢弃进展。如果每次运行都从原始种子开始,你就会一遍又一遍地重新付出重新发现相同路径的代价。

为避免这种情况,每个 harness 都会获得一个稳定的语料目录,它能在迭代之间以及整个 campaign 之间存活:

<workspace>/corpus/harness_<id>/

在每次迭代结束时,AFL 的队列会被合并到这个目录中,并通过 afl-cmin 运行以保持其大小有界。效果是,昨天有趣的输入会延续到今天的运行中,你上周 campaign 中找到的输入会延续到这一次。如果你停止并重新启动一个 campaign,你不会丢失任何东西。

分类与漏洞报告

发现崩溃只是工作的一半。任何做过根因分析的人都知道,分类通常是整个过程中最乏味的部分。这是 agent 大放异彩的另一个地方。

模糊测试循环结束后,会自动运行三个阶段。首先,每个崩溃都用 afl-tmin 最小化,在 ASan 下重放以捕获堆栈跟踪,并通过栈顶哈希去重(顶部规范化帧,剥离模板、内联命名空间和 LTO 后缀,以便语义相同的崩溃合并在一起)。其次,先前已知的崩溃会针对当前二进制重放,以查看上游修复是否已解决它们。第三,agent 读取 harness 源码和崩溃函数,从公共 API 回溯调用链,并编写每个崩溃的 markdown 报告。

每份报告都会给出以下结论之一:

  • 漏洞
  • library_hardening
  • harness_bug
  • OOM
  • timeout
  • assertion_failure
  • duplicate

真正的漏洞(可通过公共 API 触达并利用)与单纯的 harness_bug(缺陷出在我们自己的 harness 中,而非库本身)之间的区分,恰恰是那种过去需要我坐下来手工追踪代码才能做出的判断。每份报告都包含带 file:line 引用的根因分析、可达性论证、可利用性评估、以 unified diff 形式给出的建议修复,以及回归测试草案。

澄清一下:建议的补丁被标记为“需要审查”是有原因的。该 agent 的分析受限于模型对目标代码的理解,而且它确实会出错。请把这些结论视为一份为人类准备得非常充分的起点,而非最终结果。

实时仪表盘

运行自主活动却看不到它在做什么,会让人不安,因此流水线会把所有内容发布到一个实时 HTML 仪表盘上。你一启动活动,它就会在后台自动启动,监听端口 8765。在 Codespace 中该端口会自动转发,因此你可以在任意浏览器中打开它,在仪表盘上实时查看活动进展。

Screenshot of the fuzzing dashboard.

页面展示的内容包括:

  • 每个 harness 的“运行中”脉冲
  • 带内联迷你走势图的覆盖率趋势表
  • 崩溃热力图
  • 迭代时间线

结论

我启动这个项目,是出于任何安全研究员都熟知的局限:模糊测试有效,但若没有人的关注就无法扩展,而人的关注正是瓶颈。Fuzzing Taskflow 是我尝试把这一瓶颈向后推的成果——把重复性工作(编写 harness、阅读覆盖率、追查缺口、分类崩溃)交给 LLM agent,同时在 agent 的判断与执行实际工作的工具之间保持清晰的分离。

如果你是 C/C++ 项目的维护者,那么请试一试。如果你的项目此前从未做过模糊测试,那么这个工具能帮你快速上手。或者,如果你的项目曾经做过模糊测试,那么这个工具或许能通过提高模糊测试覆盖率来帮你发现新的缺陷。

源代码是开源的,所以如果你遇到任何缺陷,请创建 issue。也欢迎贡献!

文章 AI-powered fuzzing with the GitHub Security Lab Taskflow Agent 首次发表于 The GitHub Blog。

相关阅读

专题与标签

相关资讯专题

标签