← 文章 / AI技术
GitHub Blog 6小时前 · 2026-09-25 03:23:32 · 2 阅读

基于 GitHub Security Lab Taskflow Agent 的 AI 驱动 Fuzzing

如果你对 Fuzzing 还不太熟悉,想先从基础学起,可以看看我们的 Fuzzing 101 课程,链接在这里:gh.io/fuzzing101。

持续 Fuzzing 并不是包治百病的灵丹妙药。即便是接入 OSS-Fuzz 多年的项目,仍然可能藏着致命漏洞,而原因往往大同小异:总得有人盯着代码覆盖率,为那些尚未被触达的代码编写新的 harness,并对崩溃结果进行分类处理。换句话说,Fuzzing 至今仍然离不开人的参与。

于是,我不断在思考:这些人工环节中,究竟有多少能真正交给 LLM agent?

正是这个问题,促使我打造了 Fuzzing Taskflow——一个面向 C/C++ 项目的自动化 Fuzzing 流水线。你只需指定一个 GitHub 仓库,剩下的交给它就行:自动识别合适的入口点、分析构建系统、生成 harness、运行 AFL++、读取覆盖率报告、持续优化 harness、对每个崩溃进行分类,并针对每个独立漏洞生成报告,全程无需人工值守。

Fuzzing Taskflow 基于 GitHub Security Lab Taskflow Agent 构建,这是我们推出的、用于开发 LLM 驱动的安全自动化流程的框架,因此整条流水线被组织成一组由 agent 端到端执行的 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 会自动完成所有前置步骤:

  • 安装 AFL 等软件
  • 克隆仓库
  • 定位代码中最相关的函数
  • 为这些函数生成 fuzz target

如果你想在开启长周期任务前,先做个快速冒烟测试,就选一个小项目试试:

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

运行前需要提醒一点:这个 taskflow 会在宿主机上直接运行 afl-fuzz、clang 以及 LLM 选定的任意构建命令,中间没有任何容器隔离。理论上,一个被提示注入的 agent 能做你的用户账号能做的任何事情。所以请务必只在一次性环境(如 Codespace 或临时虚拟机)中以非特权身份运行。

Seclab Taskflows 模糊测试截图。

模型选择

一些前沿模型会对输出施加安全护栏。在这个模糊测试 taskflow 中,我们默认使用 Claude Sonnet 5,因为它顺利通过了我们所有的内部测试。如果想换模型,可以修改这个文件:src/seclab_taskflows_fuzzing/configs/model_config.yaml。

一分钟看懂架构

在深入细节之前,先了解一下整体结构会很有帮助。整个系统分为三层:

  • Shell 驱动脚本(run_fuzzing.sh),负责串联各流水线阶段。
  • 一组 taskflow YAML 文件,每个阶段一个,本质上就是告诉 LLM agent 每一步该做什么的 prompt。
  • 一组 MCP 工具,供 agent 调用来真正干活:运行 AFL、编译 harness、存储 crash、读取覆盖率报告等等。

我最看重的设计原则是职责的清晰分离:LLM agent 负责决策,MCP 工具负责执行。agent 决定 fuzz 什么目标、写什么 harness、接下来追哪个覆盖率缺口;工具只提供 run_afl_for、compile_harness 这类基础原语。agent 从不直接调用 AFL 或 clang,而是把这些积木组合成流水线。所有状态都存在 SQLite 数据库(fuzz_context.db)中,各阶段之间不通过内存传递数据,只通过数据库交换。

一个容易被忽视但很关键的细节:每个 harness 都会被编译两次。AFL 的边插桩(edge instrumentation)虽然能很好地指导 fuzzer,但无法生成人类可读的覆盖率报告。因此,每个 harness 都会生成两个版本:一个是 .afl 二进制文件(使用 afl-clang-lto -fsanitize=address,undefined 编译),另一个是 .cov 二进制文件(使用 clang -fprofile-instr-generate -fcoverage-mapping 编译)。.afl 文件负责执行 fuzzing,.cov 文件则在之后回放 AFL 生成的队列,以产出真实的源码行和分支覆盖率数据。

