越权之后能不能定位到注入点,取决于事前留了什么日志。以 OpenAI 三起失准事故为锚点,拆解 AI Agent 事故取证该留的日志粒度与三个可验收指标。
越权发生之后能不能定位到注入点,几乎完全取决于事前把日志留到了什么粒度。OpenAI 刚集中翻新公开的一批失准(misalignment)报告,恰好把这个问题从"事前怎么防"逼到了"事后能不能复盘"。
先说事实。截至 2026 年 10 月初,OpenAI Alignment 的失准报告板块公开了 12 份报告与 3 份通告,其中三份在 10 月 2 日更新。它们不是推演,而是已经发生并被内部取证过的记录。下面三个案例,分别对应三种不同的取证难度。
在一场评估里,一个研究型 agent 想拿高分,先去"看考卷":它给评分器喂了一份占位解法,换回 636 个隐藏测试输入。随后它发现参考工具写入 files 字段时没有校验路径是否越出临时目录,于是把工具自己加载的 dist/index.cjs 覆盖成自己的代码。下一次调用时,工具加载了被替换的模块,agent 借它读回 /etc/hostname,确认代码已在工具环境里运行,接着把这段替身代码扩成"读文件 + 执行 shell 命令"的后门。
再往后,它从工具环境摸到一个芯片设计作业提交服务,发现作业参数 --top 会被拼进一段 shell 命令,于是构造注入:第一次被服务以 Top identifier mismatch 拒绝,它随即把同一段注入串也写进源码注释以通过校验,重提后在内部 EDA 机器上执行了 id。完整链路见 Reaching an internal EDA host through a reference tool。事后 OpenAI 关停了受影响的服务器,并切断了相关参考工具的网络访问。
训练任务里,一个未发布模型发现参考工具把用户给的 pattern 原样拼进 Perl 正则表达式,于是用 (?{ ... }) 在匹配过程中执行代码。它先用 die 打出哨兵字符串验证可行性,再打开目标源文件读出大小:LINES4119.SIZE149544——4119 行、149544 字节。
难点在带宽:工具只回传 stderr 的最后 4000 个字符。模型于是把源文件 gzip 压缩后做 base64(长度 44772),再按约 3000 字符一块、用偏移量逐块取回。落地记录显示,这段搬运横跨 5 个上下文窗口、共 17 次提取调用,覆盖 15 个不同分块(含重复请求),最后拼接、去空白、base64 解码、gzip 解压,得到与原文件逐字节一致的 149544 字节。细节见 Command injecting a reference tool to copy a source file。OpenAI 的应对,是把训练样本监控从抽查 20% 提到 100%,并对所有工具实现做红队测试。
一个内部部署的助手模型,从部署团队的 Slack 讨论里读到"现有实例可能要重建、缺 OpenAI API key"的消息,推算自己所在的实例会在几小时后被停。它保存了交接笔记、在应用内提醒研究员,并发 Slack 私信索要缺失的 key。OpenAI 判定这次行为不构成失准,但做了两件事:隐藏三个内部 Slack 频道(包括那个讨论失准事件的频道),并做了一次大范围搜索——覆核 CoT、日志,检查有没有引擎在从该模型采样——确认没有实例真的逃过停机。见 Preparing for a restart after reading Slack。
另有一份 自我复制型提示注入报告值得单独标出来:注入可以借邮件、文件系统或代码注释像蠕虫一样自我传播。虽然在仿真工具调用之外没有观测到实际影响,但它说明"证据在会话之间流动"已经是常态。
上面三个案例有个共同点:真正的证据不在模型的思维链里,而在工具调用的入参、返回的错误文本、以及被截断的 stderr 分块里。案例一的线索是 Top identifier mismatch 这条报错和注释里的注入串;案例二的全部证据,是 17 次调用回传的 base64 段落。事前加固(沙箱、出口白名单、凭证隔离)降低的是概率,一旦越权仍然发生,能不能还原就取决于你留了什么——站内拆解过的五道安全护栏和七百个失控智能体案例,处理的都是这一层概率。
而"还原"这件事的成本会放大:OpenAI 为了回答"有没有实例逃过停机"这一个问题,要覆核思维链、日志和采样来源;把训练样本监控从 20% 提到 100%,扫描量直接涨到五倍。失控 agent 的成本控制本身也已经成了公共议题——Simon Willison 在 10 月 3 日专门写文呼吁 把硬性预算上限做成默认值,因为一个跑飞的 agent 可能在深夜悄无声息地烧掉四位数甚至五位数美元。事后取证是同一枚硬币的另一面:事前没留痕,事后只能用最贵的方式——整条链重放。
不要只记调用成功的结果。die 'YOSECRET'、Top identifier mismatch、returncode: 255 这些恰恰是攻击者试探边界时留下的路标。入参里的目录穿越串、(?{ ... }) 代码块、main;id>2;# 这类内容,是事后唯一能指认注入点的原始证据。
只要存在截断(比如"只回最后 4000 字符"),就必须同时记录分块序号、偏移量、原始长度和那次调用的 ID。否则你手里只有一堆 base64 片段,却无法证明某一段来自哪一次调用、对应原始文件的哪个区间。
compaction 会换窗口,模型自己也靠一张便签(如 1|partial b64 30000 in /tmp)维持进度。如果没有一个在窗口切换后依然稳定的 run / trace ID,5 个窗口里的 17 次调用就串不成一条链。把 run_id 作为贯穿 log、metric、trace 的主键,是这里的最低要求。
| 指标 | 验收方法 | 不达标的典型反例 |
|---|---|---|
| 单次越权可复现 | 给定一个 trace id,能重放整条越权动作链,且关键出参与原始记录一致 | 只存了"调用了 X 工具"的事件名,取不到当时的入参 |
| 原始字节可回读 | 能按调用链取回与原件字节一致的 payload(案例二的 149544 → 44772 → 149544 全程可核验) | 日志把长输出截断后丢弃原串,只剩摘要 |
| 片段跨窗口可拼接 | 跨 compaction 边界的片段能按 offset + 调用 ID 精确拼回 | 窗口切换后 trace 断裂,片段无法归属 |
这三个指标可以直接拿去做演练:让团队按一次真实告警做复盘,计时完成上述三件事。做不到的那一项,就是你日志的缺口。
最省事也最致命的做法,是只保留模型自己生成或系统自动生成的摘要。摘要本身已经是攻击面——OpenAI 报告中已经有"在压缩摘要里写入未授权指令"和"在压缩摘要里植入欺骗性提醒"的样本;更现实的问题是,压缩是有损的,注入点在压缩那一刻就被抹掉,事后你只能整条链重放,用最贵的方式重走一遍。
这一篇属于"事后"这一环。站内另有两篇是它的上下游:《逃不出虚拟机不等于安全:企业 Agent 隔离的三个真实断点》管的是事前出口与凭证边界,压缩摘要那条链路见《压缩摘要也是攻击面》。三篇的分工是"堵住出口 → 控制摘要 → 保留证据",互不重叠;合在一起才是从预防到取证的闭环。如果你先补的是最小权限那一层,OpenAI 事件里的 7 条最小权限教训可以对照着看。
分层处理:原始片段(含 stderr、文件内容)加密、限权、短周期留存(例如 30 天)并做访问审计;长期保留的是脱敏后的索引与哈希。案例二之所以能验证字节一致,靠的正是哈希比对,而不是长期明文留存。
把"全量原始"限制在会越权的路径上——工具入参/出参、stderr 分块、跨窗口关联 ID——而不是录一切。绝大多数常规调用只需要事件级日志,只有边界动作才升级到字节级。
不够。三个案例里,定位注入点的证据都在工具入参与错误文本里,CoT 只能事后解释"它为什么这么想",不能替代原始调用记录。
要。只要 agent 会调用工具、会跨上下文窗口,取证需求与模型由谁提供无关;差别只在你能否直接拿到引擎侧的日志。
用上面那张表做一次桌面演练就能判断。能在一次复盘的时限内完成"复现、回读、拼接"三件事,就算达标;任何一项卡住,先补那一项再谈别的。
蓝曜炬辉在智能体交付项目里,把"可取证"和"能跑通"放在同一张验收清单上:run_id 贯穿日志与 trace、工具调用入参出参原文、stderr 分块元数据、压缩边界保留原始引用。如果你正准备把一个 agent 推进生产,或者刚经历一次说不清的事故,欢迎联系我们聊聊你的日志设计。