
研究人员利用 CVE-2026-99999 逃逸 Firecracker 微虚拟机并在宿主机获取 root 权限
来源标题:Firecracker Escape
研究人员成功从 Firecracker 微虚拟机中逃逸,并在宿主机上以 root 权限执行代码。他们利用宿主内存访问原语定位内存中的 Firecracker 进程,从客户机内部接管了运行 vCPU 的活跃线程,篡改其在 KVM_RUN 返回时的寄存器与恢复位置。整个过程中 Firecracker 持续运行,虚拟机仍可通过 SSH 访问。
以下正文由 AI 翻译自 Hacker News 最新(原文版权归原站所有),可能存在偏差,重要内容请以原文为准。
Firecracker 本应将不受信任的代码限制在 microVM 内。我们却成功逃逸,并在宿主机上以 root 权限实现了代码执行。 我们弄清了它如何保存寄存器、如何改变恢复执行的位置,并让它从 KVM 返回时执行一次由我们选定的调用。随后 guest 恢复了临时的栈修改。Firecracker 继续运行,VM 也仍然可以通过 SSH 访问。 PwnEscape 第一部分介绍了这项工作背后的宿主机内存原语。这里我们继续链条的下一环:利用该访问能力在内存中找到 Firecracker,并从 guest 内部接管一个活动线程。这是后续内容,用于在 Firecracker 环境中展示 CVE-2026-99999。 一个 Firecracker microVM 在宿主机上有一个管理它的进程。每个虚拟 CPU(vCPU)在该进程中都有一个线程。该线程进入 KVM 运行 guest,当用户空间需要处理事件时再返回 Firecracker。Firecracker 在其架构概览中记录了这种安排。 运行虚拟 CPU 的调用如下所示: ioctl(vcpu_fd, KVM_RUN, 0); 当线程处于该系统调用中时,宿主机内核将其保存的用户空间寄存器保存在一个名为 pt_regs 的结构中。这些寄存器描述了 Firecracker 线程:其指令指针指明执行应从何处恢复,其栈指针指明应使用哪个用户空间栈。 这使得从 KVM_RUN 返回成为一个有吸引力的目标。如果我们能够定位并修改保存的帧,就可以选择线程返回用户空间时 Firecracker 执行的代码。一个导致这种返回的 guest 操作将提供触发点。 我们已经通过一个 guest 映射获得了对选定宿主机物理内存的访问。要利用它,我们需要识别 Firecracker 进程、在该进程中找到可调用的代码,并定位属于其活动 vCPU 线程的寄存器帧。起点是我们在 guest 内部掌控的一个内存页。 图 1. guest 识别其 Firecracker 进程,找到 vCPU 线程保存的寄存器,并在该线程从 KVM 返回时重定向它。 guest 和宿主机使用不同的物理地址空间。一个页在 VM 内部有一个 guest 物理地址(GPA),在支撑它的机器中有一个宿主机物理地址(HPA)。在目标上,扩展页表(EPT)提供两者之间的转换。 我们在一个 guest 页中放置了一个可识别的字节模式,即 nonce。第一个解析器将该页已知的 GPA 及其内容作为输入。二者共同为我们提供了一个要转换的地址,以及一种检查转换是否到达我们页面的方法。 解析器在内存原语覆盖的宿主机物理范围内搜索候选 EPT 根。EPT 根是转换表的起点。对于每个候选者,解析器沿着 guest 地址所选择的条目前进,直到到达一个物理页,然后检查该页是否存在 nonce。 下面的代码片段是带有描述性辅助函数名的解释性伪代码。它们展示了各阶段之间传递的数据以及用于连接它们的检查。 for root in candidate_ept_roots: page_hpa = walk_ept(root, guest_gpa) if page_hpa is invalid: continue contents = read_host(page_hpa, length(nonce)) if contents == nonce: candidates.append((root, page_hpa)) nonce 检查将提议的转换与我们控制的内存连接起来。更广泛的搜索产生了两个候选者;对嵌套页表的验证将它们缩减为一个 EPT 根。遍历该根给出了我们 guest 页的宿主机物理地址。 我们将这个结果称为 guest_page_hpa,并将其传递给下一个解析器。现在我们在宿主机内存中有了一个可识别的对象。下一个任务是找到映射它的宿主机进程。 Firecracker 将 guest 使用的内存映射到自己的地址空间中。这种映射为我们提供了一种从 guest 页反向追溯到进程的方法。 宿主机进程使用页表将其虚拟地址转换为物理内存。顶层称为页全局目录(PGD)。恢复适当的 PGD 将使我们能够在 Firecracker 内部转换地址,并通过内存原语读取其代码和栈。 我们的反向映射器在原生宿主机页表中搜索 guest_page_hpa 的映射。正常的页表遍历从虚拟地址开始并产生物理地址。在这里,我们寻找指向一个我们已经知道的物理页的映射,然后恢复与之关联的虚拟地址和页表根。 for mapping in reverse_map_host_page_tables(guest_page_hpa): pgd = mapping.root guest_page_va = mapping.virtual_address resolved_hpa = walk_host_page_tables(pgd, guest_page_va) if resolved_hpa != guest_page_hpa: continue if read_host(resolved_hpa, length(nonce)) != nonce: continue process_candidates.append((pgd, guest_page_va)) 这将宿主机进程的虚拟映射与通过 EPT 找到的物理页连接起来。反向映射器返回了一个经过验证的地址空间。我们将其根保留为 host_pgd,并用它来检查该进程。 下一个解析器在这些映射中寻找 Firecracker 可执行文件。它识别出一个 ELF 映像,并检查 Firecracker 的 write(2) 包装器中一个已知的字节序列。这将恢复的地址空间与我们针对的二进制文件连接起来。 ASLR 会改变可执行文件的加载位置。ELF 映像中的地址与其对应的运行时地址之间的差异就是加载偏移。一旦映射器识别出该映像,它就可以恢复该偏移,并用它来定位漏洞利用所需的代码: runtime_code_address = load_bias + code_address_in_elf 映射器将加载偏移作为 firecracker_base 返回。有了该值和 host_pgd,我们就可以在运行中的进程里定位代码,将其虚拟地址转换为宿主机物理内存,并将那里的字节与预期的二进制文件进行比较。
(原文较长,此处译出前 6000 字,完整内容请点「打开原文」。)
正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。