跳到正文
Hacker News· SerCe·· 4 小时前AI 评分65

Firecracker VM 逃逸:PwnEscape 第二部分实现宿主机 root 代码执行

Firecracker VM Escape: PwnEscape Part 2

AI 导读

PwnEscape 第二部分展示了从 Firecracker microVM 内逃逸并在宿主机上以 root 执行代码的完整链路,对应 CVE-2026-99999。

正文 · AI 翻译

Firecracker 本应将不受信任的代码限制在 microVM 内。我们从中逃逸,并以 root 身份在宿主机上实现了代码执行。

我们发现了它如何保存寄存器、如何改变恢复执行的位置,并让它在从 KVM 返回时执行我们选定的调用。随后客户机恢复了临时的栈改动。Firecracker 继续运行,VM 仍可通过 SSH 访问。

PwnEscape 第 1 部分 介绍了这项工作背后的宿主机内存原语。这里我们继续链条的下一部分:利用该访问权限在内存中找到 Firecracker,并从客户机内部接管一个活动线程。这是延续性内容,用于在 Firecracker 环境中展示 CVE-2026-99999。

00 / 选择接管控制的位置

Firecracker microVM 在宿主机上有一个管理它的进程。每个虚拟 CPU(vCPU)在该进程中都有一个线程。该线程进入 KVM 运行客户机,当需要在用户空间处理事件时返回 Firecracker。Firecracker 在其 架构概览 中记录了这种安排。

firecracker-host-integration-dbdde36ef234
firecracker-host-integration-dbdde36ef234

运行虚拟 CPU 的调用如下所示:

当线程处于该系统调用中时,宿主内核将其保存的用户空间寄存器保存在一个名为 pt_regs 的结构中。这些寄存器描述了 Firecracker 线程:其指令指针指明执行应从何处恢复,其栈指针指明应使用哪个用户空间栈。

这使得从 KVM_RUN 返回成为一个有吸引力的目标。如果我们能定位并修改保存的帧,就能选择 Firecracker 在线程返回用户空间时执行的代码。导致这种返回的客户机操作将提供触发条件。

我们已经通过客户机映射获得了对选定宿主物理内存的访问权限。要利用它,我们需要识别 Firecracker 进程、找到该进程中可调用的代码,并定位属于其活动 vCPU 线程的寄存器帧。起点是我们在客户机内部控制的一个内存页。

01-guest-to-vmm-chain-5c0fbee0cb07
01-guest-to-vmm-chain-5c0fbee0cb07

图 1。客户机识别其 Firecracker 进程,找到 vCPU 线程保存的寄存器,并在该线程从 KVM 返回时重定向它。

01 / 在宿主机上找到我们的客户机页

客户机和宿主机使用不同的物理地址空间。一个页在 VM 内部有客户机物理地址(GPA),在支撑它的机器中有宿主物理地址(HPA)。在目标上,扩展页表(EPT)提供两者之间的转换。

我们在一个客户机页中放置了可识别的字节模式,即 nonce。第一个解析器将该页已知的 GPA 及其内容作为输入。两者共同为我们提供了一个要转换的地址,以及一种检查转换是否到达我们页面的方法。

解析器在内存原语覆盖的宿主物理范围内搜索候选 EPT 根。EPT 根是转换表的起点。对于每个候选,解析器沿着由客户机地址选定的条目前进,直到到达一个物理页,然后检查该页是否有 nonce。

下面的片段是带有描述性辅助名称的解释性伪代码。它们展示了各阶段之间传递的数据以及用于连接它们的检查。

nonce 检查将提议的转换与我们控制的内存联系起来。更广泛的搜索产生了两个候选;对嵌套页表的验证将它们缩减为一个 EPT 根。遍历该根得到了我们客户机页的宿主物理地址。

我们将此结果称为 guest_page_hpa,并将其传递给下一个解析器。现在我们在宿主内存中有了一个可识别的对象。下一个任务是找到映射它的宿主进程。

02 / 查找 Firecracker 进程

Firecracker 将客户机使用的内存映射到自己的地址空间。这种映射为我们提供了一种从客户机页面反向追溯到进程的方法。

宿主进程使用页表将其虚拟地址转换为物理内存。顶层称为页全局目录,即 PGD。恢复适当的 PGD 将使我们能够转换 Firecracker 内部的地址,并通过内存原语读取其代码和栈。

我们的反向映射器在原生宿主页表中搜索 guest_page_hpa 的映射。正常的页表遍历从虚拟地址开始并产生物理地址。在这里,我们寻找一个到已知物理页的映射,然后恢复与之关联的虚拟地址和页表根。

该检查在两个方向上运行:

