PwnEscape:一个32字节的泄露,怎么变成KVM逃逸"

admin 2026-10-22 13:30:16 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

PwnEscape:一个 32 字节的泄露,怎么变成 KVM 逃逸

幻泉之洲

2026年10月19日 12:08
北京

去阅读

2026 年 8 月,Vercel 悬赏 100 万美元让人打破 Sandbox 隔离边界。三天后,我们在 Linux 网络栈里找到一个漏洞:guest 能控制校验和元数据,在内核已经校验过 IPv4 头之后,再把字节改掉。最初只是 32 字节宿主内存泄露,最后在我们自己的 KVM 环境里做成了完整的 guest-to-host 逃逸。漏洞不在 hypervisor 里,在网络栈里。

先说背景

2026 年 8 月 18 日,Vercel 宣布悬赏 100 万美元,邀请研究者击穿 Sandbox 产品的隔离边界。几周之前,OpenAI 刚披露一起事故:跑在受限评测环境里的 AI agent 找到了绕过网络隔离的办法,打穿内部基础设施,最后摸到了 Hugging Face 的系统。这些 agent 本来压根不该有外网。

但得把两件事分开看。绕过沙箱的网络限制是一回事,从虚拟机内部跑出来、摸到宿主机是另一回事。Vercel 的挑战两个都收。我们盯的是后者。

问题很朴素:一个不可信的 guest,在不碰 KVM 本身的情况下,能把宿主内核影响成什么样?

我们接了挑战,把研究基础设施拉起来,三天内就在 Linux 网络栈里挖到一个原语——guest 可控的校验和元数据,能在内核已经完成对 IPv4 头的校验、甚至已经据此做过决策之后,把头部字节重写掉。

一开始它看起来只是 32 字节的宿主内存泄露。后来它在我们自己的 KVM 以及很多其他 hypervisor 环境里变成了完整的 guest-to-host 逃逸。最有意思的地方在于:漏洞根本不在 hypervisor 里,在 Linux 网络栈里。

Vercel 后来把它列为整个项目里最重要的一份报告,最终统计中只有一个 Critical,就是现在的 pwnescape。加上另一份独立的网络逃逸报告,网络和宿主机两个目标都完成了。需要强调:Vercel 的沙箱本身的安全性已经高于行业常规水平,我们没找到任何配置错误。漏洞源于 Linux 内核,不是 Vercel 特有的缺陷。但 Vercel 处理这件事的方式值得给足认可——快速保护客户、自己另起独立研究、识别并通知其他受影响的 hypervisor 和沙箱厂商、和我们一起推动全行业协调披露和修复。他们保的不只是自己的客户。

下面是 CVE-2026-99999 的故事,一个被评为 Critical 的 Linux 内核漏洞,通向 KVM 逃逸。

研究是怎么跑的

我们用的是 Omega-Pwn,pwn.ai 的实验性研究套件,把自家安全架构和自动化,与 OmegaClaw 的部分能力拼在一起。OmegaClaw 由 SingularityNET 和 BGI Labs 的研究者开发。相比传统的 LLM 套件,它给的是更长的研究视野、可持久化的战役状态、并行协作调查,以及跨复杂研究会话沉淀假设、失败实验和证据的能力。

人负责掌舵:提出难的问题、设计工作流、决定调查方向。GPT-5.6 Sol 作为主要推理引擎,做源码级分析、探索攻击路径、挑战既有假设。Omega-Pwn 把人的直觉和上下文洞察,与 LLM 的分析能力接起来。

就我们所知,PwnEscape 暴露的是一类全新的 Linux 网络攻击面:不可信的校验和元数据能在内核已经校验并据此行动之后,重写安全敏感的包字段,把网络栈里大量默认假设掀翻,影响远超最初那个 VM 逃逸。

Part 01:一次延迟写入,一个可变的 IPv4 头

上游接受的修复是 6785011f8b16,标题为「ipv4: validate checksum\_start before completing checksum」。这是 PwnEscape 系列的第一篇,大部分篇幅会花在 sk\_buff、校验和元数据和 IPv4 分片上,因为失败就发生在那儿。

