一、项目出发背景
做固件审计的师傅大概都被这几件事磨过:
- 工具不缺,binwalk、FirmAE、radare2、QEMU 都在,可它们之间那段路全靠人肉走;
- 让大模型去跑命令,我心里发虚——它一句
apt install就能把我环境搞乱; - 它说”这里有个命令注入”,自己复现利用失败,转头追问AI,AI:是我的错误…;
- 真挖到了洞,后面还得手抄复现步骤、整理证据、套 CNVD 模板,一晚上又没了。
所以有了 FirmAudit2:让Agent去查,同时把”结论可信”这件事本身变成一个工程问题——过程可回放、字段可校验、产物可核对。
三层约束设计:
1. 过程约束:漏洞资料按段落逐段引导填充,每段过校验(第三节);
2. 结果约束:任务收口和交付前各设门,共四道(第四到第七节);
3. 运行时约束:会话级隔离 + 单次工具调用级活性看护(第八、九节)。
实践数据与项目代码信息由AI负责整理,内容较多且考虑到截图时敏感信息的处理(其实是我懒得打码),数据统一从log/database中提取并使用文本格式展示,建议略读¯꒳¯
后文项目能力展示以 Tenda AC15升级软件 V15.03.05.18(2017.05.16)为例,所有漏洞均已在CNVD、CVE平台公开;AI模型为自己中转的 Opus4.6
二、系统总览

能力概览

工作流程图

交付门由四条判据串成,顺序固定:本地仿真验证 → 八段齐备 → 报告复核(含漂移复查)。代码在 internal/agent/findings_service.go:
1 | // DeliveryGaps 按八段式段落表回查漏洞资料缺口(节选) |
gapWithReason 会把原因拼在缺口名后面(本地仿真验证:证据里看不到目标被起来的痕迹……),最长 160 字裁断。
界面上每一条缺口就是一行能读懂的人话,操作员不必去猜平台为什么不出包。
三、过程约束:分段引导与校验回环
为什么要有它
一开始我和大家的想法一样:把要求写清楚,Agent 自己就会按格式产出。
结果挺难看。同一个任务里它会给我三段”我觉得这里有洞”的散文,复现步骤的具体命令和数学答案一样“略略略”,报告内容结构东一块西一块(逻辑体系可读性低),报告我拿过来还得重写一遍。
问题多半出在模型被允许一次性自由发挥,而质量这种东西一旦自由发挥就没法校验。
于是换成:先登记,再逐段填充,每段都过校验,填不完不算完成。以较长的输入去约束得到更标准的、较少的输出(我称为“分段式引导设计”)
漏洞资料的 8 个段落与交付报告的章节一一对应:
1 | // internal/agent/vuln_guided.go |
这张表同时驱动三处:下发引导提示词、校验提交规则、渲染报告和 docx 章节。
填写要求与最终交付内容都出自同一份定义,不会出现对不齐的情况。
在工程上这叫单一事实源(SSOT),放到审计场景里,它换来的是可校验性。