这将宿主进程的虚拟映射与通过 EPT 找到的物理页连接起来。反向映射器返回了一个经过验证的地址空间。我们将其根保留为 host_pgd,并用它来检查该进程。

下一个解析器在这些映射中查找 Firecracker 可执行文件。它识别出一个 ELF 映像,并检查了 Firecracker 的 write(2) 包装器中已知的字节序列。这将恢复的地址空间与我们目标二进制文件连接起来。

ASLR 会改变可执行文件的加载位置。ELF 映像中的地址与其对应的运行时地址之间的差异就是加载偏移。一旦映射器识别出映像,它就可以恢复该偏移,并用它来定位漏洞利用所需的代码:

映射器将加载偏移返回为 firecracker_base。有了该值和 host_pgd,我们就可以定位运行进程中的代码,将其虚拟地址转换为主机物理内存,并将那里的字节与预期的二进制文件进行比较。

我们检查了三个位置:将产生我们证明的 write(2) 包装器、KVM ioctl 之后的继续执行点,以及干净返回所需的短 xor eax,eax; ret 序列。当我们在下面构造返回路径时,它们的用途就会变得相关。

此时,客户机可以检查 Firecracker 映像及其私有映射。我们仍然需要当前运行我们 VM 的线程的已保存寄存器。

02-address-resolution-0e47fd8bf102
02-address-resolution-0e47fd8bf102

图 2. nonce 标识宿主内存中的客户机页面。其映射指向宿主页表根,使下一个解析器能够定位并验证 Firecracker 映像。

03 / 定位活动的 vCPU 帧

帧查找器从前面的阶段获取了 host_pgd 和 firecracker_base。它使用它们来识别宿主线程的已保存状态。

最有用的线索是返回地址。KVM_RUN 中的 vCPU 线程应具有一个已保存的指令指针,指向 Firecracker 中相应的 ioctl 继续执行点。我们可以从已验证的二进制文件及其加载偏移计算出该地址。

然后我们在宿主内存中搜索与该调用一致的 pt_regs 帧。仅地址匹配也会拾取过时的栈内容,因此候选者必须满足若干相关检查:

已保存状态我们检查的内容
系统调用系统调用标识和进行中状态与活动的 ioctl 匹配。
请求参数ioctl 请求为 KVM_RUN。
文件描述符描述符在辅助程序的预期范围内。
指令指针它与已验证的 Firecracker ioctl 继续执行点匹配。
栈指针它通过恢复的宿主 PGD 进行转换。
段选择子和标志他们描述了一次有效的返回用户空间。
调用方返回地址它落在经过验证的 Firecracker 文本映射范围内。

栈检查将该帧与我们已从客户机页面恢复的同一地址空间绑定。指令地址和调用方地址将其与 Firecracker 镜像绑定。系统调用字段将其与运行 vCPU 的操作绑定。

搜索后只剩一个候选。其位置被存储为 frame_hpa,即保存的寄存器帧的宿主机物理地址。我们将它、PGD 和加载偏移传递给最终辅助函数。

在更改内存之前,辅助函数重复了代码签名、帧和栈检查。这确保了活动阶段使用的进程状态与解析器所识别的一致。随后的执行和恢复检查将确认所选帧是活跃的。

03-live-kvm-run-frame-ddb3ad26cf50
03-live-kvm-run-frame-ddb3ad26cf50

图 3. 候选寄存器帧必须与 KVM ioctl、恢复的进程页表以及经过验证的 Firecracker 代码一致。

04 / 重定向来自 KVM 的返回

我们选择了一个简单的载荷:让 Firecracker 进程向其自身的 stderr 写入一个可识别的字符串。这将为宿主机运行器提供来自我们已接管进程的可观察结果。

该字符串为 N8V23M2\n,七个字符后跟一个换行符。它占用八个字节,可容纳于一个栈字中。我们希望 Firecracker 执行的调用是:

此处 2 是 Firecracker 的 stderr 文件描述符。缓冲区指针必须指向 Firecracker 地址空间中的字符串,因此我们将该字符串存储在其私有用户空间栈上。

在修改之前,我们保存了三个栈字。一个存放字符串;另外两个存放临时调用序列的返回地址。然后我们修改保存的寄存器帧,以提供包装器地址、其参数以及该序列的栈指针:

从 write 返回也需要小心处理。成功写入八个字节返回 8,而成功的 KVM_RUN 调用返回零。我们将 xor eax,eax; ret 的地址放在返回序列的首位。它在第二个地址将执行带回 Firecracker 原始 ioctl 继续之前清除返回值。

由此产生的控制流为:

在栈和保存的寄存器准备就绪后,客户机向其串行端口写入一个字节。该操作导致 vCPU 线程从 KVM_RUN 返回。内核恢复了修改后的寄存器,Firecracker 带着我们的参数进入 write 包装器。