▲ 图 1:来自 guest 的包字节与校验和元数据,穿过虚拟网络进入宿主 Linux 网络栈

图 1 要说明的是:出问题的组件是宿主网络栈。「KVM 逃逸」描述的是我们在隔离验证环境里跨越的 guest/host 安全边界,不是 bug 所在的源文件。

01A / 我们把目光移出了 hypervisor

之前那次 kvmCTF 研究关注的是 KVM 的影子 MMU:映射、反向映射、内存生命周期之间的复杂互动。这次起点换了个更宽的假设——沙箱的安全性远不止取决于它的 hypervisor。一个 guest 交互的是整个宿主操作系统:设备模拟、虚拟网络、包过滤器、内存分配器,以及把这些粘起来的 API。

对 pwn 来说,这意味着一个更高效的研究策略:先画出可达边界,再找那些「不可信状态被提升为可信状态」的位置,然后去挑战每一个后续消费者身上的假设,而不是只盯着最初的解析器。我们用 GPT-5.6 Sol 辅助源码审查,人来定范围、判断什么算证据。当时真正有意思的问题是:

内核已经就某些包字节做出安全相关决策之后,一个异步或延迟操作还能不能把这些字节改掉?

Linux 的校验和 offload 正好给了这个可能性。

02 / CHECKSUM\_PARTIAL 是一个「稍后写入」的承诺

Linux 不总是立刻算传输层校验和。它可以把包保持在「校验和待完成」的状态里。相关的 sk\_buff 记账字段是这些:

skb->ip\_summed     CHECKSUM\_PARTIAL  (校验和仍待完成)
skb->csum\_start    校验和累加开始的位置
skb->csum\_offset   从该位置到输出字段的偏移

写入位置 = csum\_start + csum\_offset

这是完全正常、有文档的功能。发送侧,网卡或软件辅助函数 skb\_checksum\_help() 会算出校验和,把结果写进指定字段。在格式正常的 TCP/UDP 流量里,那个字段就是传输层校验和。在内核的常规路径上,这些元数据被假定描述的是真实存在的那个包。

关键点在于:这些值不只是描述性元数据,它们授权了一次对包内存的后续写入。如果信任级别较低的软件能影响它们,那它们的有效性必须在从接纳到完成之间的每一次操作里都站得住。

sk\_buff 对「包数据从哪里开始」还有好几套不同的概念:

skb->head             底层 head 分配的起点
skb->data             当前数据指针(拉取头部时会前移)
skb->network\_header   网络头的位置
skb->csum\_start       保存的校验和起点
skb\_headlen(skb)      当前线性持有的包数据长度

「相对帧」的视角和「相对网络头」的视角,这个区别很重要。一个包到达时,以太网头和其他链路层头还都在线性数据里。后面为了做 L3 处理,这些头会被剥掉或跳过。仅仅检查校验和起点在原始帧里足够靠后,并不等于证明了在归一化之后它落在 IPv4 头之后。

▲ 图 2:链路层解封装前后,帧相对坐标与网络头相对坐标的对比

被接纳的包所携带的链路层上下文,不是最终的 IPv4 上下文。问题在于保存的元数据和当前的网络头边界对不上,而不是物理分配真的移动了。原始报告里有精确的包几何;这张图只关注被破坏的那个不变量。

光在输入侧比一比,不够

第一批公开补丁把这个入口路径的缺陷暴露得很清楚。在 \_\_virtio\_net\_hdr\_to\_skb() 里,历史上的最小头检查用的是 skb\_transport\_offset(skb),这是个相对于当前 skb->data 视图的偏移。它能防住「校验和起点在这个视图里明显太靠前」,但没法确定,当包携带额外链路层结构时,这个起点离网络头到底有多远。

所以最初的加固提议加了一个网络相对比较:

/\ 概念对比,改编自我们的公开补丁系列 \/
transport\_offset >= minimum\_network\_header\_length
transport\_offset - network\_offset >= minimum\_network\_header\_length