打回是下一句提示词的开头
校验不通过时,平台只记日志没有意义,得把具体原因拼回下一次引导里:
1 | // recordVulnFillFailure 记录段落校验失败:重试计数 + 最近错误,超限后停等转人工 |
回执也是新加的。以前 Agent 提交完文件就被平台挪走了,它不知道文件是收了还是拒了,只能凭感觉重来,于是出现”同一个漏洞登记三次”。现在每个 inbox 文件都按原名在 .cpi/vulns/receipts/ 留一份 accepted / rejected + 原因 + 下一步:
1 | s.writeVulnReceipt(task.ID, dirEntry.Name(), vulnReceipt{ |
一次只推进一个漏洞,投递还要对账
1 | // sendPendingVulnGuides 按登记顺序串行驱动引导(节选) |
rearmLostGuide 管的是另一件更隐蔽的事:写 FIFO 成功,并不代表 Agent 收到了。
Pi 在回合中间不读 stdin,消息可能被整轮吞掉。所以每次投递记下时刻,超时窗口内等不到任何一次段落提交,就判定这条没送到,重投;重投超限发一条 audit.guide.lost 提醒人工,而不是假装发过了。
实践任务判定日志:
1 | 2026-09-21T08:13:54 audit.guide.lost VULN_01_CWE78_pingSet |
效果对比(由AI对本地任务数据的情况统计)
下面每行都是本平台跑出来的任务事件,任务号在最后面的编辑提示里:
| 时间 | 引导方式 | 段落入库 | 打回 | 结局 |
|---|---|---|---|---|
| 9/19 14:17 | 5 个漏洞并行下发首段 | 0 | 0 | 引导发完就没下文,5 条全部无回应 |
| 9/20 06:33 | 4 个漏洞并行,已加校验回环 | 4(全是首段) | 12 | 4 条一起卡在第 2 段(复现步骤),任务最终失败 |
| 9/21 01:20 | 单漏洞串行推进 | 8/8 | 4 | 6 分 48 秒填齐,全部自愈 |
| 9/21 08:14 | 串行 + 8 段 + 回执 + 送达判定 | 24/24(3 个漏洞) | 7 + 1 次投递判丢重投 | 恢复后队首漏洞 6 分填完 7 段,第二、三个各用 4 分 40 秒、3 分 36 秒 |
在第二行对应的任务中的 12 次打回里,有 8 次是同一个原因:Agent 爱把步骤写成 JSON 数组,而校验只认字符串。表面看像模型不配合,其实是我平台太挑剔。折中之后两种写法都收,数组自动折叠成编号行:
1 | // stepsPayload 解析 {"steps": …}:字符串与字符串数组两种写法都接受(节选) |
剩下的打回原因是真在拦东西。9/21 那次队首漏洞的 6 条打回,原文如下(一条都没加工):
1 | description content.text 不能为空且不少于 30 字(当前不足) |
同一条漏洞填齐之后的复现步骤,从空变成 970 字的命令级链路:
1 | 1. 校验并解包固件: |
「代码位置」段也不再是一句”在 httpd 里”,而是可核对的反汇编:
1 | /lib/libapcommon.so 0xf2bc-0xf354 |
报告质量变成了可校验的状态,靠的是判据和回环,文件索引级审计分析报告使最终报告的可靠性和可读性显著增大。

四、结果门之一:收口门——状态判定的前置条件
引导管过程,这道门管结论。任务结束判定之前,平台先看一遍引导进度,没填完就不许说完成:
1 | // pauseForUnfinishedGuides 是"资料没填完就不算完成"的收口闸门(节选) |
实践过程的能力反馈: 9/21 任务,当天 07:12 上游模型连续返回 500 导致项目收敛,会话直接结束,Agent 一条总结都没留下:
1 | 2026-09-21T07:12:57 task.paused |
一小时后我触发“恢复审计”,同一条漏洞接着从第 3 段填到第 8 段,前后 6 分钟。(狠狠夸下自己“断点续传”的设计)
同一套思路还管着另一种尴尬:Agent 跑完了、一条漏洞都没有。这种情况以前会把任务停在”运行中”,看着像还在干活。现在统一收口成”暂停中”,并把缘由写进状态字段:
1 | // 与手动暂停的区别是必须把缘由落到 last_error:自动暂停没有操作员在场, |
平台自己做的决定,就得自己解释——自动决策的可解释性同样也是产品的一部分。
五、结果门之二:本地仿真验证硬门——证据只认运行时输出
动机来自一份看起来很好的资料
实践过程负反馈:9/21的任务,三条漏洞全部 8/8 填齐、全部打包成功。其中命令注入那条的复现步骤末尾,Agent 自己写了这么一行(真实数据,只删去敏感信息):
1 | 验证级别说明:本漏洞通过深度静态分析完成证实,正则绕过已通过 Python 脚本验证, |
翻译为人话:目标它没起来过。可这条资料字段齐全、逻辑自洽、反汇编精确到指令,顺利出了包。
静态分析做得再漂亮,它也只是分析;CNVD 评审要的”已成功复现”,缺的就是这一步。
所以加了硬门,判据两条:复现步骤里要有实际生效的启动命令,证据文件里要同时出现”目标被起来”和”目标给出结果”两类痕迹。
1 | // emulationEchoTracePatterns 是"目标给出了结果"的痕迹(节选) |
痕迹是从文件里读出来的,Agent 没法靠蒙混过关。
证据是 script -qec 命令转录和 FirmAE 日志,关键回显往往在结尾,只读头部会把真验证误判成未验证。
1 | // readEvidenceTrace 小文件整读,大文件读头 emulationTraceMaxBytes + 尾 emulationTraceTailBytes 两段 |
两类痕迹都取不到时,缺口原因长这样(真实输出):
1 | 本地仿真验证:证据里看不到目标被起来的痕迹(需含 qemu/FirmAE/Unicorn 启动输出或服务 |
段落引导阶段它也在拦。9/21 第二条漏洞的复现段被打回过一次,原因写得很具体:
1 | 复现步骤缺少把目标跑起来的具体命令:需至少一条仿真/服务启动命令(如 bash ./run_emulation.sh、 |
有一类漏洞不必起目标:硬编码密钥、弱口令这类,文件系统里就能确证。
类型豁免写在 emulationExemptKeywords + normalizeVulnType 里,避免门变成死规矩。
六、结果门之三:资产证明门——版本信息必须在头两步
CNVD 报告的第一诉求其实很朴素:先证明你复现的是这块固件(需要展示版本号/固件信息)。解包之后一条 cat 把版本信息打出来,评审者一眼就能对上报告标注的版本号。这一步 Agent 从来不会主动写,因为提示词里没人要求过。
实现单独一个文件,判据是”命令动词 + 版本载体在头两个编号步骤内同现”:
1 | // internal/agent/version_proof.go(节选) |
只看前两步是有原因的:哈希校验和解包通常占掉第一步,版本信息紧随其后才叫”首要步骤”。拖到起目标、发 payload 之后再提版本,就成了顺带说一句,评审者无法据此确认前面每一步操作的是哪份固件。
接线点在段落校验里,复现段一次过三道判据:
1 | // internal/agent/vuln_guided.go case "reproduce":(节选) |
同一条判据还有第二个出口:versionProofDefect 被报告复核门复用,引导阶段和复核阶段是同一把尺子。
七、结果门之四:报告复核门,以及对”事后回改”的重开
八段资料填齐只说明材料收齐了,报告正文还得写对。正文由 Agent 撰写,路径固定在该漏洞资料夹里的 report.md,它是交付报告的唯一事实源(docx 由 pandoc 从它转换)。平台不下发”请自检一下”这种没判据的要求,而是把正文和已入库的结构化字段逐条对账:
1 | // internal/agent/vuln_review.go 文件头注释(节选) |
章节由 reportAnchors 声明(关键词 + 该章节的附加校验),复现章节挂 requireReproduceChain,里面同时调用版本证明判据;正文长度下限 vulnReportMinRunes = 800,低于这个数必然是骨架稿。连续 vulnReviewMaxRounds = 3 轮不过转停等,和段落引导一个口径。
人工”确认”仍然是最权威的终审,它会直接解除复核门:
1 | // resolveVulnReviewByHuman:确认动作本身就是比 AI 复核更权威的终审, |
过了门也不是一劳永逸
report.md 是可写的。复核通过后 Agent 再改正文、把资料夹里的证据文件删了,”已复核”就变成一句过期的话。所以交付门的判定里对已过复核的漏洞再跑一次同一套对账,一旦发现漂移就把复核门重开:
1 | // DeliveryGaps 只读报告缺口,不改状态;重开由引导队列负责(节选) |
这里有个细节我特意留着:DeliveryGaps 只报告缺口,状态变更只在 reopenVulnReview 一处。读一次判据函数就顺手改一次状态,是这类监视循环最容易出双写冲突的地方。

八、运行时约束:给”一次工具调用”上计时器
固件审计最常见的翻车现场,是命令没报错、却永远不回来。
吃了实践任务的亏:Agent 本意执行 ping 1;id,参数被目标拼成 ping 1 -c 4 …,对面不回包,进程挂住 1 小时 44 分钟,把任务总时长吃光,期间已产出的证据全部作废。任务级超时挡不住这种事,因为它的粒度是”整个任务”。
现在的看护阈值是这么定的:
1 | const ( |
判静默的依据是:Pi 的 bash 工具边执行边回流输出,只要还在往外吐字就不算卡死;并行调用时取”最近活动最新”的那个作判据,避免一个还在跑、另一个卡住被误杀。
15 分钟这个值也是从数据里挑的——本机运行库 419 次已完成的工具调用里,最长两次是 16.2 分钟和 15.6 分钟,内容都是 find / -name … 全盘扫描;再往上就该被拦。
处置动作是分级的:先中止回合,紧接着把执行纪律原样重新说一遍,并把剩余预算摊开讲清楚,实在不认账才提前判失败:
1 | 【平台看护 · 回合已中止】 |

九、环境约束:会话级 overlay 临时根
隔离走的 WSL2 独立发行版 + 命名空间,使用会话级 overlay 临时根实现任务隔离:Agent 在回合里 apt install、改 /etc、往 /root 写东西全部成功,但写进的是这个会话私有的 upper 层,回合结束随命名空间一起销毁。
1 | # internal/sandbox/scripts/firmaudit-task.sh cmd_session_root()(节选) |
两个细节我比较在意:
- overlay 挂不上可以降级,退回只 bind
/workspace的最小隔离,不因隔离失败把任务卡死; /workspacebind 不上必须直接退出。整套上报协议都以/workspace为锚,静默继续等于 Agent 往共享目录里写,产出一堆、平台一条收不到——这个事故我真踩过,比任务报错危险得多。
另外任务状态落盘在 .sandbox/<key>/(env/cmd/rpc/exit.code),包装器用 setsid 脱离父进程,程序重启不打断正在跑的审计。
十、把约束写进提示词,再用回归测试锁住
看护是兜底,更省事的是别让 Agent 写出会挂死的命令。审计和复现两个入口提示词里都有一节【非阻塞执行纪律】,四条硬约束:
1. 会打到目标 / 仿真栈上的命令必须显式 timeout -s KILL N 包裹(会吞 SIGTERM 的守护进程要用 -s KILL);
2. 探测自带次数与超时:ping -c 2 -W 2、traceroute -m 5 -w 2、nc -w 3、curl -m 5 --retry 0,严禁裸 ping;
3. 长命令后台化 + 轮询日志,不在前台干等;
4. 注入段放在被污染参数之前,并用 # / %23 注释掉残余参数。
1 | If pingIp = '1;id', command becomes: ping 1;id -1 -c 1 -s 32 -W 5 |
ping 1 是个不带 -c 的裸命令,永远不回;id 后面拖着 -1 -c 1,也执行不了。写成 1 -c 1;id;# 才对:先注入,再注释残余参数——第三节那条 970 字步骤里的 ";id;#" 就是这条约束生效之后的产物。
提示词会被人改,改坏了很难发现,所以给它配了断言:
1 | var nonBlockingMarkers = []string{ |
这一轮加了三道门之后,同一套做法也用在了门口径上:复核引导里列出的五条必过项,逐条对应 validateVulnReportMarkdown 的实际判据;
firmware.md、reproduce.md、两个 skill 和agents/common/AGENTS.md里关于结果痕迹只认运行时输出的表述和 emulationEchoTracePatterns 的注释保持同一份措辞。
十一、交付形态:自包含平铺总包
这一段代码量最大,讲起来最没意思,但它是前面所有约束的落点。任务收口时,过了门的漏洞会被打成一个自包含的 CNVD 复现资料总包:
- 一漏洞一档,材料全部平铺在包根,评审者不需要理解目录结构;
- 固件保留上传时的原文件名随包,脚本自己定位同目录的固件包(
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)),复现步骤里的./<固件包名>才指得通; - 复现步骤到命令级,从校验哈希、出资产证明、起仿真到发请求,逐条能抄进终端;
- 报告章节和第三节那张段落表同源,要求填的就是报告里有的。
固件副本走同一条通道,取不到就显式失败:
1 | // packaging.CopyFirmwareInto(节选):按 attachments/(审计)与 attachments/firmware/(复现) |
平铺装配用 flatWriter,它管三件事:内容哈希去重、认得已经在包里的文件、把该进包却没进的材料记下来。
1 | type flatWriter struct { |

十二、现有产出展示
目前就一个下编号了,其他的现在在等待三审/验证。(产出后续会在评论区展示,但我会厚码˶>ᗜ<˶)
实力不详,遇强(AI模型)则强(项目产出效果):

十三、购买入口(白帽集市)

FirmAudit2 · 固件漏洞审计平台
- 【价格】 ¥50(中秋 + 首发公测价)
- 【授权方式】 单设备永久授权
- 【库存数量】 20
- 【售后与更新】 版本更新:保持迭代推送;答疑渠道:微信群聊
- 【购买入口】 白帽集市搜索「FirmAudit2」,或直接扫描上方二维码 / 点击阅读原文
- 【前置环境】:Windows 11 x64、已启用 WSL2、磁盘建议 ≥ 40GB 可用、首次部署工具链需联网。大模型 API 需自备,平台不内置任何模型额度。
说些什么吧!