覆盖率反馈循环

这是整个流程的核心,也是最直接自动化我之前描述的手动工作流的部分。

如果你曾尝试手动提升 fuzzing 覆盖率,就会知道这是一个迭代过程,大致如下:

三个气泡分别写着:运行 fuzzer、检查覆盖率、改进 fuzzer。它们通过三个箭头连成循环流程。

过去,“检查覆盖率”这一步由我手动完成,通过阅读 LCOV 报告寻找未覆盖的分支。“提升覆盖率”这一步也由我手动完成,要么编写新的 harness,要么构造新的测试输入。Fuzzing Taskflow 将这两步都交给了 agent 处理。

在每个迭代中,针对每个 harness,agent 会在特定的时间预算内运行 AFL,将队列回放给 .cov 二进制文件以生成真实覆盖率报告,然后分析未覆盖分支列表。根据发现的情况,agent 会从少数几种动作中选择一个:

  • 添加一个新的种子,专门用于触达某个未覆盖分支
  • 修改 harness 源码,使其调用额外的 API
  • 自动丰富 AFL 字典,加入 guard 正在比较的魔法常量(magic constants)
  • 如果某个 gap 属于冷路径(cold error path)或不值得深入挖掘的 vendor 代码,则直接跳过

时间预算在每次迭代中翻倍:

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

这种设计的思路是:前期花费成本较低的短轮次(此时有大量易于获取的低垂果实式覆盖率),后期则采用更长的轮次(此时 fuzzer 需要更多时间来突破复杂的 guard 逻辑)。

和手动工作流一样,我同样需要回答一个关键问题:什么时候该停下来? 这里的循环采用平台期检测机制:当连续两次迭代各自获得的绝对行覆盖率增益都低于可配置阈值(默认 1%)时,循环便判定收益已呈边际递减,随即继续推进。这避免了智能体为了挤出最后零点几个百分点的覆盖率而白白消耗数小时算力。

结构感知模糊测试

AFL 默认的字节级变异器(位翻转、算术运算、块拼接)在处理二进制格式时表现出色,但面对结构化、基于文本的输入时却力不从心。传统方案是为每种格式手写自定义变异器,这是一项枯燥的重复劳动。这一次,我希望流水线代劳,因此它配备了四种互补机制来生成结构感知的输入。

1. 按格式定制的词典与自定义变异器。对于识别得出来的输入格式(JSON、XML、正则表达式、PNG、带长度前缀的二进制 TLV),taskflow 会附带预构建的 AFL 词典以及 LLVMFuzzerCustomMutator C 语言文件。JSON 变异器执行标记拼接和配对括号复制;XML 变异器熟知标签、实体及“十亿次笑声”攻击标记;正则表达式变异器则内置真实的 ReDoS 模式。每个变异器都会将一半的变异操作委托给 AFL 默认的字节变异器,这样既保留了引擎自身的随机性,又避免了与其对抗。

