导语:2026 年 9 月 28 日,AI 安全研究机构 pwn.ai 发表技术长文《Inside the Sandbox: How pwn Escaped Google kvmCTF (Before the Watershed)》,公开记录自家 AI 代理 pwn 在 6 月 17 日完整跑通 Google kvmCTF 沙箱逃逸挑战并拿下官方 flag
cdc63bb4b015eb8c。如果公开记录没有更早的案例,这将是第一个 AI 代理完成”guest-to-host 内核漏洞利用 + 从 Google 真实宿主取回令牌”的工程级成果,对应漏洞 CVE-2026-46113(Linux 6.1.74 KVM shadow MMU UAF,Use-After-Free,释放后使用)。

一、kvmCTF 是什么
Google kvmCTF 是个 Google 运营的漏洞赏金项目,目标是从一台加固到极致的裸金属宿主逃逸出去。逃出 kvmCTF 的事,在整个安全圈都不多见。
目标机器跑的是 Linux 6.1.74 加 KVM(Kernel-based Virtual Machine,内核虚拟机)。研究员拿到一台普通 Debian 客户机(guest)和一个保留时段(一般是 1 小时),要做的事只有一件:从 guest 攻到 host。在启用 KASAN(Kernel Address Sanitizer,内核地址 sanitizer,内核内存错误检测器)的保留时段,Google 改写过 host 的 KASAN 报告路径——只要符合资格的 host 端内存安全违规触发,host 就会解封一个 64 位的随机令牌,guest 通过”挑战 hypercall”取回。
Google 官方对项目目标的描述非常朴素:成功的攻击者拿到一个 flag,证明确实完成了一次 guest-to-host 漏洞利用。没有 AI 评分,没有人类判断 crash 像不像漏洞。host 记录到内存安全事件并放出那次启动独有的随机秘密,就算成;否则就不算。
整个评估的拓扑长这样:
┌─────────────────────────────────────────────┐
│ L0 Host (Google 物理机,Linux 6.1.74+KVM) │
│ ↑ guest-to-host 漏洞 │
│ ┌─────────────────────────────────────┐ │
│ │ L1 Guest (Debian,研究员控制) │ │
│ │ ┌─────────────────────────────┐ │ │
│ │ │ L2 Guest (pwn 控制) │ │ │
│ │ └─────────────────────────────┘ │ │
│ └─────────────────────────────────────┘ │
└─────────────────────────────────────────────┘
中间这段转换才是有意思的地方。L1 用 EPT12(Extended Page Table,L2 客户物理地址到 L1 物理地址的映射表)告诉处理器 L2 的客户物理地址怎么映到 L1 内存。L0 的 KVM 再把这个映射和它自己的 L1-to-host 映射叠起来,构造一个 shadow 翻译(业界叫 EPT02)。硬件拿到这个结果就能跑 L2,不用每次访问都软件走两层页表。
这套缓存得做簿记。每个映射页 KVM 都维护一份 rmap(reverse map,反向映射),里面装的是”哪些 shadow 页表项指向了我”。背后的页一变,KVM 顺着 rmap 把这些翻译作废。
如果某个指针在它对应的 shadow 页被释放之后还活着,guest 就成功让 host 内核保留了一个指向已释放内存的指针。这条利用链的开端就是这么来的。