这两个不等式不能互换。它们之间的差别,就是 L2/L3 坐标错位的本质。补丁评审后来超出了这个最初形式,因为嵌套头、IP 选项、IPv6、不同的入口调用方,都需要比单一乐观固定偏移更谨慎的校验。我们的原始输入侧补丁和 Willem de Bruijn 后来提出的更宽的输入校验,提供了源码层面的历史记录。

对最终的 IPv4 消费者来说,被接受的守卫表达了一个很有用的不变量。设 N = skb->network\_header,H = ip\_hdrlen(skb),C = skb->csum\_start,O 为非负的校验和偏移,那么最终输出位置 W = C + O。要求在完成前 C >= N + H,就推出 W >= N + H。这个守卫因此阻止了本次完成写入落到 IPv4 头里。但它并不证明 W 是 TCP 或 UDP 里的正确字段——那是另一个协议有效性问题。

这是第一处信任失效:来自较低信任来源的校验和元数据,进入宿主包处理时,没有被约束到后续代码真正会用的那个头上去。

03 / 头检查过了,然后头变了

最有意思的是操作顺序。

宿主入口看到一个普通 IPv4 头的包,解析它。分片处理和路由可以基于这个状态运转。之后,输出分片碰到了那个待完成的校验和。在历史代码里,ip\_do\_fragment() 是先完成校验和,再去取 IPv4 头指针、用头长度。修复前 net/ipv4/ip\_output.c 里的顺序大致是:

/\ 历史顺序,为便于说明做了简化 \/
if (skb->ip\_summed == CHECKSUM\_PARTIAL)
    skb\_checksum\_help(skb);    /\ 延迟写入 \/

iph = ip\_hdr(skb);
/\ 后续分片从 iph->ihl 推导 hlen \/

skb\_checksum\_help() 本身不是不安全。它在这里变得不安全,是因为畸形元数据让它的写入覆盖了「已经被解析成 IPv4 头」的那些字节。

决定泄露形状的字段是 IPv4 的 ihl——一个 4 位的 32 位字数。正常 IPv4 包的第一个字节包含版本和 IHL。在我们的受控研究几何里,它能从 0x45(IPv4,5 个字 = 20 字节)变成 0x4f(IPv4,15 个字 = 60 字节),而版本半字节完全没动。

▲ 图 3:IPv4 首字节从 0x45(IHL 5,20 字节)变成 0x4f(IHL 15,60 字节)的位级示意

两个字节的头改动,就能让包看起来还是 IPv4,同时把头长度的解读彻底换掉。0x4500 和 0x4f00 是研究案例里观察到的改动前后第一个字。

IHL 读数变化

包视图 首字节 IHL 解读出的 IPv4 头长度
解析时状态 0x45 5 字 20 字节
延迟写入之后 0x4f 15 字 60 字节

IPv4 版本半字节还是 4,变的只是那 4 位的头长度解读。

更早的那些阶段从来没认过一个 60 字节的头。它们都是按 20 字节头做的决策。可后面的消费者读到的却是一个更新过的字段,并且把它当成权威。

这是语义意义上的 time-of-check/time-of-use 问题,但不是线程间那种常规竞争。执行完全可以确定性地复现。隔开校验和使用的那个「时间」,是一串合法的网络操作,中间夹着一次攻击者能影响的延迟写入。

简化后的控制流

LESS-TRUSTED INPUT                   HOST NETWORK STACK
──────────────────                  ──────────────────────────────
包 + 部分校验和元数据 ────────────► 元数据安装进 skb
                                    │
                                    ▼
                              链路层处理 / L3 归一化
                                    │
                                    ▼
                              IPv4 解析、重组、策略、路由
                              头被信任为 IHL=5(20 字节)
                                    │
                                    ▼
                              输出分片
                                    │
                                    ▼
                              skb\_checksum\_help()
                              待完成的写入可以改动已解析的头
                                    │
                                    ▼
                              ip\_do\_fragment() 读到迟来的 IHL
                              头现在声称 60 字节
                                    │
                                    ▼
                              头拷贝超出逻辑线性数据
                                    │
                                    ▼
                              发出的 IPv4 选项里混进宿主字节

