让 agent 攻击自己刚写出来的固件,听起来很带感。但有个残酷现实:一个不能复现的漏洞,等于没发现。模型这次把固件搞崩了,你换个温度、换个模型再跑,它复现不出来——这种 finding 毫无工程价值。

Firment 的红队功能整个是围绕「可复现」设计的。

两层金字塔

  • 底层:确定性变异语料。给定种子 + 一个合法基线帧,生成一串固定顺序的畸形输入(边界长度、逐位翻转、超长、格式串、分隔符混淆、数值极值)。同种子同字节序列,跨机器一致。一个漏洞的复现方式就是 seed + case id,不需要模型在场。
  • 上层:LLM 攻击 campaign。在语料跑完后,让攻击者子 agent 探索语料没覆盖的(构造结构上错但语义相关的帧、竞态两个输入)。但它的发现必须回填成语料里的一个 case 才能进报告,否则降级为 UNVERIFIED。

崩溃哨兵:优先级很关键

每个用例发完 payload,收集一段输出窗口,纯文本分类:

  1. Crash —— 命中故障签名(HardFault/panic/…)
  2. Reboot —— 启动横幅在已有流量后重现(不该出现时又出现了)
  3. Hang —— 定义了心跳、窗口内没命中、且超时
  4. Alive —— 心跳命中,或根本没定义心跳契约

Crash 压过 Reboot:崩了又被看门狗重启的目标,是 crash 不是神秘重启。沉默不等于死亡:没定义心跳时,一片安静判 Alive——你不能宣称一个从没建立过「存在」的东西缺失了。

证据封顶

finding 必须引用捕获文件(崩溃还会在复位前抢一份 debug forensic 现场快照)。finalize 检查这些文件真实存在,缺证据的封顶 low + UNVERIFIED。模型写的 campaign 证据更严:payload 必填、引用路径必须落在本次运行目录内、且文件里得真的有它引用的那行——防止它拿一个存在的无关文件(甚至报告自己)冒充证据。

恢复与边界

崩了的目标在用例之间会被重刷/复位救回来;救不活就中止整个套件,而不是对着死板子把后面每个用例都判成假 Hang。headless 实弹必须显式 --live,否则只做语料彩排。

一个诚实的局限

任何 debug 动作(包括 forensic)退出后目标都停在 halt——所以「碰过调试器之后的沉默」是攻击者自己的产物,不是 hang 的证据。这条我写进了权限闸门和 campaign 提示词里,宁可啰嗦也不让它自证。


红队的价值不在「聪明地找到 bug」,在「找到之后别人能复现、能回归」。确定性打底、LLM 探索,这个顺序不能反。