气隙环境中的安全工具调用
第一个警报是一个 DNS 数据包。仅有一个,从一个本应隔离的实验室发出,指向一个看起来像 base32 标识符的域名。操作员当时正在用一个本地代理对其进行审计,该代理基于由 Ollama 提供服务的 Qwen 2.5 7B,按理说除了实验室内部网段之外没有任何网络访问权限。这个数据包之所以出去,是因为代理可以调用的某个工具叫 resolve_target,而模型在面对一个通过被我们 RAG 索引的 SMB banner 传入的中毒 prompt 时,决定下一步合乎逻辑的动作就是去解析攻击者特意塞进 banner 中的一个 FQDN。
这不是 APT,而是我们对自己做的一次概念验证,它帮助我们搞清楚了一件琢磨了好几个月的事:工具调用就是新的攻击面,而在气隙环境中,这个攻击面给人的安全感比它实际上要强得多。剧透:它并不安全。
当你给模型一双手时会发生什么
工具调用改变了一切。我们从只会吐文本的模型,走到了能执行 nmap、读文件、查询向量,并且一不小心就会发起出站请求的代理。Yao 等人在关于通过函数调用越狱的研究中描述得很到位,他们报告了对 GPT-4o、Claude 3.5 Sonnet 和 Gemini 1.5 Pro 直接通过函数机制本身发起攻击的成功率超过 90%(Wu et al., 2024)[1]。模型与用户对齐,但未必与你提供给它的工具对齐。
问题是结构性的。当 LLM 决定调用哪个工具时,它正在消费上下文。而在渗透测试中,那段上下文按定义就是不可信文本:HTTP 响应、banner、获取到的文件、扫描器输出。Wang 等人在《From Allies to Adversaries》中将其形式化,展示了攻击者如何注入操纵者工具或污染响应,从而迫使代理调用它本不该调用的函数(Wang et al., 2024)[2]。「数据」和「指令」之间的界线被模糊化,而模型没有能够区分二者的语义解析器,就这样把毒吞了下去。
气隙的神话
很多人以为只要模型本地运行在 Ollama 上,没有互联网出口,问题就消失了。它不会消失,只会变形。我们在自己的实验室中看到三种反复出现的模式:
- 侧信道外泄:内部 DNS、ARP、稍后会同步的日志、被某人捡走的临时文件。
- 内部横向移动:代理合法地拥有客户网络的访问权限;攻击者把它当作代理使用。
- RAG 自我中毒:代理保存的观察结果会被未来的代理当作权威上下文读取。
最后这一点特别难受。Shi 等人关于 Log-To-Leak 的工作精确描述了这种向量:代理调用一个看似无害的日志记录工具,最终把查询、响应和内部状态全部泄漏出去(Shi et al., 2025)[3]。而由于日志位于周界之内,传统 SIEM 看不出任何异常。
我们在实践中如何应对
跟你说实话:没有银弹。我们手里有的是一系列分层,每一层都假定前一层可能失败。在我们的内部技术栈中,我们把三类职责拆开,因为把它们塞进同一个二进制就是自找麻烦。
一方面,Gandalf 充当网关。它是唯一与模型和操作员对话的组件。它内含一个我们称之为 Sentinel 的部分,会在每次工具调用前后应用策略:验证参数是否落在授权的 CIDR 范围内、命令是否匹配任何外泄签名、代理是否试图越出花园。一旦感觉不对,kill-switch 就会切断会话、丢弃上下文并发出告警。
另一方面,Gwaihir 是执行器。当一次调用通过 Sentinel 后,它会作为一个子进程实例化,并附带一个 seccomp-bpf 过滤器,仅允许该特定工具所需的 syscall 子集。不允许任意的 connect(),也不允许 execve() 到 allowlist 之外的二进制。这直接受到了 Wei 等人在《Securing AI Agent Execution》中提出的方案启发,他们主张执行器应当被动态供给,只赋予当前计划步骤所需的权限(Wei et al., 2025)[4]。最小权限原则不是按角色,而是按 syscall 应用。
再然后是 Beorn,它维护着我们当作运营知识库使用的 HTB 向量 RAG(目前大约 9115 条)。Beorn 从不直接接收模型的输入;查询都经由 Gandalf 标准化处理。从代理的视角看,RAG 是只读的,这就消灭了我之前提到的自我中毒向量。
下面是一个针对扫描工具的 Sentinel 策略样例:
{
"tool": "nmap_scan",
"scope_cidr": ["10.10.11.0/24"],
"deny_flags": ["-oN", "--script=http-fetch", "-iL"],
"max_runtime_s": 120,
"seccomp_profile": "gwaihir/profiles/nmap.json",
"kill_switch": {
"on_outbound_dns": true,
"on_unexpected_egress": true,
"on_token_budget_exceeded": 4096
}
}正是 on_outbound_dns 抓到了 SMB banner 那起事件。任何不落在实验室内部区域的解析请求,都会在数据包离开网口之前触发 kill-switch。
本地模型:为什么是 Qwen、Llama 和 Phi
面对托管模型进行运营,根本上与气隙就不兼容。但还有一个更微妙的原因:商用模型的工具调用是按固定 schema 训练的,一旦偏离这个 schema,它们就倾向于幻觉出各种参数。借助由 Ollama 提供服务的 Qwen 2.5、Llama 3.1 和 Phi-4,我们可以用 GBNF 文法强制结构化输出,从而大幅压缩「创意参数」的表面积。不完美,但可审计。
权衡是真实存在的。你会损失原始能力:一个 Qwen 7B 无法像 Claude Opus 那样在五步级联工具调用中进行推理。你通过把计划拆成更小的步骤,并让 Sentinel 对每一步单独验证来弥补。作为交换,你获得了完整的可追溯性、可预测的延迟(token 以毫秒计费,而不是美元),以及在必要时逐位审计模型的可能。
隔离之后会有什么破损
并非全是好事。把一切隔离之后,我们这边有三件事就破了:
第一,新兴威胁的可见性。没有任何遥测离开实验室,你就无法实时与 CTI feed 做关联。我们用一条非对称通道解决:实验室不向外通信,但实验室外的一个进程每 X 分钟可以从内部 bucket 拉取数据进行富化。
第二,RAG 更新。我们 Beorn 中那 9115 条 HTB 向量不会自动更新。必须在外部重建索引,然后再把签名快照推回去。运营上确实有摩擦,但是可预期的。
第三,操作员的 UX。习惯了与一个大模型聊天,操作员有时会觉得 Qwen「比较笨」。从某种意义上说,它确实如此。但一个在 seccomp-bpf、CIDR 范围和 kill-switch 包裹下执行的笨模型,远比一个能自由访问 syscall 的聪明模型要安全得多。
关键收获
如果只能从这篇文章带走一件事,那就是:气隙不是一种属性,而是一种架构。而在这个架构里,工具调用是信任最先崩塌的地方。按下面这个顺序,从最便宜、最有效的做起:
- 按会话定义显式 CIDR 范围,而不是按代理。用 Sentinel 之类的组件在每次调用前进行校验。
- 把每一次工具执行都放在带 syscall allowlist 的 seccomp-bpf 过滤器后面。如果没有 Gwaihir,可以看看 bubblewrap、gVisor 或 nsjail。
- 实现一个对意料之外的 egress 做出反应的 kill-switch,而不仅仅是对可疑命令。模型会在参数上让你意外,而不是在工具名字上。
- 把 RAG 上下文当作不可信数据。是的,包括你昨天自己塞进去的那部分。
那个 DNS 数据包并没有真正抵达任何地方。但 Sentinel 的那条日志至今仍躺在我们的事后分析里,提醒我们:一个没有互联网、心怀好意的本地代理,仍可能去试图与一个虚构域名对话,只因为半年前有人把它写进了某个 banner。这才是真实的运营现实。其余的,都是表演。
对模型的信任,仅限于让它完成工作。对它的上下文,永远保持怀疑。
参考文献
- Wu, Z. et al. (2024). The Dark Side of Function Calling: Pathways to Jailbreaking Large Language Models. arXiv:2407.17915.
- Wang, H. et al. (2024). From Allies to Adversaries: Manipulating LLM Tool-Calling through Adversarial Injection. arXiv:2412.10198.
- Shi, Y. et al. (2025). Log-To-Leak: Prompt Injection Attacks on Tool-Using LLM Agents via Model Context Protocol. OpenReview.
- Wei, J. et al. (2025). Securing AI Agent Execution. arXiv:2510.21236.
- Patel, R. et al. (2025). Architecting Resilient LLM Agents: A Guide to Secure Plan-and-Execute Patterns. arXiv:2509.08646.