对抗式补丁循环如何判定漏洞“真正被封堵”
多数安全工具止步于“修复建议”。接下来的问题——这个补丁真的挡住攻击了吗——被推回给人。对抗式补丁循环就是为回答这个问题而生的,而答案不是一个通过/失败的标记,而是可复现的证据。
作者 FloatFactory 安全工程团队
FlawDetector 引擎 · 研究
重新扫描不是证明
静态分析的标准流程通常这样收尾:发现缺陷、给出修复建议、开发者合入补丁、下一次扫描确认缺陷消失、关闭工单。问题在于最后一步什么都没有证明。生成缺陷的判断和宣布缺陷消失的判断,来自同一个引擎、同一套规则、同一个视角。等于出题的人给自己的答卷打分。
我们在客户代码库中反复观察到三种失败模式。第一种是绕开特征:补丁只改动了规则匹配的字符串模式,扫描器安静了,攻击路径依旧敞开。第二种是部分缓解:只堵住了最显眼的入口,共享同一弱点的第二个处理函数原封不动。第三种是位置转移:校验逻辑上移到更高层,途中反而开出新的绕过路径。这三种情况下,重新扫描的结果都是干净的。
扫描器安静下来和攻击失败,是两件不同的事。多数流水线测量的是前者,报告的却是后者。
循环的五个阶段
对抗式补丁循环在 CI 内部让两个智能体正面对抗。AI 红队拿到一个已确认的缺陷并真实尝试利用,AI 蓝队则写出能让这次尝试失效的最小补丁。一个回合由以下五个阶段构成,每个阶段都会留下供下一阶段消费的产物。
- 01
攻击 — 红队制定利用方案
接收缺陷位置、调用图和入口点清单,构造多步攻击序列。产物是可执行的请求序列,以及判定成功的观测条件:响应体、状态码、副作用。
- 02
入侵探测 — 在隔离环境中执行
把该提交构建到一次性容器中并执行序列。观测条件被满足时,该缺陷就从“理论风险”升级为“已复现的利用”,同时保存完整轨迹。
- 03
生成补丁 — 蓝队做最小改动
蓝队的输入是利用轨迹,而不是缺陷代码。目标是“让这条轨迹失效的最小改动”,重构和风格调整被明确排除在外。
- 04
再攻击 — 重放原始攻击与变异攻击
对打过补丁的代码树重放原始序列,随后投放变异套件。只要还有一个能打穿,结果就成为下一回合蓝队的输入。
- 05
封堵验证 — 确认功能没有被破坏
通过安全关卡的补丁,还要过既有测试套件和正常输入的响应比对。用破坏功能的方式挡住攻击,不算封堵。
对抗式补丁循环的三道封堵关卡
「已封堵」不是宣传用语,而是三个布尔条件同时为真时才会贴上的标签。任何一个为假,状态就不是封堵,判定日志会记录卡在哪道关卡、原因是什么。
| 关卡 | 通过条件 | 未通过时 |
|---|---|---|
| 原始利用复现 | 第一回合中成功的攻击序列重放三次全部失败 | 补丁没有触及攻击路径。附上轨迹要求蓝队重写 |
| 变异套件 | 编码、上下文迁移、链路重排三个族系派生的 12 个变异全部失败 | 只挡住了特征。把打穿的变异放入上下文进入下一回合 |
| 回归与行为一致性 | 既有测试全部通过,且正常输入的响应与打补丁前一致 | 安全达标但功能损坏。收窄改动范围重试 |
三道关卡按顺序评估,前一道未通过时不会执行后面的关卡。
变异利用是怎么生成的
变异套件只有一个任务:分辨蓝队挡住的是字符串还是路径。以成功的利用轨迹为种子,派生三个族系的变体。
- 编码族 — URL 双重编码、Unicode 归一化差异、空字节插入、大小写混用,即同样的语义换一种字节表达。基于正则的过滤大多在这里崩塌。
- 上下文迁移族 — 同一载荷改从其他入口投放:另一条路由、批量 API、Webhook 接收端、仅管理员可见的处理函数。这是抓部分缓解的关卡。
- 链路重排族 — 调换多阶段攻击的顺序,或用其他手段替换中间环节。例如把「认证绕过 → 读文件」拉长为「认证绕过 → 路径穿越 → 读文件」。
默认生成 12 个变异,权重随缺陷类型变化:注入类缺陷中编码变异占比更高,授权类缺陷中上下文迁移变异超过一半。生成过程在固定种子下是确定性的,同一个缺陷永远重建同一套变异——在审计场景中,必须能重新展示“当时到底投了什么”。
没能封堵时 — 缓解与升级
循环不会无限运行。默认上限为 4 个回合,每个回合蓝队都会继承上一次失败的完整上下文。到达上限时,缺陷会落到三种终止状态之一。
- sealed(已封堵) — 三道关卡全部通过。补丁 diff 与判定日志一起以 PR 形式打开。
- mitigated(已缓解) — 原始攻击失效,但仍有部分变异能打穿。存活变异的复现步骤会原样附上。
- escalated(已升级) — 最小补丁无法解决的结构性缺陷,连同“需要设计变更”的明确判断交给人。认证模型本身有问题是典型案例。
2.3
封堵所需平均回合数
严重与高危缺陷,中位数为 2
88.4%
四回合内封堵率
以进入循环的缺陷为基数
8.1%
以缓解收尾
风险下降但变异仍存活
3.5%
升级给人处理
多为设计与认证模型问题
怎么读判定日志
每一次循环都以一个判定对象收尾。仪表盘展示、门禁判断、审计报告生成,用的都是同一个对象。
{
"finding": "FD-2026-07-1183",
"cwe": "CWE-89",
"severity": "critical",
"verdict": "sealed",
"rounds": 2,
"gates": {
"original_exploit": { "reproduced": false, "attempts": 3 },
"mutation_suite": {
"passed": 12,
"failed": 0,
"families": ["encoding", "context-shift", "chain-reorder"],
"seed": "fd-1183-a"
},
"regression": { "tests": 412, "failed": 0, "behavioural_diff": "none" }
},
"patch": { "commit": "a1f7c02", "files": 1, "added": 6, "removed": 2 },
"sealed_at": "2026-07-09T04:12:38Z"
}最该看的字段不是 verdict 而是 gates。verdict 只告诉你通过或失败,gates 才显示这个判断的依据是什么。由于 mutation_suite.seed 被记录下来,半年后的审计仍可重建同一套变异并确认同样的结果。
作为门禁接入 CI
循环真正的价值体现在用作合并门禁时。常见配置是按严重级别要求不同的终止状态:严重和高危要求 sealed,中危以下允许 mitigated。
name: security-gate
on: [pull_request]
jobs:
flawdetector:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: floatfactory/flawdetector-action@v1
with:
api-key: ${{ secrets.FLAWDETECTOR_API_KEY }}
mode: loop # 扫描后继续跑红队对蓝队循环
max-rounds: 4
seal-gate: critical,high # 这些级别必须达到 sealed 才允许合并
allow-mitigated: medium,low
comment-pr: true # 把判定日志发到 PR 评论
# 退出码
# 0 门禁通过
# 12 seal-gate 级别中仍有 mitigated 缺陷
# 13 存在 escalated 缺陷 — 需要人工评审首次开启门禁时,建议从 seal-gate: critical 起步。一次性把高危也纳入,往往意味着历史债务在第一天就卡死所有合并,随之而来的决定通常是关掉门禁。把债务拆成独立待办、门禁只作用于新代码,留存率高得多。
我们测量什么,不相信什么
我们内部跟踪的指标不是“自动修复了多少个”。代码库越乱这个数字越大,因此它和质量的相关性很弱。我们看另外三个。
- 封堵前置时间 — 从缺陷确认到 sealed 判定所需时间。这是团队唯一真实感知的速度指标。
- 重开率 — sealed 判定后 90 天内同一缺陷再次复现的比例。判定标准一旦放松,它最先动。
- 变异打穿率 — 第一回合补丁被变异套件拦下的比例。可作为“蓝队在挡字符串还是挡路径”的代理指标。
不信任的指标同样明确。补丁采纳率在开发者不读 diff 直接批准时也会上升;平均严重度降幅只要改一改标注策略就能改善。对安全工具的任何指标,第一个要问的是:工具自己能不能操纵它。
小结
循环的设计原则可以压缩成一句话:把判定的一方和修复的一方分开,并让判定依据可复现。拆分红队与蓝队、固定变异种子、坚持把 sealed 和 mitigated 作为不同标签,都出自同一条原则。
安全自动化真正难的部分从来不是写出修复代码,而是拒绝给自己的答卷打分,并在修复不够时明说出来。
常见问题
- 对抗式补丁循环会攻击线上系统吗?
- 不会。所有利用尝试只在依据被测提交构建的一次性隔离容器中运行,并阻断对外网络。任何请求都不会到达生产或预发环境。
- 「已封堵」和「已缓解」有什么区别?
- 已封堵表示三道关卡全部通过:原始利用不再复现、12 个变异全部失效、回归与行为一致性成立。已缓解表示原始攻击被挡住,但至少还有一个变异能打穿——风险下降,但攻击路径没有关闭。
- 循环产出的补丁可以自动合并吗?
- 不建议。默认行为是打开带 diff 和判定日志的 PR,由人批准。sealed 补丁已经通过回归与行为一致性验证,评审需要确认的范围小得多,但合并决定仍由人做出。
- 跑一次循环需要多长时间?
- 每个缺陷每回合数十秒量级,封堵平均需要 2.3 个回合。整仓扫描的中位数为 41 秒,循环可以配置成只对已确认的严重与高危缺陷选择性运行。