借助GitHub Security Lab Taskflow Agent实现AI驱动的模糊测试

GitHub Blog 2026-09-25T06:35:46.046729

如果你是模糊测试新手,想先打打基础,可以看看我们的 Fuzzing 101 课程:gh.io/fuzzing101。

持续模糊测试并不是什么包治百病的银弹。有些项目加入 OSS-Fuzz 已经好几年了,照样藏着严重漏洞。原因几乎每次都一样:得有人盯着覆盖率,为无人触及的代码写新的 harness,再分类处理跑出来的崩溃样本。换句话说,模糊测试仍然离不开人盯着。

所以我一直在问自己一个问题:这些人工活儿里,到底有多少能交给大语言模型(LLM)智能体去做?这就是我开发 Fuzzing Taskflow 的起点——一套面向 C/C++ 项目的自主模糊测试流水线。你只需把它指向一个 GitHub 仓库,剩下的它全包:找出合适的入口点、分析构建系统、编写 harness、运行 AFL++、读取覆盖率报告、改进 harness、分类每个崩溃,并为每个独立的 bug 写一份漏洞报告,全程不需要人照看。

Fuzzing Taskflow 构建在 GitHub Security Lab Taskflow Agent 之上——这是我们用来编写 LLM 驱动安全自动化的框架。整条流水线由一组 taskflow 表达,智能体从头到尾依次执行。这篇文章会带你了解它的工作原理,以及背后的设计取舍。我们开始吧!

如何运行

最简单的方式是打开 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 标识。

然后,Agent 会自己搞定所有前期准备工作:

如果你只想在投入长时间测试之前快速验证一下,可以拿个小项目试试:

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

运行之前有个警告:这个任务流会直接在宿主机上运行 afl-fuzz、clang,以及由大语言模型(LLM)选出的任意构建命令,中间没有容器隔离。一旦 Agent 被提示注入(prompt injection)攻击,理论上它就能做你的用户账号能做的任何事。所以请只在一次性环境里运行(比如 Codespace 或临时虚拟机),而且不要给管理员权限。

模型选择

有些前沿模型会对输出施加安全限制。在模糊测试任务流中,我们默认使用 Claude Sonnet 5,因为它通过了我们所有的内部测试,没有出过问题。你也可以换用别的模型,修改下面这个文件即可:

src/seclab_taskflows_fuzzing/configs/model_config.yaml

一分钟看懂架构

在进入有趣的部分之前,先弄清楚各个组件怎么配合是很有帮助的。整个系统分三层:

我最看重的设计原则是职责分离要干净:LLM Agent 负责决策,MCP 工具负责执行。

Agent 决定要测什么、写什么测试框架、接下来追哪个覆盖率缺口。工具只暴露像 run_afl_for 或 compile_harness 这样的基础操作。Agent 从不直接调用 AFL 或 clang,而是用这些积木块拼出整个流水线。

所有状态都存在一个 SQLite 数据库(fuzz_context.db)里,所以各个阶段之间不会在内存中传递数据,只通过数据库交互。

还有一个不起眼但很重要的细节:每个测试框架会构建两次。

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 二进制回放队列,拿到真实的覆盖率报告,接着读一遍未覆盖分支的列表。根据读到的结果,它从几个动作里挑一个:

时间预算每轮翻倍:30 秒 → 60 秒 → 120 秒 → 240 秒 → 480 秒 → 960 秒(每个目标约 32 分钟)。

思路是前期多跑便宜且短的轮次(此时大量容易摘的覆盖率果实还没摘),后期再拉长轮次(此时 fuzzer 需要更多时间才能突破某个难缠的判断条件)。

和我的手工流程一样,这里也要回答一个问题:什么时候停?循环用的是平台期检测:一旦连续两轮迭代的覆盖率增长都低于一个可配置阈值(默认是行覆盖率绝对值提升 1%),循环就判断已经收益递减,转向下一个目标。