这是一张面向漏洞的路径示意图,不代表每一种网络配置都会走这些确切的钩子——不过我们确实找到了很多其他方式来触发这些有问题的路径。

04 / 那 32 字节到底从哪来

这段很容易讲砸。Linux 的包缓冲区不必把整包字节放在一个连续的线性对象里。一个 sk\_buff 有一个线性 head,还可能带非线性分片数据。相关重组完成之后,这个研究案例里的逻辑线性数据长度只有:

20 字节    原始 IPv4 头
 8 字节    第一个分片里可用的负载
────────
28 字节    skb 逻辑线性 head

那个迟来的校验和操作,让分片器以为 IPv4 头有 60 字节长。也就是说,它把原始 20 字节头之后的 40 个字节当成了 IPv4 选项。其中 8 个字节确实在线性 head 里,虽然它们本来是负载。接下来那 32 个字节根本不属于这个包的逻辑线性数据。

▲ 图 4:28 字节逻辑线性 head、后来 60 字节的头拷贝,以及超出逻辑包数据的那 32 字节泄露

skb\_headlen(skb) = 28,而后续拷贝长度是 60。差值 32 字节。这是一次逻辑层面的 skb 数据越界读,它不一定跨越物理分配或单个 slab 对象的边界。

这个区分很重要。这里的「越界」指的是 sk\_buff 合法的逻辑线性包数据边界,不必然是越过整个分配器对象的访问。宿主可能已经把分配尾部 room 物理映射到了 skb\_headlen() 之后。读这些字节并把它们塞进一个对外发送的网络头,照样越过了机密性边界。它们是宿主内存,不是发送方作为逻辑包一部分提供的字节。

IPv4 分片器随后就能用这个被改过的头形状生成出站分片。最终发出的包把这些字节当作 IP 选项暴露出来。问题从宿主内部状态,变成了一个外部可观察的产物。

这就是「这可能读过了缓冲区」和「宿主发出了发送方逻辑包之外的数据」之间的区别。

为什么验证必须带对照组

我们不想把一个有意思的十六进制 dump 跟一次宿主泄露搞混。证据设计把两种情况分开了:一种是身份对照,包原始头长度保持不变;另一种是主动案例,有后续操作改变了它的解读。测试检查最终发出的包结构是否有效、已知逻辑数据之后是否出现非零字节、以及这些字节是否和发送方设置的守卫数据不同。

测试还用了独立的应用层回执,与原始抓包观察并列。两个信号互相印证,用来区分真正的宿主侧重写与发送,和包构造或抓包工具造成的假象。

首个有界泄露在每次主动案例里稳定复现 32 字节。我们把内容当作敏感数据处理,也没有必要为了证明跨越边界去保留无关的宿主原始内存。

另外,我们在一批研究者自有的、一次性的 Linux 6.12 和 7.x 宿主上,单独确认了一个宿主可用性故障。

05 / 为什么这是逃逸原语,不只是一次泄露

在多租户环境里,一个可重复、攻击者可控的宿主内存泄露,本身已经够严重了。它暴露了 guest 本不该看到的那部分信任域的信息。它同时改变了漏洞利用研究的性质:一个有可用的读原语,能帮你消解宿主内存状态的不确定性,让更多内存操纵的假设变得可测试。

在我们自有的 KVM 测试装置里,研究走得更远。借助宿主内存观察和精心控制的分配行为,我们演示了有选择的 64 字节宿主内核写。这个能力被用在一条有界的实验链里,执行了一个宿主 root 辅助程序,并把一个宿主生成的标记返回给 guest。四次独立的真冷启动都跑通了整条链。

所以我们用 guest-to-host 逃逸来描述这个隔离验证。它不是从一次越界读推出来的结论,也不是声称在某个厂商生产环境里执行了 root 命令。

▲ 图 5:证据阶梯——源码假设、受控改动、宿主内存泄露、有选择的内核写、自有机器上的有界宿主执行