包装器输出了标记,返回序列清除了 eax,线程重新加入了其原始执行路径。控制权返回客户机后,辅助函数恢复了全部三个栈字。

04-reversible-control-flow-10ae6af41746
04-reversible-control-flow-10ae6af41746

图 4. 保存的寄存器帧进入写入包装器。两个返回地址经过返回值调整并回到原始 Firecracker 路径。

05 / 检查执行与恢复

宿主机运行器将 Firecracker 的 stderr 与客户机分开捕获。完整输出为:

捕获内容恰好为八个字节,包括最后的换行符。Firecracker 仍在运行,客户机 SSH 响应正常,辅助函数已恢复三个临时栈字。

我们通过一次不进行内存更改的新过程检查了恢复情况。它被赋予相同的帧地址,并在触发后重复了寄存器、代码和栈检查。检查通过,且未在临时栈位置发现残留标记。

这些观察结果回答了三个不同的问题。客户机日志记录了触发和恢复过程。宿主机捕获显示 Firecracker 执行了所选操作。最终验证检查了执行后的状态。

检查记录结果
Firecracker stderr标记的一份副本,共八字节
Firecracker 进程PID 2936,触发后仍存活
客户机健康状态SSH 可响应;运行时间 1615.72 秒
临时栈状态三个原始字全部恢复
可执行文本和共享库未修改
触发后的宿主机内核错误未观察到
恢复后的验证提供的帧地址及周围不变量均通过
05-proof-receipt-6e8ad89bf50c
05-proof-receipt-6e8ad89bf50c

图 5。客户机日志、Firecracker stderr 捕获和最终验证记录了触发、宿主机执行和恢复后的状态。

06 / 宿主机上的 root 执行

当标记出现时,客户机已选中宿主机线程的指令指针、函数参数、栈数据和返回路径。Firecracker 使用自身权限执行了该调用。

目标是 x86-64 上的 Firecracker v1.16.1,宿主机内核为 7.0.0-1011-aws,客户机内核为 6.18.44+。我们以 root 身份在 systemd-run 下直接启动 Firecracker,并用 taskset 将其固定。因此,受控调用以 root 身份在宿主机上执行。

现有的写入封装为我们提供了对该控制的紧凑证明。将其扩展到其他调用需要选择与该进程映射和系统调用策略兼容的代码和参数。

此处使用的配置没有 jailer。Firecracker 的生产环境隔离包括降低权限,并通过命名空间和 seccomp 限制进程。使用这些控制的部署需要单独评估其 VMM 被攻陷后可用的权限和资源。

地址发现使漏洞利用能够跟随正在运行的实例。EPT 解析器将客户机页连接到宿主机内存;反向映射器恢复宿主机地址空间;镜像映射器在 ASLR 下定位 Firecracker;帧查找器识别 vCPU 的保存状态。将最后阶段适配到另一个构建需要验证该二进制的代码签名和返回路径。

客户机利用对宿主机内存的访问接管了运行 VM 本身的线程。串行写入提供了触发,而下一个用户空间操作属于我们。

捕获的解析器值和产物

以下值来自保留的执行记录。上述方法使用符号名称,以便每个值的作用清晰;此表将这些名称与记录的地址关联起来。

值记录的地址在链中的位置
guest_gpa0x08f03000nonce 页的客户机物理地址,提供给第一个解析器
guest_page_hpa0x165610000该页的宿主机地址,由 EPT 解析器找到
host_pgd0x14afc2000由反向映射器恢复的宿主机页表根
firecracker_base0x7298c2400000由镜像映射器恢复的 Firecracker 加载偏移
frame_hpa0x13a5bff48为活动阶段选择的保存寄存器帧

记录使用运行 ID eventpath-v23-0907self02 和启动 ID 2b3cd5ac-2311-427f-a50a-6a79ecb8daa7。这些标识符将解析器记录、宿主机捕获和恢复后验证关联起来。

较早的内存提供程序还在构建内存孔径后,接收了宿主机选择的任务和 canary 页帧号作为独立验证叠加层。这些地址未被 EPT 解析器、反向映射器、Firecracker 映射器、帧查找器或最终标记辅助程序使用。

发布包包含最终辅助程序源码、JSONL 解析器记录、主机捕获、恢复清单、四个保留的只读解析器二进制文件及其来源信息,以及 SHA-256 清单。最终辅助程序接受解析器生成的 PGD、加载偏移和帧 HPA。其检查覆盖运行标识符、地址范围、代码签名、保存的寄存器和栈几何结构;其试运行模式执行验证而不写入内存。

概念验证

工件哈希为:

使用以下命令对照清单验证发布包:

shasum -a 256 -c SHA256SUMS

技术参考

来源:Hacker News · pwn.ai