GitHub Security Lab 发布 LLM 驱动的 Fuzzing Taskflow,自动完成 C/C++ 项目模糊测试全流程

GitHub Blog·2026-09-25 02:26·51分钟前·Antonio Morales
AI 导读

GitHub Security Lab 的 Antonio Morales 开源了基于 Taskflow Agent 的 Fuzzing Taskflow,指向 GitHub 仓库即可自动识别入口点、编写 harness、运行 AFL++、读取覆盖报告并分诊崩溃。

GitHub Blog
精选
64AI 编辑部评分,满分 100

GitHub Security Lab 发布 LLM 驱动的 Fuzzing Taskflow,自动完成 C/C++ 项目模糊测试全流程

2026-09-25 02:26· 51分钟前· Antonio Morales
AI 导读

GitHub Security Lab 的 Antonio Morales 开源了基于 Taskflow Agent 的 Fuzzing Taskflow,指向 GitHub 仓库即可自动识别入口点、编写 harness、运行 AFL++、读取覆盖报告并分诊崩溃。

推荐理由

作者开源了完整的自主模糊测试流水线,并讲清覆盖反馈、结构感知和崩溃分诊的设计取舍,C/C++ 维护者可直接复用。

正文 · 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 slug。接下来,智能体会自行处理所有前期步骤:

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

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

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

运行之前有一点提醒:这个 taskflow 会直接在宿主机上运行 afl-fuzz、clang 以及 由 LLM 选定的任意构建命令,中间没有容器隔离。一个被提示词注入的智能体原则上可以做你的用户能做的任何事情。所以请只在一次性环境(例如 Codespace 或一次性虚拟机)中运行它,且不要使用提权权限。

Screenshot of Seclab Taskflows Fuzzing.

模型选择

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

一分钟了解架构

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

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

我最看重的设计原则是职责的清晰分离:LLM 智能体负责决策,MCP 工具负责执行。智能体决定要 fuzz 什么、要编写什么 harness、接下来要追查哪个覆盖率缺口。工具只暴露诸如 run_afl_for 或 compile_harness 这样的原语。智能体从不直接调用 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 报告,寻找未覆盖的分支。“提升覆盖率”这一步同样由我完成,这次是编写新的测试框架或构造新的输入。而 Fuzzing Taskflow 把这两步都交给了智能体。

每一轮迭代中,对于每个测试框架,智能体会在给定的时间预算内运行 AFL,将队列针对 .cov 二进制文件重放以获得真实的覆盖率报告,然后读取未覆盖分支的列表。根据所发现的情况,它会从若干操作中选择一项:

  • 添加一个为触达未覆盖分支而专门构造的新种子
  • 编辑测试框架源码以调用额外的 API
  • 用某个守卫正在比较的魔术常量自动丰富 AFL 字典
  • 如果只是冷门的错误路径或不值得追查的厂商代码,就直接跳过这个缺口

时间预算每一轮迭代翻倍:

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

其思路是早期进行廉价、短时间的轮次(此时有大量唾手可得的覆盖率可以获取),后期则进行更长的轮次(此时模糊测试器需要更多时间才能突破某个难以攻克的守卫)。

而正如我手动工作流中的做法,我需要回答这个问题:我们什么时候停下来?在这里,循环使用了平台期检测:一旦连续两次迭代各自的增益都低于一个可配置的阈值(默认是 1% 的绝对行覆盖率),循环就判定已经进入收益递减,随即继续推进。这能避免智能体耗费数小时算力去榨取最后那百分之零点几的覆盖率。

结构感知模糊测试

AFL 默认的字节级变异器(位翻转、算术运算、块拼接)在二进制格式上表现出色,但面对结构化的、基于文本的输入时就力不从心了。经典的解决方案是为每种格式手写自定义变异器,但这是一项繁琐的工作。这一次,我希望流水线替我完成这项工作,因此它内置了四种互补机制来生成结构感知的输入。

1. 按格式定制的字典与自定义变异器。对于输入格式可被识别的目标(JSON、XML、regex、PNG、带长度前缀的二进制 TLV),该 taskflow 内置了预构建的 AFL 字典和 LLVMFuzzerCustomMutator C 文件。JSON 变异器执行 token 拼接和括号配对复制;XML 变异器了解标签、实体以及 billion-laughs token;regex 变异器则携带真实的 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 报告。

每份报告会给出以下判定之一:

  • 漏洞
  • 库加固
  • 测试框架缺陷
  • OOM
  • 超时
  • 断言失败
  • 重复

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

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

实时仪表盘

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

Screenshot of the fuzzing dashboard.

该页面展示的内容包括:

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

结论

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

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

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

来源:GitHub Blog· github.blog