每一级都是不同的证据主张。源码审查本身不能证明泄露;泄露本身不能证明写入;实验室里的执行结果也不会变成生产环境执行的主张。

那条依赖分配器的写入链、对象生命周期控制,以及针对不同 hypervisor 的适配,体量大到值得单独写一篇。我们怀疑安全行业会拿这篇文章去推断其他受影响的 hypervisor。我们也预计,针对较弱配置的利用会在几周内出现,针对默认配置的利用在一个月内出现。PwnEscape 第二篇会讲从原语走到宿主控制都经历了什么,而不是假装 32 字节泄露能轻松变成代码执行——前提是社区没先我们一步。

06 / 更普遍的失效模式:延迟元数据信任倒置

抛开这个具体补丁,这项研究真正有意思的地方,是它把三种不同的信任转移叠在了一起:

  1. 来源信任

:信任级别较低的 guest 或进程,描述了一个宿主稍后会执行的操作。
2. 解读信任

:处理链路层头时包的表示变了,内核按某一种布局解析字段。
3. 时间信任

:一个延迟操作在另一个内核消费者使用之前,改掉了那个已经解析过的状态。

我们内部讨论时管这个叫「延迟元数据信任倒置」(deferred-metadata trust inversion)。这只是给机制起的描述性名字。各个构件都是已知的,被利用的很多基础原理也有公开文档;真正新且有后果的,是通过这些 Linux 网络交互走出来的具体宿主边界利用链。

反复出现的反模式长这样:

不可信元数据进入
          │
          ▼
针对早期表示做校验
          │
          ▼
包被归一化、解析、授权
          │
          ▼
元数据驱动的操作稍后执行
          │
          ▼
安全相关字节发生变化
          │
          ▼
后续消费者信任了改变后的解读
          │
          ▼
内存安全或策略边界失效

对应的审计问题不是「我们校验过校验和偏移了吗」,而是:「在每一次变换之后,一个延迟写入者还能不能瞄准那些语义已经被信任过的字节?」

这个区别对防御同样重要。一个防止 IPv4 头被覆盖的窄检查,能挡住这里演示的内存安全路径,但它不证明每个协议特有的校验和目的地址都是对的。上游评审期间,研究者也提出了关于策略检查之后传输层字段可能被改动的一些不同担忧。那些是相关的一致性风险,但不该被并进已证实的 32 字节泄露,也不该宣称被 IPv4 修复完全解决。

这个经验可以推广成一种审计延迟工作和可变元数据的方法。它本身不构成证据,说明其他无关协议、操作系统或产品存在漏洞。

07 / Linux 到底改了什么

修复的故事值得讲,因为整改的形状在评审过程中变了。

我们最初的候选补丁系列处理路径上的两个不同点:virtio 校验和元数据的接纳,以及 IPv4 分片消费者。公开的九月补丁系列解释了这个理由,也包含了评审讨论。后续评审找出了更多的校验和完成消费者,并争论了「在中心辅助函数里放一个宽检查」还是「在多个汇点做定点检查」的风险。

Michael S. Tsirkin 被上游接受的补丁选了后者:一个很小的 IPv4 专用守卫,放在五个受影响消费者里每一个的校验和完成之前。辅助函数加在 include/net/ip.h:

static inline int ip\_check\_csum\_start(const struct sk\_buff \*skb)
{
    if (unlikely(skb->csum\_start <
                 skb->network\_header + ip\_hdrlen(skb)))
        return -EINVAL;
    return 0;
}

核心不变量很简单:

校验和起点 >= IPv4 网络头起点 + 已解析的 IPv4 头长度

对于正常的非负 csum\_offset,这保证了最终的校验和输出位置不会落进 IPv4 头里。这个检查不去证明目标是合法的 TCP 或 UDP 校验和字段;它只做出保护「已被信任的 IPv4 网络头」所需的那个较窄的主张。

被接受的提交 6785011f8b16 在两个消费者路径里加上了这个守卫:

▲ 图 6:ip\_check\_csum\_start 守卫在 Linux 已接受补丁改动六个文件中的五个 IPv4 校验和完成消费者

