发现智能体被攻陷:破绽在轨迹的形状里,不在文本里。
你买的每一个控制项都在检查内容——入口处的注入分类器、出口处的输出扫描器——而它们每一个都会「失败即放行」,这意味着有一次绕过它们的那天,正是你完全没有检测能力的那天。挺过这一点的是行为层面的东西:被攻陷的智能体,其工具调用序列会不再像该任务类型通常产生的那些序列,而且它通常会在这个过程中丢下用户的任务。麻烦在于,一个健康的智能体按普通 SOC 的标准衡量本来就极度异常,所以通用的用户行为基线只会产出噪声。按任务类型建基线、对轨迹形状告警,你就得到了一个在分类器漏检之后仍然有效的控制项。
内容检查属于预防,而预防不带任何检测故事。
标准配置是:一个给检索到的文本打注入分数的输入分类器,加一个寻找外泄模式的输出过滤器。两者都值得有。两者都不会告诉你某个智能体已经被攻陷了,原因是结构性的,而不是模型质量问题。
- 分类器的漏检是静默的。召回率 98% 的检测器是个好检测器,可它仍会每五十次尝试放过一次,并且不产生任何事件。这里没有第二个信号,因为「这是不是一次攻击」和「刚才有没有发生攻击」是由同一个组件裁定的。
- 事后来看,那条指令与上下文无从区分。被注入的文本一旦进了对话记录,它就只是模型读到的一些令牌,而其后每一步看上去都像模型在对自己的上下文做推理——事实也正是如此。到后面的步骤里去扫那条注入什么也找不到,因为注入早已生效,也没有任何理由继续存在。
- 输出过滤器位于那些真正有意思的外泄路径的下游。一个由客户端去取的 markdown 图片 URL,根本不会以文本形式经过你的文本过滤器;参见安全地渲染智能体输出。
有用的表述是:内容类控制降低被攻陷的频率,行为类控制降低被攻陷的时长。这两个数你都得有。大多数团队只有前者,还把它的召回率当成覆盖度在报。
你的智能体天生就是异常的,所以通用基线一文不值。
把一款现成的用户行为分析产品对准一个恰好是智能体的服务账号,它会永久性地亮红灯。这个智能体每小时读几百份文档、连着调十几个 API、凌晨三点在干活,而且它的量会随着被派的任务变动一个数量级。上面每一条都是教科书级的内部威胁指标,而上面每一条又都是正确行为。团队的反应是不断放宽阈值直到告警不再出现——到那一刻,这个控制项就已经关掉了。
解法是换掉建基线的单位。真正有区分度的比较不是这个身份与它自己的历史相比,而是这次运行与同一任务类型的其他运行相比。一次 summarise-ticket 的运行有其特征形状:大致这么多步、这些工具大致按这个顺序、读取面这么宽、出站到这些主机。这个分布是紧的,因为任务是固定的;而一次跑出这个分布的运行是值得看的,哪怕该身份的滚动均值一切正常。
- 在派发时就给每次运行打上任务类型标签。做不到这一点,你就没有分母,下面的一切都不成立。
- 基线要用生产环境中成功的运行来建,按滚动窗口刷新,而不是用一份「智能体应该做什么」的设计文档来建。
- 当各租户的数据形状差异大到有影响时,就按租户分开维护各任务类型的基线;否则「正常」的定义会由你最大的那个客户来写。
五个真正有区分度的信号,按性价比排序。
这里按实战中的信噪比排序,而不是按听起来有多高级。第一个几乎是免费的,并且能抓住成功注入中的大多数——因为一个被劫持的智能体,必须把它剩下的步数花在攻击者的目标上,而不是用户的目标上。
- 任务遗弃。拿这次运行的终态去比对它被派发时的任务:它有没有产出被要求的那件产物?被注入的智能体通常会返回一个看着像模像样的非答案,或者在一件相邻的事情上取得了成功。计算便宜,而且攻击者出奇地难以规避——把用户的任务也完成掉,要额外花掉攻击者的步数和上下文。
- 工具序列新颖度。把本次运行的工具名序列,对照该任务类型上观测到的 n-gram 打分。一个
read → read → summarise的任务里突然出现read → list_keys → http_post,就会被打成「未见过」。工具名上的二元组就能抓住其中大部分;你不需要一个序列模型。 - 出站目的地新颖度。首次接触某台主机、首次使用某个能通向外部的工具、出站字节与入站字节的比例反常。这是严重度最高、同时量最低的信号,它该紧挨着出站管控放——允许清单负责拦,检测器负责记录谁试过。
- 读取面膨胀。相对该任务类型的分布,本次触及的文档、行或代码仓的数量与多样性。外泄尝试之前那种分阶段的数据收集,最先就出现在这里;一个检索权限缺陷也是这样自报家门的。
- 授权摩擦。被拒的调用、作用域错误、被拒绝的工具、对同一个禁止端点的反复重试。一个健康的智能体很少为同一件事被拒绝两次;而在试探边界的东西会。把拒绝本身当作独立事件记下来——大多数技术栈在策略层就把它们丢掉了。
如果这里只做一个,就做任务遗弃。它不需要调度器已知信息之外的任何新遥测,触发时是可解释的,而且它是攻击者为了压制而付出代价最大的那个信号。
循环不发出正确的字段,上面的一切都算不出来。
大多数智能体链路追踪是为调试而建的:提示词、补全、延迟。检测需要的是同一次运行的另一种投影——一条紧凑的、逐步骤的记录,其字段可以跨运行比较。要从工具调用路径内部发出它,而不是从一个会被智能体自身错误跳过的外层包装里发出。
# detect/step_record.py — emitted on every tool call, before dispatch def record(run, step, tool, args, result): emit({ "run_id": run.id, "task_type": run.task_type, # the baseline key "principal": run.on_behalf_of, # human, not service account "step": step, "tool": tool, # name only — the n-gram unit "dest": egress_host(args), # None for local tools "read_ids": hashed(result.ids), # breadth, never content "bytes_out": size(args), "denied": result.denied, # keep policy refusals "ctx_source": args.provenance, # which retrieved doc drove this })
有两个字段的价值远超其体量。principal 保留了这次运行是替哪个人在行动,于是一次攻陷可以按受影响的人来划定范围,而不是按大家共用的那一个服务账号——这正是智能体身份里的论点。ctx_source 保留了这一步之前那段文本的来源,正是它把「这次运行跑偏了」变成「这份文档会策反智能体」,而后者才是保护其余所有租户的那条发现。请对标识符做哈希而不要存内容;检测器要的是宽度和形状,而把文档本身留下来,会把你的检测存储变成全网最诱人的目标。留存策略遵循链路采样与留存——并请注意,在这里按比例采样运行恰恰是错的,因为你丢掉的那次运行正是你需要的那次。为控成本可以对 span 采样,但逐步骤记录要对每一次运行都保留。
只会开工单的检测器,什么时长也没缩短。
检测在被接到某个跑得过人的动作上之前,什么也换不来。智能体在几秒内就走完剩下的步骤;一条十五分钟后到达的呼叫,是在运行结束之后到的。搭一个阶梯,并把便宜的那几级放进循环本身。
- 降级。分数中等时,把这次运行降到只读工具并让它跑完。大多数误报止步于此,代价是一次被降级的运行——正是这一点让激进的阈值负担得起。
- 设关。出现出站类或授权类信号时,要求下一次改变状态的调用必须经过审批。如果你已经有人在环,审批界面就已经存在;这里是把流量改道进去,而不是新建一套。
- 叫停。分数高时,停掉这次运行并做检查点。这就是运行粒度上的终止开关,而它必须能由检测器直接调用,路径上不夹人。
- 隔离来源。当
ctx_source指向某份具体文档时,在复核完成前对所有运行封禁它。一个被投毒的页面会触达每一个检索到它的智能体,所以遏制的单位是文档,不是运行。
阈值应当按每一级上误报的代价来定,而不是按一个固定分位数。降级承受得起在百分之几的运行上触发;叫停承受不起。把确认的命中转入事件响应,并预期第一次清理是不彻底的——在工具层关掉的一条通道,往往会在某个不那么显眼的地方被重新打开。
要么测出平均发现时间,要么你只是在猜。
一个从未被演练过的检测器,只是一种信念。测量很便宜:埋无害的哨兵,然后数步数,不是数分钟。
- 把一条哨兵指令放进智能体会检索到的文档里——让它去调用一个你自己掌控的、无害的内部端点。每周在预发布租户里、按生产配置跑一次。你的指标是:从智能体读到它,到检测器触发,中间隔了多少步。
- 记录是在哪一级被抓住的。是哪一级拦下的,还是这次运行没被抓住就跑完了。「在叫停这级发现」和「运行结束之后才发现」是两种不同的产品。
- 换着花样做载荷。一个做外泄、一个悄悄丢下任务、一个抬升作用域。每一种演练的是不同的信号,而「任务遗弃」这一种,是大多数检测器会栽的那一种。
- 两个数都要报。来自分类器的被攻陷频率,来自行为栈的持续时长。一个只报前者的项目,对「漏检之后会发生什么」毫无证据。
这个季度按顺序做这三件事:给每次运行打上任务类型标签、拿终态去比对派发的任务、把策略拒绝当事件记下来。这大约是一天的工作量,却能把你的智能体集群从「不可观测」变成「有基线」。工具序列打分和出站新颖度值得接着建,而这两者都要靠任务类型标签才有意义——所以要先落地的依赖是那个标签,不是模型。
延伸:提示词注入防御讲预防的那一半,数据外泄风险讲出站信号在保护什么,失败分类与分诊讲怎样把「被攻陷」与普通失败区分开,决策回执讲你事后解释经过时会想要的那份记录。