0xCyberstan/CVE-2026-53360-POC

GitHub: 0xCyberstan/CVE-2026-53360-POC

该项目是 KVM SEV-SNP PSC 处理程序中堆越界读写漏洞(CVE-2026-53360)的概念验证,用于复现恶意客户机破坏宿主机内核堆的攻击路径。

Stars: 0 | Forks: 0

# CVE-2026-53360:KVM SEV-SNP PSC 堆越界 KVM 的 SEV-SNP Page State Change (PSC) 处理程序中堆越界读取和写入的概念验证。一个恶意的 SEV-SNP 客户机使得宿主机内核在解析 PSC 条目数组时越过了其 slab 分配的末尾。这会泄露相邻 `kmalloc-cg-32` 对象的布局,并将一个受控的小值写入其中,且客户机可以随意重复此操作。 完整分析:https://cyberstan.co.uk/sev-snp-oob/ | | | |---|---| | CVE | CVE-2026-53360 | | 组件 | KVM SNP 宿主机支持,`arch/x86/kvm/svm/sev.c` | | 引入版本 | `9b54e248d264`(首个 KVM SNP PSC 处理,2024 年 5 月,~v6.10) | | 修复版本 | `db3f219`(主线,2026 年 5 月,Cc: stable),标记为 `Fixes: 4af663c` | | 报告 | security@kernel.org,2026 年 4 月 8 日 | | 受影响范围 | 仅限 SEV-SNP 宿主机路径。KVM 不会为普通的 SEV-ES 客户机启用 PSC。 | ## 影响 任何 SEV-SNP 客户机都可以通过发送格式错误的 PSC 请求来破坏宿主机内核的堆,并读取有关其布局的信息。这是从客户机到宿主机的攻击方向:SEV-SNP 的构建是为了保护客户机免受不可信宿主机的攻击,但宿主机仍必须防御恶意客户机,而此处理程序并没有做到这一点。 ## 环境要求 这需要真实的 SEV-SNP 硬件。它无法在 Intel 上复现,且嵌套虚拟化无法为你提供 SNP 客户机。 硬件: - 带有 SEV-SNP 的 AMD EPYC 服务器芯片:Milan (7003) 或更新版本,即 Genoa (9004)、Bergamo、Siena 或 Turin。SEV-SNP 是 EPYC 专属硅片。它不适用于 Ryzen 或 Threadripper,并且这里没有 Intel 的等效产品(Intel 使用 TDX)。 - 裸机。裸机云实例也可以工作(Vultr、AWS `*.metal`、Hetzner AX 等)。 - 在 BIOS 中,启用 SEV、SEV-ES、SEV-SNP、SME、IOMMU 和 SVM。托管的裸机云通常默认已开启这些功能。 宿主机内核: - 在编译时开启 KASAN,以便报告越界访问。如果没有 KASAN,该 bug 仍然会破坏宿主机内存,只是不会被打印出来。在 6.11.11 上测试过。 CONFIG_KASAN=y CONFIG_KASAN_GENERIC=y CONFIG_KVM=y CONFIG_KVM_AMD=y CONFIG_KVM_AMD_SEV=y CONFIG_CRYPTO_DEV_SP_PSP=y - 启动时开启 SNP 并在 multi-shot 模式下启用 KASAN,以便记录每一次命中: kvm_amd.sev=1 kvm_amd.sev_es=1 kvm_amd.sev_snp=1 kasan_multi_shot - 确认宿主机已准备就绪: cat /sys/module/kvm_amd/parameters/sev_snp # Y ls /dev/sev # /dev/sev 宿主机用户空间: - 支持 SNP 的 QEMU。原版 QEMU 不支持 SNP,因此需要构建 AMD 的 分支: git clone https://github.com/AMDESE/qemu.git cd qemu && git checkout snp-latest mkdir build && cd build ../configure --target-list=x86_64-softmmu && make -j$(nproc) - 从 https://github.com/AMDESE/AMDSEV/releases 获取 SNP OVMF 固件。 客户机: - 任何可以在 SNP 下启动的 Linux 客户机,并安装了 `build-essential` 和 `linux-headers-$(uname -r)`,以便你能在其中构建模块。 ## 漏洞详情 SEV-SNP 客户机通过 GHCB(一个 4 KB 的共享页)与宿主机通信。PSC 请求将 `SW_EXITCODE` 设置为 `SVM_VMGEXIT_PSC` (0x80000010),将 `SW_SCRATCH` 指向一个描述符,并将描述符长度放入 `SW_EXITINFO2` 中。 描述符是一个 `struct psc_buffer`:一个 8 字节的头部,后跟一个 8 字节条目的数组。这里没有显式的计数字段。宿主机从 `hdr->cur_entry` 到 `hdr->end_entry` 处理条目,这两者都是由客户机控制的。 ``` struct psc_hdr { u16 cur_entry; u16 end_entry; u32 reserved; } __packed; /* 8 bytes */ struct psc_entry { u64 cur_page : 12; u64 gfn : 40; u64 operation : 4; u64 pagesize : 1; u64 reserved : 7; } __packed; /* 8 bytes */ ``` GHCB v2+ 的客户机应该将其暂存区(scratch area)保留在 GHCB 的 2032 字节共享缓冲区内,以便宿主机可以重用其现有的映射。`(2032 - 8) / 8 = 253` 个条目可以放入其中,这也是协议最大值 `VMGEXIT_PSC_MAX_COUNT` (253) 的来源。这个数字只有在缓冲区确实是共享缓冲区时才有意义。 如果客户机将暂存区指向 GHCB 之外,宿主机就无法使用其映射,因此 `setup_vmgexit_scratch()` 会分配一个单独的、大小为客户机所要求尺寸的缓冲区。SNP 不应该走这条路径,但没有什么能阻止它: ``` scratch_va = kvzalloc(len, GFP_KERNEL_ACCOUNT); /* len == exit_info_2, guest-controlled */ ``` `len` 直接来自客户机,而 `GFP_KERNEL_ACCOUNT` 会将分配放入计入 cgroup 的 `kmalloc-cg-N` 缓存中。如果请求 `exit_info_2 = 24`,你将得到一个位于 32 字节的 `kmalloc-cg-32` 槽位中的 24 字节分配:空间只够容纳头部加上两个条目。在 `entries[1]` 之后的所有内容都属于另一个对象的内存。 然后 `snp_begin_psc()` 会根据协议常量检查条目数量,而不是根据它实际分配的缓冲区: ``` idx_end = hdr->end_entry; if (idx_end >= VMGEXIT_PSC_MAX_COUNT) { /* checks 253, NOT the buffer size */ snp_complete_psc(svm, ...); return 1; } for (idx = idx_start; idx <= idx_end; idx++) { entry_start = entries[idx]; /* OOB once idx >= 2 */ ... } ``` 对于一个 24 字节的缓冲区,只存在两个条目,但该检查却允许 `end_entry` 达到 252。将其设置为 252,循环就会越过分配区域大约 2 KB,跨越相邻的 slab 对象。 ## 原语 每越过末尾一步,就会将接下来的 8 字节 slab 内存重新解释为 `psc_entry`,并通过 PSC 代码运行它。这会产生三个结果: 1. **读取预言机。** 宿主机读取相邻的 qword 仅仅是为了解码它,从而从缓冲区从未拥有过的内存中提取出 `entry.gfn` 和 `entry.operation`。这就是 KASAN 捕获到的 slab 越界读取。 2. **受限写入。** 如果解码后的条目看起来有效并作为 `KVM_HC_MAP_GPA_RANGE` 分发,完成代码会写回到同一个越界槽位:`entries[idx].cur_page = entry.pagesize ? 512 : 1`。在客户机选择的 word 的低 12 位中写入两个小值之一,并且可重复执行。 3. **失败预言机。** 如果条目未能通过验证,`SW_EXITINFO2` 中的响应会报告它停止的索引。每次将 `end_entry` 增加一个,就会逐个槽位地泄露相邻内存是解码为无操作还是失败,这足以找到对象边界并区分零和非零值。 每次 VMGEXIT 都会重新分配暂存缓冲区,因此重复的请求会落在不同的 freelist 槽位中,让客户机可以扫过相邻对象,而不是被困在一个对象上。综合起来,这会产生堆布局泄露、上述的受限写入以及跨请求的 use-after-free。 ## PoC 的执行流程 `trigger.c` 是一个客户机内核模块。在 SEV-SNP 客户机中加载它,它会通过单个 `insmod` 针对宿主机驱动四个阶段: - **阶段 1** 每次探测 48 个越界条目,并构建宿主机堆的映射(零与非零相邻内存的对比)。 - **阶段 2** 通过将 `cur_page` 写入一个零相邻项并确认后续请求跳过它,证明 OOB 写入在多次 VMGEXIT 之间持久存在。 - **阶段 3** 发送一个 `end_entry=200` 的请求,并测量在遇到非零数据之前 OOB 读取能延伸多远。 - **阶段 4** 发送 200 个请求,其中 `entries[3..10]` 越界,每一个都会在宿主机上触发一次 KASAN 报告。 该模块分配一个页,使用 `set_memory_decrypted()` 将其标记为已解密,将其用作暂存区,并手工构建 GHCB PSC 请求。它以 `-EAGAIN` 退出,因此不会保持加载状态。 ## 构建与运行 ### 1. 启动 SNP 客户机 根据你的环境调整 OVMF 和磁盘路径: ``` qemu-system-x86_64 \ -enable-kvm -cpu EPYC-v4 \ -machine q35,confidential-guest-support=sev0,memory-backend=ram1 \ -object memory-backend-memfd,id=ram1,size=4G \ -object sev-snp-guest,id=sev0,cbitpos=51,reduced-phys-bits=1,policy=0x30000 \ -smp 4 -m 4G \ -bios OVMF_SNP.fd \ -drive file=guest.qcow2,format=qcow2,if=virtio \ -netdev user,id=net0,hostfwd=tcp::2222-:22 \ -device virtio-net-pci,netdev=net0 \ -nographic ``` ### 2. 在客户机中构建并加载 将 `trigger.c` 和 `Makefile` 复制到客户机中,然后执行: ``` make insmod trigger.ko ``` 该模块首先通过 CPUID 检查是否存在 SEV-SNP,如果在其他环境中运行则会拒绝执行。它运行其四个阶段并自行卸载(init 返回 `-EAGAIN`,因此它永远不会驻留)。 ### 3. 观察宿主机 在宿主机上执行: ``` dmesg | grep -E "KASAN|BUG|snp_begin_psc" ``` 预期输出: ``` BUG: KASAN: slab-out-of-bounds in snp_begin_psc+0x126/0x890 Read of size 8 at addr ffff888219ffb5e0 by task qemu-system-x86/2199 BUG: KASAN: slab-out-of-bounds in snp_begin_psc+0x468/0x890 Write of size 8 at addr ffff888351566648 by task qemu-system-x86/2199 The buggy address belongs to the object at ffff888XXXXXXXXX which belongs to the cache kmalloc-cg-32 of size 32 ``` 在测试宿主机上,单个 `insmod` 产生了 73 份 KASAN 报告(62 个 slab 越界、7 个 slab use-after-free、4 个 use-after-free),全部针对 `kmalloc-cg-32`。测试宿主机:AMD EPYC 7443P,Ubuntu 24.04.4,带有 KASAN 的内核 6.11.11,运行在 AMDESE QEMU(`snp-latest`)下的客户机。 ## 修复方案 上游修复在 `setup_vmgexit_scratch()` 中拒绝了 GHCB v2 及更高版本的任何 GHCB 外部暂存区,这将缓冲区固定为一个固定、已知的大小,从而使得循环永远不会越过末尾运行: ``` } else { + /* GHCB v2 requires the scratch area to be within the GHCB. */ + if (to_kvm_sev_info(svm->vcpu.kvm)->ghcb_version >= 2) + goto e_scratch; + /* * The guest memory must be read into a kernel buffer, so * limit the size ``` 这四行代码就是 `db3f219`。它们作为一个更大系列的一部分被合并,该系列还根据实际缓冲区大小限制了条目计数,并通过 `READ_ONCE()` 重新读取了一次描述符,这弥补了同一处理程序中的缓冲区偏移变体以及 time-of-check/time-of-use 竞争条件。 ## 文件 | 文件 | 描述 | |---|---| | `trigger.c` | 驱动四个 PoC 阶段的客户机内核模块 | | `Makefile` | 根据正在运行的客户机内核构建 `trigger.ko` | | `LICENSE` | GPL-2.0,与模块的 `MODULE_LICENSE` 相匹配 | ## 故障排除 - `not an SEV-SNP guest`:QEMU 启动时未带有 `sev-snp-guest`,或者宿主机 SNP 已关闭。 - QEMU `SEV-SNP not supported`:检查 `/sys/module/kvm_amd/parameters/sev_snp`、BIOS 设置以及启动参数。 - QEMU `LAUNCH_START failed`:PSP 未初始化。检查 `dmesg | grep psp` 和 `CONFIG_CRYPTO_DEV_SP_PSP=y`。 - 没有 KASAN 输出:验证宿主机命令行上是否启用了 `CONFIG_KASAN=y` 和 `kasan_multi_shot`。 ## 警告 这会破坏宿主机内核堆内存,触发 KASAN,并可能导致宿主机崩溃。请仅在你自己控制的一次性测试宿主机上、属于你自己的 VM 中运行它。请勿在共享或生产环境的基础设施上运行。 ## 参考 - 分析文章:https://cyberstan.co.uk/sev-snp-oob/ - CVE-2026-53360(在 linux-cve-announce 上追踪提交 `db3f219`) - 修复:`db3f219`,作者 Mike Roth,审核人 Tom Lendacky,提交者 Paolo Bonzini - 引入版本:`9b54e248d264`;修复标记为 `Fixes: 4af663c` - GHCB 规范,第 2.1 节(SW_SCRATCH 必须位于 GHCB 共享缓冲区内) ## 许可证 `trigger.c` 为 GPL-2.0,与其 `MODULE_LICENSE` 相匹配。请参阅 [LICENSE](LICENSE)。
标签:CVE-2026-53360, KVM, Maven, PoC, Web报告查看器, 内核漏洞, 客户端加密, 暴力破解, 漏洞验证, 虚拟化逃逸, 身份验证强制