这样能避免 agent 为了挤出最后 0.1% 的覆盖率,耗费数小时算力。

结构感知模糊测试

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 经典字典的形式输出(数值常量同时提供两种字节序,这样无论在什么主机字节序下,fuzzer 都能满足对 4 字节魔术值做 memcmp 的判断)。随后每完成一个覆盖率步进,流水线会检查未覆盖行附近的守卫条件(strncmp、memcmp、case 0xN、== 'X'),把新发现的 token 追加进去。字典会朝着 fuzzer 还够不到的代码不断生长。

4. 语料拼接操作符。

智能变异器还能从语料库目录里加载文件,把其中随机片段拼接到输入里,这是 AFL 原生 havoc 模式不太擅长的重组式操作。

不断进化的语料库

有一件事会悄无声息地拖垮模糊测试的效率:丢掉已有进展。如果每次都从原始种子开始跑,你就得反复为重新发现同样的路径付出代价。为了避免这种情况,每个测试程序都分配了一个稳定的语料库目录,它跨迭代、跨整个测试活动持续存在:

<workspace>/corpus/harness_<id>/

每轮迭代结束时,AFL 的队列会合并进这个目录,并用 afl-cmin 处理一遍,让体积保持可控。这样一来,昨天发现的有价值的输入会带到今天的运行中,上周测试中找到的输入会带进这一轮。中途停下再重启,什么都不会丢。

崩溃分类与漏洞报告

找到崩溃只是完成了一半工作。做过根因分析的人都知道,分类往往是整个流程中最枯燥的环节。而这正是这个 Agent 的另一个亮点。

模糊测试循环结束后,三个阶段会自动执行:

第一,每个崩溃都会用 afl-tmin 最小化,在 ASan 下重放以捕获堆栈跟踪,再按栈顶哈希去重(取顶部规范化帧,去掉模板、内联命名空间和 LTO 后缀,这样语义相同的崩溃会被合并到一起)。

第二,把之前已知的崩溃拿到当前二进制上重放,看看上游修复是否已经解决了它们。

第三,Agent 读取测试程序源码和崩溃函数,从公开 API 反向追溯调用链,为每个崩溃写一份 markdown 报告。

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

区分一个真正的漏洞(能通过公开 API 触达并利用)和一个仅仅是测试程序本身的 bug(问题出在我们自己的测试代码里,不是库),正是那种过去需要我坐下来手工追踪代码才能做出的判断。

每份报告都包含根因分析(附带文件与行号定位)、可达性论证、可利用性评估、以 unified diff 形式给出的修复建议,以及一个回归测试草稿。需要说明一点:建议补丁标注“需人工复核”是有原因的。Agent 的分析受限于模型对目标代码的理解程度,确实会出错。把这些结论当作一份准备充分的起点交给人类来判断,而不是最终结果。

实时仪表板

跑一场自主运行的任务却看不到它在做什么,这种感觉很难受。所以整条流水线会把所有信息发布到一个实时 HTML 仪表板上。启动任务后,它会在后台自动运行,监听端口 8765。在 Codespace 里这个端口会自动转发,所以你可以用任意浏览器打开仪表板,实时查看任务进展。页面上展示的内容包括:

结语

我做这个项目的动机,源于任何安全研究员都熟悉的局限:模糊测试确实有效,但没有人盯着就没法规模化,而人的注意力就是瓶颈。Fuzzing Taskflow 是我对这个瓶颈的一次回推尝试——把那些重复性的工作(编写 harness、阅读覆盖率、追踪覆盖盲区、分类崩溃)交给 LLM agent,同时让 agent 的判断和实际干活的工具之间保持清晰的边界。

如果你是 C/C++ 项目的维护者,不妨试一试。如果你的项目从未做过模糊测试,这个工具能帮你快速起步;如果之前已经做过,它可能通过提升覆盖率帮你发现新的 bug。源码是开源的,遇到问题欢迎提 issue,也欢迎贡献代码!

查看原文