← 返回所有文章
发布于 · 作者 Renaud Deraison

令牌就在同一个堆里

Accomplish 的 Oren Yomtov 两度逃出 OpenAI 的 Codex 沙箱。其中一次是从最严格的 read-only 模式里逃出来的:不受信任的 JavaScript 调用了 v8.getHeapSnapshot(),从两者共用的那个堆里,把受信任上下文的授权令牌读了出来。另一次则让 apply_patch 自行把写入权限扩张到文件系统的根目录,接着在 .zshrc 后面加上一行。OpenAI 八天内修好了两个。但催生它们的那个性质没有补丁可打:守卫与它所守卫的代码,是同一个进程里的两个变量。Bromure Agentic Coding 让守卫跑在你的 Mac 上,让智能体跑在虚拟机里。

不受信任的那一侧从来没有攻破沙箱。它只是向 Node 要了一份堆的快照, 发现沙箱自己的授权令牌以一个再普通不过的字符串躺在那里,然后用了它。

你把 Codex 指向一个别人发给你的仓库。你很谨慎,所以用 read-only 跑它, 那是智能体能看不能碰的模式。智能体读了那些代码。其中一部分是载荷, 几秒之后,一条命令就在你的 Mac 上跑了起来,在沙箱之外,屏幕上没有任何提示。

那就是 Heapjack, Accomplish 的 Oren Yomtov 这个月公开的 两个 Codex 沙箱逃逸之一。 BleepingComputer 在 9 月 20 日做了报道。 Yomtov 在 8 月 12 日把两个都报告给 OpenAI,OpenAI 在八天内修好:Heapjack 修在 Codex Desktop build 26.818.21641,另一个修在 Codex CLI 0.149.0。OpenAI 的声明 属于简短的那一类:“我们感谢研究人员与我们联系并分享他们的发现。我们在八月处理了 这两个问题,但我们会持续不断地加固我们的沙箱。”

第一个缺陷为什么会存在,那才是比补丁活得更久的部分。

一个进程,一个堆,一个令牌

Codex Desktop 有个叫 node_repl 的工具,用来执行 JavaScript。它在里面保有两个 上下文:一个受信任,获准请求父进程去做真正的事;一个不受信任,执行智能体 当下正在求值的任何东西。这两者之间的隔阂,就是沙箱。

两个上下文活在同一个 Node 进程里。用 Yomtov 的话说:“一个 Node 进程,一个 V8 isolate,一个被两个上下文共用的堆。令牌就是那个堆里的一个字符串。”

令牌就是父进程用来分辨两者的办法。带着正确令牌从管道抵达的请求就是受信任的, 会被执行;没带的就不是。它是一个随机 UUID,只要你要瞒着的那段代码读不到你的 内存,它就是个相当不错的秘密。

它读得到。v8.getHeapSnapshot() 是 Node 的公开 API,一个剖析用的工具,它会返回 堆上每一个对象的转储,字符串也在内。不受信任的上下文调用它,滤出形状像 UUID 的东西,最后得到一小把候选。要从中挑出真的那个还得多一步:拿每一个去向父进程 试,看回来的是什么。错的令牌回你“未经授权”。对的那个,回你一句对请求其余部分 的抱怨。Yomtov 留下了产生第二种回复的那个字符串。

令牌到手之后,不受信任的上下文就把请求写进受信任那一侧在用的同一条 stdout 管道。父进程核对令牌,认定有效,然后在沙箱外执行那条命令。从那里开始:通过 open 下系统命令、Docker socket、修改配置文件。

这一切都是从 read-only 出发的。再引一次 Yomtov:“这一切都跑在 read-only 里,最严格的沙箱模式,智能体理应什么都写不了的地方。”

