跳到正文
原文
GitHub Blog · AI & ML· Antonio Morales·· 20 天前精选AI 评分63

GitHub Security Lab 发布 AI 驱动的 Fuzzing Taskflow 智能体

AI-powered fuzzing with the GitHub Security Lab Taskflow Agent

AI 导读

GitHub Security Lab 开源 Fuzzing Taskflow,这是一个基于 Taskflow Agent 框架的自主 fuzzing 流水线,面向 C/C++ 项目,只需指向一个 GitHub 仓库即可自动识别入口点、分析构建系统、编写 harness、运行 AFL++、读取覆盖率报告并改进 harness,还对每个崩溃做去重和漏洞报告。

推荐理由

作者公开了自主模糊测试流水线的分层架构与覆盖反馈闭环设计,适合关注 LLM Agent 落地工程细节的读者。

正文 · AI 翻译

如果你刚接触模糊测试,想先了解基础知识,请查看我们的 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 标识。接下来,智能体会自行处理所有前期步骤:

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

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

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

运行前需要提醒一句:这个任务流会直接在宿主机上运行 afl-fuzz、clang 以及 由 LLM 选择的任意构建命令,中间没有容器隔离。被提示注入的智能体理论上可以做你的用户能做的任何事。因此请仅在一次性环境(例如 Codespace 或一次性 VM)中运行它,并且不要使用提升的权限。

Screenshot of Seclab Taskflows Fuzzing.

模型选择

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

一分钟了解架构

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

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

我最关心的设计原则是职责的清晰分离:LLM agent 负责决策,MCP tools 负责执行。Agent 决定 fuzz 什么、编写什么 harness,以及接下来追逐哪个覆盖率缺口。Tools 只暴露像 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
  • 用某个 guard 正在比较的 magic 常量自动丰富 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 运行以保持其大小有界。效果是,昨天的有趣输入会延续到今天的运行中,而你在上周活动中发现的输入也会延续到这一次。如果你停止并重新启动一个活动,你不会有任何损失。

分类与漏洞报告

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

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

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

  • 漏洞
  • 库加固
  • 测试桩缺陷
  • 内存溢出
  • 超时
  • 断言失败
  • 重复

真正的漏洞(可通过公开 API 触达且可利用)与单纯的测试桩缺陷(缺陷出在我们自己的测试桩里,而不在库中)之间的区分,正是那种过去需要我坐下来一行行手动追踪代码才能做出的判断。每份报告都包含带有 文件:行号 引用的根因分析、可达性论证、可利用性评估、以统一 diff 形式给出的修复建议,以及回归测试草案。

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

实时仪表盘

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

Screenshot of the fuzzing dashboard.

页面展示的内容包括:

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

结论

我启动这个项目的动机,源于任何安全研究员都熟知的那些局限:模糊测试有效,但没有人盯就难以扩展,而人的注意力正是瓶颈。Fuzzing Taskflow 是我尝试把这个瓶颈往后推的一次努力——把重复性的工作(编写测试桩、阅读覆盖率、追查缺口、分类崩溃)交给 LLM 智能体,同时在智能体的判断与真正干活的工具之间保持清晰的分离。

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

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

文章 AI-powered fuzzing with the GitHub Security Lab Taskflow Agent 首次出现在 The GitHub Blog 上。

来源:GitHub Blog · AI & ML · github.blog