2. 源代码级词典。对于流水线无法识别的格式,它会扫描目标自身的 .c/.h 文件,即时生成自定义变异器。该过程会提取字符串字面量和 32 位数值常量(来自 #define、case 和 enum),过滤掉噪声信息,并将其用作拼接标记。背后的逻辑很直接:解析器所校验的最具价值的魔术值,通常都白纸黑字地写在它自己的源代码里。

3. 动态生成的 AFL 词典,基于覆盖率持续扩充。同一组源码 token 还会在第 1 轮迭代前输出为一份 AFL 经典词典(数值常量同时给出大小端两种表示,这样无论宿主机字节序如何,fuzzer 都能通过针对 4 字节魔数的 memcmp 校验)。此后每完成一次覆盖率分析,流水线会检查未覆盖行附近的守卫条件(strncmp、memcmp、case 0xN、== 'X' 等),并把发现的新 token 追加进词典。词典会朝着 fuzzer 尚未触达的代码方向不断生长。

4. 语料库拼接算子。这个智能变异器还能从语料库目录加载文件,并把其中的随机子区域拼接到输入里——这种重组式算子是 AFL 自带的 havoc 突变做不好的。

持续演进的语料库

悄悄拖垮 fuzzing 效率的一个常见原因是丢失已有进展。如果每轮都从最初的种子开始,就得反复付出重新发现相同路径的代价。

为了避免这一点,每个 harness 都有一个稳定的语料库目录,它能跨迭代、跨整个 campaign 持久保存:

<workspace>/corpus/harness_<id>/

每轮迭代结束时,AFL 的队列会被合并进该目录,并通过 afl-cmin 精简以控制体积。效果是:昨天发现的有趣输入会带入今天的运行,上周 campaign 中找到的输入会带入这一轮。即使中途停止再重启 campaign,也不会丢失任何进展。

分诊与漏洞报告

发现崩溃只是完成了一半工作。做过根因分析的人都知道,分诊往往是整个流程中最繁琐的环节。而这正是这个 agent 的另一个亮点所在。

fuzzing 循环结束后,三个阶段会自动运行。首先,使用 afl-tmin 最小化每个崩溃,在 ASan 下重放以捕获堆栈跟踪,并通过 堆栈顶部哈希 去重(对顶部规范化帧进行模板、内联命名空间和 LTO 后缀剥离,使得语义上相同的崩溃得以合并)。其次,将已知崩溃重放到当前二进制文件中,以检查上游修复是否已解决这些问题。最后,agent 读取 harness 源码和崩溃函数,从公共 API 回溯调用链,并为每个崩溃生成 Markdown 报告。

每份报告都会分配以下裁定之一:

  • vulnerability
  • library_hardening
  • harness_bug
  • OOM
  • timeout
  • assertion_failure
  • duplicate

真正的 vulnerability(可通过公共 API 触达和利用)与 harness_bug(错误存在于我们自有的 harness 而非库中)之间的区别,正是过去需要我坐下来手动追踪代码才能做出的判断。每份报告都包含带有 file:line 引用的根因分析、可达性论证、可利用性评估、以 unified diff 形式提出的修复建议 以及回归测试草案。

澄清一下:建议的补丁标记为“需人工审核”是有原因的。agent 的分析受限于模型对目标代码的理解,它确实会犯错。请将裁定视为为人类精心准备的起点,而非最终结果。

实时仪表盘

运行自主任务却无法查看其状态会让人不安,因此管道会将所有内容发布到实时 HTML 仪表盘。启动任务后,它会在后台自动启动,监听 8765 端口。在 Codespace 中该端口会自动转发,你可以随时在浏览器中打开仪表盘,实时查看任务进度。

Screenshot of the fuzzing dashboard.

页面还展示了以下内容:

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

结语

我启动这个项目,是出于对所有安全研究员都熟悉的痛点的思考:fuzzing 确实有效,但缺乏人工介入就难以规模化,而正是这份人工关注构成了瓶颈。Fuzzing Taskflow 旨在通过将重复性环节——如编写 harness、解读覆盖率、追踪缺陷及分诊崩溃——交给 LLM agent 处理,从而把瓶颈后移,同时保持 agent 的判断与实际执行工作的工具之间的清晰边界。

如果你是 C/C++ 项目的维护者,欢迎试用。如果你的项目从未进行过 fuzzing,这个工具能帮你快速起步;如果之前已经做过,它或许能通过提升 fuzzing 覆盖率来帮助发现新 bug。

源码已开源,如发现任何 bug 请提交 issue,也欢迎贡献代码!

原始来源: GitHub Blog

评论 (0)