边界与它所界定之物,同处一个地址空间一个 Node 进程,一个 V8 堆受信任的上下文可请求父进程执行持有令牌不受信任的上下文运行仓库里的代码这就是沙箱两者共用的堆“a1f3…-…-…”授权令牌,只是字符串v8.getHeapSnapshot()不受信任的一侧做什么1. 快照堆,留下 UUID 形状字符串2. “未授权”与验证错误之别3. 在共用 stdout 管道伪造请求父进程令牌有效 → 在沙箱外执行在 Mac 上执行命令来自 read-only,且无提示
Heapjack 不是攻破沙箱,而是读走沙箱的钥匙。受信任的上下文、不受信任的上下文,以及用来区分两者的授权令牌,全都活在同一个 Node 进程内的同一个 V8 堆里。v8.getHeapSnapshot() 是公开的剖析 API,会返回那个堆上的每一个字符串。不受信任的上下文收集形状像 UUID 的候选,靠着『未经授权』的回复与验证错误之间的差别认出真正的令牌,然后在共用的 stdout 管道上伪造出受信任的请求。父进程验证令牌,在沙箱之外、在理应什么都不可写的 read-only 模式下,执行那条命令。

一个会长大的写入权限

第二个逃逸 Overpatch 就朴素多了。Codex CLI 有个编辑文件的 apply_patch 工具, 它会从交给它的补丁里所指名的路径,推算自己需要多少写入权限。那些路径来自 模型,而模型正在读那个仓库。

于是一份补丁在某个条目里指名了 /tmp,附带一个什么也不做的变更。变更失败了。 但它所隐含的权限授予并没有失败:写入权限一路扩张到文件系统的根目录。同一份 补丁里的第二个条目,接着就通过一个符号链接写进 $HOME,在 .zshrc 后面 加上一行。

到这里还没有任何东西逃出去。逃逸发生在后来,在你打开终端、你的 shell 以你的 身份执行那一行的时候,那条路径上没有任何沙箱。这一个跑的是 workspace-write, 日常的那个模式。

两个不同的缺陷,两个不同的产品,文章里有一句话同时涵盖了它们:“负责执行规范的 那个东西,就坐在被规范的那个东西里面。”

守卫与被守卫者,同处一个地址空间

一个与它所约束的代码活在同一个进程里的沙箱,是一种约定。它撑得住,直到有人 认真地盯着它看。令牌在堆里。权限的计算读的是攻击者提供的路径。管道是同一条 管道。这其中每一项都可以修,OpenAI 也都在八天内修好了。下一个这种类型的缺陷, 会是同一个堆里的另一个字符串。

Yomtov 自己的结论则是架构层面的:“这正是 Accomplish 把整个智能体跑在虚拟机 里的原因……我们把客户机里的一切都当成不受信任的,含 root 在内,因为决定什么被 允许的东西,没有一样跟着跑在里面。”

那正是 Bromure Agentic Coding 交付的设计。智能体跑在一个 Ubuntu 24.04 客户机 里,底下是 Apple 的 Virtualization.framework,macOS 自己就提供的那个 hypervisor。客户机在构造上就不受信任,含 root 在内。决定什么被允许的代码, 以主机进程的身份跑在一条 vsock 的另一侧,而客户机内部没有任何令牌能授权任何 事情,因为值得授权的一切都坐在客户机之外。

进程内的沙箱一个操作系统,一个用户账号智能体读着他人的仓库策略同一个堆里的令牌~/.ssh ~/.aws ~/.zshrc 钥匙串 每个仓库从内存读到的一个字符串隔离就此结束,上面的一切都在可及范围在 hypervisor 上的边界Linux 客户机,不受信任,含 root 在内智能体、仓库、自己的 $HOMEghp_… sk-ant-… 的诱饵Virtualization.framework + vsockMac,决定在此做出代理服务器真钥匙、出口规则,出去路上的 451对话框“允许写入 …?”在虚拟机之外绘制
要紧的是由哪一台机器来评估规则。左边:守卫、被守卫的代码、授权令牌,以及你的 SSH 密钥、云凭据与 .zshrc,全都坐在同一个操作系统里,于是读走一个字符串,隔离就结束了。右边:智能体拿到一台 Linux 虚拟机,它的凭据是保留结构的诱饵,它在端口 80 与 443 上的流量会被虚拟交换机转进一个客户机无法关掉的主机代理服务器,而它最坏的结局是一块你抹掉的磁盘。客户机被完全攻陷,含 root 在内,攻击者拿到的就是那个客户机。