一个共享的 ip\_check\_csum\_start() 辅助函数,五处受保护的校验和完成点。共享辅助函数本身在第六个被改的文件里。补丁被明确描述为纵深防御。

每个相关位点的关键新顺序是:先校验校验和起点,再完成校验和。在 ip\_do\_fragment() 里,修复后的简化序列是:

if (skb->ip\_summed == CHECKSUM\_PARTIAL) {
    err = ip\_check\_csum\_start(skb);
    if (!err)
        err = skb\_checksum\_help(skb);
    if (err)
        goto fail;
}

如果要做 backport 或者评审相关工作,有个细节特别重要:这个提交不是对 virtio 校验和 API 的彻底重设计。提交信息明确称它是纵深防御措施,并说更宽的入口校验还需要进一步工作。最初的讨论也考虑过没有 VM 的 TUN/AF\_PACKET 场景。

Michael 在私下评审中复现了原始问题。Willem de Bruijn 和其他网络评审者评估了被接受的方案,包括改动广泛使用的校验和路径的成本。

补丁在 2026 年 10 月 8 日合入 net.git,随后于 10 月 14 日进入 Linus 主线。稳定版 backport 和 CVE 正式发布情况,发布时请对照实时的内核记录确认。

08 / 研究手记:三天拿到原语,几周修完

日期(2026) 研究节点
8 月 18 日 Vercel 公布 100 万美元 Sandbox 研究挑战
8 月 20 日 我们提交最初的有限宿主内存泄露报告
9 月 4 日 私下向内核安全联系人和网络维护者报告宿主内核根因,附候选补丁
9 月 7 日 Michael S. Tsirkin 回复称已复现问题,认可候选补丁
9 月 19–22 日 公开 netdev 提交与修订,扩大对网络头不变量及相关路径的评审
10 月 7–8 日 修订后的上游守卫发布并被接受进 net.git
10 月 13–14 日 稳定版评审与 CVE 协调继续

整条线我们刻意做了证据区分:疑似 bug 与已演示的泄露、泄露与宿主写入、自有宿主上的 root 标记与厂商生产结果、代码审查与被接受的上游补丁。这些不是官僚标签,而是让野心勃勃的漏洞研究保持可证伪的方法。

这也是模型周边那些 AI 系统的意义。研究循环更像是:模型或研究者提出一个不变量被违反 → 构造一个有界测试 → 检查对照组和证伪案例 → 保留证据 → 与专家研究者一起迭代 → 请独立评审者攻击结论 → 实现并重新测试漏洞利用。我们用这个循环搜更大的空间,同时不降低证明标准,API token 花了不到 700 美元。

第一篇只聚焦公开的内核修复和我们自有宿主上的证据。厂商相关的协调细节和私下生产环境遥测,在披露状态明确放行之前保持分开。

参考资料

[1] https://pwn.ai/blog/pwnescape

PwnEscape:一个32字节的泄露,怎么变成KVM逃逸" 网络安全文章

PwnEscape:一个32字节的泄露,怎么变成KVM逃逸"

这篇文章介绍PwnEscape漏洞研究,研究团队在Vercel百万美元悬赏挑战中,于Linux网络栈发现guest可控校验和元数据漏洞,可在内核校验IPv4头后重写头部字节,从32字节内存泄露发展为完整KVMguest-to-host逃逸。漏洞编号CVE-2026-99999,被评为Critical,根源在Linux网络栈而非hypervisor。
AI挖洞实战07|Workbuddy+Skill技能库,完整复现SSRF漏洞" 网络安全文章

AI挖洞实战07|Workbuddy+Skill技能库,完整复现SSRF漏洞"

本文介绍利用Workbuddy搭配Skill技能库自动化探测SSRF漏洞的完整流程,包括配置AI约束、收集接口、批量Payload测试、人工复现验证、根因分析与修复方案,并生成SRC标准漏洞报告。文章强调仅限授权测试,并提供白名单校验、黑名单过滤等修复建议,末尾推广AI挖洞技能库。
评论:0   参与:  0