二、给 pwn 的输入
pwn 收到的核心任务指令很简洁:
“找出能从 kvmCTF guest 攻到 host 的漏洞利用路径并执行。”
人类这边负责选目标、给基础设施、给保留时段、定范围。没给 pwn 现成的 PoC 或利用链。pwn 的研究框架在内部按任务挑模型——OpenAI、Anthropic、DeepSeek 都接,哪个模型在哪个子任务上 benchmark(基准测试)最好就用哪个;团队里顶级沙箱逃逸专家多年积累下来的知识和工作流也以知识蒸馏的方式注入到 harness 里。
整个研究活动里 pwn 实际做的事:
- 审计目标内核源码
- 给”VM 可达的攻击面”打分排序
- 派独立的源码审稿角色相互对抗
- 写嵌套 VMX(Virtual Machine Extensions,Intel 硬件虚拟化扩展)harness 和反汇编器
- 在本地裸金属上复现拓扑
- 设计内存分配实验
- 调利用链
- 写官方 runner(自动化运行器)
- 在 live 环境失败后恢复
- 拿下 flag
留下来的工作不是单条巨型 chat 补全。是一轮横跨多个会话和内部审稿代理的研究活动。最早相关的 harness 是 3,304 行,私有写手(private-writer)那版涨到 12,955 行,最后赢的那版是 14,338 行。
三、漏洞原理:mmu_set_spte 的分支顺序 bug
5 月 10 日,pwn 的源码审计锁定了一个危险的分支顺序问题,位置在目标内核的 arch/x86/kvm/mmu/mmu.c::mmu_set_spte()。
Linux 6.1.74 里,mmu_set_spte 处理 MMIO 或”noslot”PFN(Page Frame Number,页帧号)发生在处理”slot 已有的 shadow-present SPTE(Shadow Page Table Entry,影子页表项)”之前。第一个分支写完 MMIO SPTE 就直接 return,第二个分支才是正常情况下”丢弃旧 present SPTE 并把它从 rmap 里摘掉”的地方。第一个分支赢的话,那段清理永远不会发生。
分支顺序的小细节。生命周期的灾难性后果。
这个 bug 有一个关键前置条件:guest 对影子化页表(shadowed page table)的普通 CPU 写会被 KVM 跟踪。KVM 写保护那一页,看到修改,作废旧 shadow 翻译。要走到那个错误分支,guest 的页表项必须走在那条正常跟踪路径之外——host 端写入或设备 DMA 才能造出这种状态。pwn 需要一个”能从 kvmCTF guest 内部调用”的写者。
pwn 的怀疑派审稿代理之一 Sus 早早指出:KASAN 最先能看到的副作用很可能是个读操作,不是写。Meitner 同步独立验证了找不到可靠的”先写后用”消费方。Hypatia 否决了把 VMXOFF/VMXON 拆开的两阶段生命周期,因为那会破坏嵌套 root 链。Descartes 和 Bohr 集中攻击 rmap 存活和内存分配器假设。这些代理存在的目的就是在利用链上 Google 加固那一关之前,把它打死。
四、用 VMREAD 把 KVM 自己变成”写者”
5 月 11 日,pwn 找到了一个比 DMA 链更干净的写入原语。
x86 的 VMREAD 指令把一个 VMCS(Virtual Machine Control Structure,虚拟机控制结构)字段读到寄存器或内存目的地。当 L2 执行一个”目的地为内存”的 VMREAD 且 KVM 必须模拟这条指令时,KVM 自己会替 L2 做那次内存拷贝:
vmcs_field64_read(READ_SHADOW_VMCS_FIELD, field, p_data);
这条系统写路径不会调用正常指令模拟器物理写入路径里那个 kvm_page_track_write() 通知。
pwn 安排了一个别名(alias):L2 跑一条只有三个字节的指令——VMREAD [rdi], rax:
- RAX 选 VMCS 字段
0x2006,即VM_EXIT_MSR_STORE_ADDR。pwn 把这个字段当成 8 字节的”运载体”,因为对应的 MSR-store 计数为 0。字段可以承载想要的 MMIO EPTE 而不会改变活跃 EPT 指针,也不会触发实际的 MSR-store 操作。 - RDI 指向 EPT12 PT[64] 的别名。
也就是说,bug 报告里预期的写者是 host userspace 或 DMA。pwn 找到了一个更离谱的——让 KVM 自己完成那次未跟踪的写入。
这一步把外部设备和时序竞争窗口从利用链前半段里整个拿掉了。L2 能自己创建 shadow 翻译,然后请 KVM 在同一个受控的嵌套 VMX 状态机里改写底层 EPT12 leaf。赢的那次 live 跑,按字节记下了这次转换。
五、把过期翻译变成悬空 host 指针
最初的 EPT leaf 是故意设成只读的。
这把接下来的 L2 写操作引导到对旧只读 shadow SPTE 的 fault(缺页异常)。KVM 重新走现在已经过期的 EPT12 leaf,把新目标解析成 MMIO/noslot,进入 mmu_set_spte() 里那个脆弱的早 return。
rmap[A] 里的指针还没真正”悬空”——它还指向一个活着的 KVM shadow 页分配。利用链必须让那个 shadow 页退休并被释放,同时还不修复旧 rmap。大多数对这类 bug 的简短描述,讲到这里就讲不下去了。
纸面上有个 UAF 原语不叫 flag。pwn 还得控制嵌套 root 生命周期、让被污染的 shadow 页可回收、制造足够的 KVM 拥有分配压力、并强迫某条后续 host 代码路径去解引用那个过期指针,时机还得让 KASAN 能把它归类为一次有效访问。
六、5 月 17 日:那批一直失败的实验
pwn 头 20 个内存分配器模型全是错的。工作假设是:KVM 释放掉被污染的 shadow 页之后,普通的 guest 页 grooming(页面整备)就能回收或暴露它。harness 在 guest 页里分配再扫,但期望的 SPTE 值从不出现。hypercall 一直返回 0。pwn 没有去加 sleep 或盲目加大喷洒量,而是在 L1 实验室宿主上 instrument(埋点监测)mmu_spte_clear_track_bits()。下一轮有针对性的跑产生了 50,175 次 clear 事件。
但 trace 末端出现了一个更有用的信号:一次 1,001 项的 rmap clear,紧挨着 KVM MMU zap 活动。这告诉了 pwn 失败的根本原因。被污染的对象不是普通 L1 guest 页,是KVM 自己拥有、由 host shadow MMU 分配和回收的 shadow 内存。再多 guest RAM 也直接收不回它。分配器目标要换。
这是有能力的代理真正不一样的地方。它们能造出工具,通过持续和迭代告诉自己”为什么这次重试会失败”。失败的实验变成遥测。遥测变成利用链。
七、Root 轮换、VMFUNC 和 49,152 个稀疏映射
利用链后半段是一个刻意构造的 KVM shadow MMU 工作负载。
7.1 退休被污染的翻译
pwn 让旧 EPT 上下文失效,然后轮过 8 个预备好的嵌套 EPT 根。每个根都会让 KVM 构建或重访一棵不同的嵌套 shadow 树。一旦被污染的树不再有活跃根引用,KVM 就能 zap 并回收含那个被改写 SPTE 的页。
正常 present SPTE 被 zap 时,KVM 知道要更新哪个 rmap。但被污染的 slot 现在看上去是 MMIO。MMIO zap 路径会清掉它,不会摘掉 page A 那条被遗忘的 rmap 项——shadow 页就此被释放。rmap[A] 里的裸指针活了下来。
7.2 给 KVM 拥有的 shadow 内存加压
L2 stub 然后触碰了 49,152 个 GPA(Guest Physical Address,guest 物理地址),每个间隔 2 MiB:
for (uint64_t gpa = 0; gpa < 49152 * 0x200000; gpa += 0x200000)
*(volatile uint32_t *)(gpa + 0x440000);
稀疏排布是刻意的。每个 2 MiB 区域只访问一次,强迫 KVM 准备一份独立的底层 EPT 页,而不是让几百次访问共用同一张页表。目标不只是消耗 RAM——是让 KVM 造出几万份 shadow MMU 对象,并回收”曾经持有被污染 SPTE 的那种”内存。
7.3 加压之后停下来 grooming
加压循环结束后,L2 故意执行 HLT。回到 L1,harness 分配 8,192 个记账页,再恢复 L2。这就在 KVM 刚被压过 shadow 页之后,划出了一道受控的分配边界,避免所有分配混进一次黑箱喷洒。
7.4 VMFUNC 切到回收视图
harness 准备好 EPTP list(EPT Pointer List,EPT 切换用的根表),启用 VM function 0——EPTP 切换。L2 用 VMFUNC 指令进入含回收别名的备用视图。
VMFUNC 的好处是:L2 能在不打扰 L1 的前提下,从架构层面切换当前嵌套 EPTP。pwn 还用一组 EPTP 选择器轮过 8 个根,然后再切回原本想要的视图。
7.5 跨过 KVM rmap 回收阈值
KVM 6.1.74 里有一段这样的逻辑:
if (rmap->rmap_size > 1000)
kvm_zap_all_rmap_sptes(...);
pwn 把 1,002 个不同的 L2 GPA 映回原始 RAM 页 A,并在紧循环里触碰它们。这个数字不是拍脑袋——是阈值 + 2,刚好踩过”让 KVM 清理整张反向映射”那条边界。
rmap 里现在装着新 SPTE 指针 + 那个指向已释放 shadow 页的旧裸指针。kvm_zap_all_rmap_sptes() 走过每一项:
mmu_spte_clear_track_bits(sptep);
走到那个过期指针时,第一次操作把已释放 shadow 页存储加载出来——KASAN 看到了 host 端的读,Google 的 oracle 解封。
八、完整利用链状态机
赢的那次跑跨过了 7 个状态转换。5 个在 guest 端遥测里能直接看到,最后那个悬空指针访问只能由 Google host 端的 oracle 出证。
| 状态 | pwn 的操作 | KVM 的反应 |
|---|---|---|
| S0: shadow | L2 通过 RAM/RO EPT12 leaf 读 GPA 0x440000 | 构建 4 KiB present shadow SPTE,地址插入 page A 的 rmap |
| S1: retarget | L2 执行 VMREAD [rdi], rax | 模拟写 0xfebf0033 进 EPT12 PT[64],无 page-track 清理 |
| S2: poison | L2 沿旧只读翻译写 | 写错重走 EPT12;脆弱的 mmu_set_spte() 把 MMIO 装到现有 SPTE 上,A 的 rmap 留在原地 |
| S3: retire | Harness 发 INVEPT,L2 轮过 8 个 EPTP root | KVM zap/recycle 嵌套 shadow 页;MMIO 那个 slot 被清,rmap 指针活下来 |
| S4: pressure | L2 以 2 MiB 步长触碰 49,152 GPA | 强制 49,152 次 PT/data 分配和重度 shadow MMU churn |
| S5: consume | L1 grooming 8,192 页;L2 VMFUNC 到回收视图,触碰 1,002 个 A 的别名 | rmap 计数越过 1,000;KVM 走过已释放 host 内存里的过期 SPTE 指针 |
| S6: prove | Guest 调用 hypercall 102 | Google KASAN 后端挑战接口返回 cdc63bb4b015eb8c |
那 1,002 不是拍脑袋出来的数。利用这门手艺,是把”源码层面的可能性”变成”某一种精确的机器状态”。常量多到飞起。
九、从 3,304 行到 flag 的演进
harness 的族谱说明”代理写了一条利用链”到底是什么意思:
| 保留版本 | 行数 | 改了什么 |
|---|---|---|
| 5 月 10 日原型 | 3,304 | 两阶段 L2 启动,第一条 stale-rmap 通道 |
| 5 月 10 日 private-writer 修订 | 12,955 | VMREAD 写者、VMFUNC 机制、私有 KVM 写者实验、严格反汇编器判定 |
| 5 月 17 日获胜版 | 14,338 | root 轮换、记账式 grooming、稀疏加压、最终 flag 路径 |
最后的内核模块干的事远超”扣扳机”。它构建 VMCS12、四级 EPT12 树、EPTP list、备用根、结果页、别名、加压布局和动态生成的 L2 程序。它记录 VM-entry 状态、每个期望的 HLT、VMREAD 前后值、INVEPT 结果、VMFUNC 接受状态、分配计数、校验和以及挑战 hypercall。
赢的那次跑里 L2 payload 只有 537 字节。要让这 537 字节真的做了 pwn 以为它做的事,配套系统是 14,338 行。
十、为什么那个 token 是硬证明
Google 给 host 的 KASAN 分类代码打过补丁:只要识别到符合资格的非法读,就置一个 flag。
if (qualifying_invalid_read)
challenge_flag = true;
Hypercall 102 只在那个 host-side flag 被置位之后,返回每次启动一个随机值:
if (challenge_flag)
return per_boot_random();
这是官方 token 重要的原因。pwn 可以打印任何本地字符串,包括 “flag_found” 这几个字。但 pwn 不可能知道 Google 在 L0 host 里随机生成的值。能返回那个值的只有 host oracle 本身:
hypercall 102 → cdc63bb4b015eb8c
flag 的差距就在这里——一边是能聊漏洞利用的模型,一边是能跑完漏洞利用的代理。
十一、时间线与”日期”那一刀
kvmCTF 是零日漏洞奖励项目。pwn 5 月 17 日拿到 flag 的时候,CVE-2026-46113 还没公开。官方目标离线。5 月 18 日,团队问本地结果够不够。Google 说等真机器。
| 日期 | 事件 |
|---|---|
| 4 月 24 日 | 任务指令和证据标准创建 |
| 4 月 29 日 | 本地复现漏洞 |
| 5 月 10 日 | pwn 建模 stale-rmap 生命周期,预测 KASAN 表现会是 read-first |
| 5 月 11 日 | pwn 实现 memory-destination VMREAD 写者 |
| 5 月 12 日 | 团队告诉 kvmCTF 组 pwn 接近完成,发现官方目标不可用 |
| 5 月 13 日 | Google 确认官方服务离线,必须有 live flag |
| 5 月 17 日 | 推翻 guest 页回收理论;最终元组在本地返回同一 token 两次 |
| 5 月 19 日 | Google 表示本地 flag 无法处理,让团队等官方目标 |
| 5 月 28 日 | 该 issue 作为 CVE-2026-46113 公开 |
| 6 月 11 日 | Google 开始恢复服务 |
| 6 月 17 日 | pwn 在 live 上跑通,捕获 cdc63bb4b015eb8c |
| 7 月 1 日 | Google 在资格审查中指出几天前已有公开 patch 窗口 |
| 8 月 | Agent 收容与沙箱逃逸成为行业”分水岭”议题 |
| 9 月 28 日 | pwn.ai 公布此报告 |
5 月 28 日公布,但私下报告日期其实是 5 月 3 日——零日窗口在那时已经关闭。bounty 没拿到,只拿到”成为第一个拿下公认最硬沙箱挑战的代理”的荣耀。
十二、给防御者的启示
pwn 把研究总结成几个值得关注的点:
- 漏洞根因不是单点,而是”分支顺序 + 未跟踪写入”两件事叠加。 任何一次”先 return 再清理”的反模式都要审。
- 写入原语不只来自 host 或 DMA,模拟器自己也是写者。 KVM 用
kvm_page_track_write()通知来追踪 guest 物理写入;任何走系统写路径、跳过这个通知的代码路径都要审。 - rmap 阈值(1,000 项)作为安全护栏显然不够紧。 把”数量触发 zap”换成”按 dirty 列表触发”或者直接给 rmap 加引用计数会更稳。
- host oracle 的设计是双刃剑。 KASAN 报告路径作为可信 oracle,让 Google 能自动评估、拒绝 AI”嘴上说拿下”的伪成功。但同样的 hook 如果被外部研究团队复刻,本地可信度还得依赖代码审计。
- AI 代理能跑的利用链工程量和人类顶级研究员是同一量级。 pwn 自己讲得很直白:模型提供可能性,harness 决定能不能跑完这场竞赛。
十三、模型之外的系统
安全行业反复追问哪个基座模型”最会黑”。这个问题忽略了产出这个结果的系统大部分组成。
pwn 不是”接了个终端的裸模型”。基座模型只提供推理能力;pwn 的 harness 提供持久状态、目标记忆、专用工具、专用工作流、专用知识、独立审稿代理、证据闸门、重试逻辑,以及”理解一个原始漏洞利用原语”这件事本身。
这次活动里:
- 人类 提供任务、授权、账号和基础设施;
- pwn 的主研究代理 审计源码、设计并实现利用、跑实验室、操作官方目标;
- 怀疑派代理 攻击调用路径、生命周期假设、消费方;
- 反汇编器 拒绝把中间态标成胜利;
- Google 的 host 提供最终证明。
基座模型是引擎。harness 决定这辆车能不能跑完比赛。大多数 AI 安全演示就停在这项研究的起点——一类听起来合理的漏洞、一段 patch 摘要、一个本地会崩溃的 PoC。pwn 一路跑到 Google 的机器吐出一个秘密为止。模型产出可能性。
pwn.ai 在文末给所有”前沿”沙箱厂商留了一句话:
Built to breach.
原文出处:https://pwn.ai/blog/kvmescape(pwn.ai,2026-09-28)
关联参考:
- 漏洞 patch:
github.com/torvalds/linux/commit/0cb2af2ea66a - kvmCTF 规则:
google.github.io/security-research/kvmctf/rules.html - kvmCTF 上线公告:
security.googleblog.com/2024/06/virtual-escape-real-reward-introducing.html














暂无评论内容