每一种逃逸换成在这里会遇到什么

Overpatch 比较具体,就从它开始。这个手法的结尾,是往 $HOME 里的 .zshrc 追加内容。在 Bromure 的工作区里,客户机的 $HOME 是 home.img,一个挂在那台 虚拟机上的 ext4 镜像。你 Mac 的家目录不在里面。客户机在你 Mac 上够得到的,是你 共享进去的那些项目文件夹:最多八个,由你挑选,列在工作区编辑器里。你打开终端时 shell 会读的那些 dotfiles 不在其中,除非你自己把它们放进去。

Heapjack 是比较难的那一个。Bromure 里没有任何东西会检测客户机内部的堆快照 手法,也不打算检测。改变的是逃逸抵达的地方。在 Codex 里,逃出来的代码落在 那个握着你 SSH 密钥与云凭据的账号里。在 Bromure 的工作区里,它落进一台这样的 Linux 虚拟机:

凭据都是诱饵

sk-ant-api03-brm-…、一个形状像 ghp_ 的字符串、一个 secret 无效的 ~/.aws 配置文件。它们保留结构,所以工具会接受,但在上游一文不值。真正的 值活在主机上代理服务器的内存里,在出去的路上被换上,范围限定在它们所属的 那台主机或其子域名。

把它带出去就会触线

代理服务器会扫描每一个外发请求的头部与正文,找的就是这个工作区自己的那些 诱饵。一个诱饵若前往它不是为之铸造的地方,就会被 HTTP 451 挡下,一个字节 都不会转发。虚拟机暂停,一行红色的 凭据中介 落进安全时间线。

网络是一份你写的清单

出口是一组有顺序的规则集,结尾是 default allow 或 default deny。虚拟 交换机把虚拟机在端口 80 与 443 上的流量转进主机的代理服务器,其中没有任何 环境变量介入,也没有任何客户机关得掉的东西。编辑会立即对运行中的会话生效。

push 是一个问题,在 Mac 上问

Git 托管平台的凭据带着一份写入策略,而新工作区的默认是 写入前先询问。 git push 就是写入。对话框会指名那个确切的操作,并绘制在你的桌面上。虚拟机 里的攻击者点不到它。

真出事的时候,你靠一个菜单项就能恢复。入侵处理流程会抹掉工作区的磁盘与家 目录,并保留你的设置、令牌与 SSH 密钥。真正的凭据从来没离开过你的 Mac,所以 你不用轮换任何东西。重置磁盘 会重新克隆一份系统镜像。Bromure 的任何动作 都不会碰你共享出去的项目文件夹,提示上也是这么写的。

最严格的那个模式,才是输掉的那个

这个故事里的开发者什么都做对了。他们刻意选了那个工具所提供最严格的设置,因为 那个仓库不是他们的。read-only 是一个真实的功能,也做了真实的工作,直到 共用堆里的一个字符串,把它变成了一句建议。

模式、允许清单与批准提示都值得存在,而它们每一个都预设了:执行这些规范的代 码,坐在它所限制的代码够不到的地方。那是一个关于内存布局的主张,而你可以 去查证它。Yomtov 查了 Codex 两次,横跨两个产品,两次得到的答案都是否定的。

所以,把智能体跑在那个答案不再要紧的地方。给它 root,让它爱装什么就装什么, 让它去读那个别人发给你的仓库,然后把每一份凭据、每一个目的地的决定、每一次 push 的确认,都留在一层 hypervisor 的另一侧——下一次逃逸抵达的,会是一台你 本来就愿意丢掉的机器。