|
1343| 2
|
[Android 原创] 内核级内存读写的用户层检测与绕过:一次攻防对抗实践 |
发表于 2026-6-8 17:15
发现:
3.5 方法五:CPU 缓存时序检测(Flush+Reload ★物理直读检测)这是本项目最重要的发现:CPU 缓存时序检测是唯一能检测物理内存直读的用户层方法。 原理一:ARM 缓存是物理标记的ARMv8-A 架构的 CPU 缓存采用物理地址标记(Physically Tagged),具体有两种实现: PIPT(Physically Indexed, Physically Tagged):缓存行索引和标签都用物理地址。绝大多数 ARM L2/L3 缓存采用此设计。优势是完全不存在别名(aliasing)问题。 VIPT(Virtually Indexed, Physically Tagged):缓存行索引用虚拟地址的低位,标签用物理地址。ARM L1 数据缓存多采用此设计。对于满足 核心结论:无论 PIPT 还是 VIPT,标签比较用的都是物理地址。不同虚拟地址(VA_A 和 VA_B)映射到同一物理地址(PA)时,缓存控制器都能通过物理标签匹配到同一缓存行。这是整个检测方法的硬件基础。
原理二:KPM 的可缓存内存映射KPM 模块通过
ARM64 将内存类型分为三类:
如果 KPM 使用 原理三:缓存一致性协议(MESI/MOESI)ARM 多核 SoC 通过 AMBA CHI(Coherent Hub Interface)或 CCI(Cache Coherent Interconnect)实现跨核缓存一致性。经典 MESI 状态转换:
关键:KPM 的
三种命中延迟都远低于 DRAM 访问,这就是缓存时序检测的物理基础。 原理四:DC CIVAC 与 Point of CoherencyARM 架构定义了多个"点"来描述缓存操作的范围:
为什么用 CIVAC 而不是 CVAU? CVAU 只清洗到 PoU(本 Cluster 的 L2)。另一个 Cluster 的核可能还持有该缓存行的副本,防御进程切换到该核时可能误判为"命中"。CIVAC 确保全局可见性。 DSB ISH 的必要性: 原理五:信号放大——为什么遍历全部 64 个缓存行
单次访问的计时困境:
64 次访问的信号放大:
64 次访问将信号放大了 64 倍,使不同缓存层级之间的差异从无法区分变为 显著可区分(2-3 vs 31 vs 123 ticks)。这正是遍历页面全部 64 个缓存行的数学基础。 通过延时放大信号: 除了空间放大(64 个缓存行),还可以通过时间放大。在测量前通过
为什么每页恰好 64 个缓存行? 缓存行大小是 ARM 架构定义的(通常 64 字节),4KB 页面 / 64 字节 = 64 个缓存行。这不是巧合,而是页面大小和缓存行大小定义的结果。 Flush+Reload 完整流程
从测量值反推缓存层级实测数据验证了各缓存层级的理论计算值:
ARM64 汇编实现
关键参数:阈值的选择
阈值设为 50 ticks:能捕获 L1/L2 命中和跨核缓存一致性传输,同时过滤 L3 和 DRAM 访问。 高频探测循环为提高捕获概率,每个检测轮次内执行 60 次快速探测(每 5ms 一次):
60 轮 × 5ms = 300ms 总探测窗口,攻击每 20-50ms 运行一次,保证至少数次重叠。 攻击端配合:整页读取关键发现:必须让 KPM 模块读取整页(4096 字节),而非 8 字节:
实测效果攻击使用物理直读(PRCTL_MEM_READ, 整页 4096 字节)时:
数据验证:防御写入可识别模式 四、攻击端:从
|
| 测试模式 | prctl 命令码 | 内核实现 | 内存类型 | 缓存行为 |
|---|---|---|---|---|
| 无任何读取 (基线) | — | — | — | 纯防御端自检,无攻击 |
| 有缓存读取 (WB) | 0x4D454D01 | 手动页表遍历 + memremap WB | Normal WB | 高性能吞吐,经过 L1/L2/SLC 全部缓存(性能优先,非检测规避) |
| 无缓存读取 (WC) | 0x4D454D04 | 手动页表遍历 + memremap WC | Normal NC | 绕过 L1/L2,SLC 仍可能命中(规避缓存时序检测) |
| DMA 读取 (Device) | 0x4D454D06 | 手动页表遍历 + vmap Device-nGnRnE | Device-nGnRnE | 绕过全部缓存 L1/L2/SLC(终极规避) |
实验环境:M2011K2C(Qualcomm Snapdragon ARMv8-A),Linux 5.4.210-qgki。防御端检测周期 5 秒/轮,每轮含 60 次缓存探测(Flush+Reload,5ms间隔)。攻击端 ./attack <PID> 3 <mode>,3 轮攻击,每轮 58 次整页读取(~232KB/轮)。
实验流程:每种模式独立运行——启动 defense -> 等待初始化(~3s)-> 启动 attack(基线跳过)-> 运行 15-20s -> 终止进程 -> 收集日志。
为确保攻击/防御目标地址严格一致,本轮测试改为精确地址模式:防御端初始化后输出 8 个缓存探测页地址,攻击端从文件读取这些地址并循环反复读取(50 次/轮,5ms 页间延迟),每轮持续 ~2 秒覆盖防御端全部 300ms 探测窗口。
攻击配置: ./attack <PID> 3 <mode> 5 <addr_file>
精确地址: 8 个缓存探测页 (防御端输出的 CACHE_PROBE_ADDRS)
页间延迟: 5ms (与防御探测窗口对齐)
循环次数: 50 次/轮 (每轮 400 次读取, 1.6MB, 持续约 2 秒)
测试 1 — 无任何读取(基线)
========== 第 1~5 轮 ==========
[OK] 方法2(PMU): 硬件计数器正常 ← REMOTE_ACC=0, BUS_ACCESS=0
[OK] 方法3(Canary): 金丝雀完好
[OK] 方法4(PerfX): stat:+0 perf:+0 ext:+0 (正常)
[OK] 方法5(Cache): 60 轮探测无缓存命中 (正常) ← 300 次探测全部零命中
基线干净:5 轮 × 60 次 = 300 次缓存探测全部零命中,无 PMU 假阳性。
测试 2 — 有缓存读取(WB)
========================================
内核驱动内存读取 - 攻击端
======================================== 页间延迟: 5 ms 读取模式: WB (有缓存) 从文件加载 8 个精确地址 (跳过 /proc/pid/maps 扫描) 第 1 轮完成: 400 次读取, 1638400 字节 (50 次循环)
防御端检测 (第 1 轮):
========== 第 1 轮 ==========
[OK] 方法2(PMU): 硬件计数器正常
[OK] 方法3(Canary): 金丝雀完好
[OK] 方法4(PerfX): stat:+0 perf:+0 ext:+0 (正常)
*** 第 0 轮探测: 5/8 页缓存命中! *** <- 初始化噪音
*** 第 0~58 轮探测: 各 1/8 页命中 *** <- 攻击期间连续命中!
[!!! 告警] 方法5(Cache): 58 次命中 (跨 58 轮探测) ← 58/60 = 96.7%
========== 第 2~4 轮(攻击已结束)==========
[OK] 全部方法正常
测试 3 — 无缓存读取(WC,memremap WC 物理直读,已移除 WB 回退)
攻击端 (attack WC 模式):
[WC] 0x73d03a7000 -> 0xcccccccccccccccc (OK)
第 1 轮: 400 次读取, 1638400 字节 (50 次循环)
第 2 轮: 500 次读取, 2048000 字节 (50 次循环)
防御端检测 (第 1 轮, 攻击活跃期):
========== 第 1 轮 ==========
[OK] 方法2(PMU): 硬件计数器正常
[OK] 方法3(Canary): 金丝雀完好
[OK] 方法4(PerfX): stat:+0 perf:+0 ext:+0 (正常)
*** 第 0 轮探测: 7/8 页缓存命中! *** <- 冷启动一次性噪声
[!!! 告警] 方法5(Cache): 7 次命中 (跨 1 轮探测)
========== 第 2~5 轮(攻击活跃期)==========
[OK] 方法5(Cache): 60 轮探测无缓存命中 (正常) ← 连续 4 轮 240 次探测零命中!
对比:移除 WB 回退前后
移除前 (有 WB 回退) 移除后 (无 WB 回退)
WC 缓存命中 37/60 = 61.7% ~0/60 = ~0% (仅冷启动)
综合检测率 ~62% ~0%
memremap(MEMREMAP_WC) 失败时 KPM 静默回退到 WB,这些 WB 读取数据进入 L1/L2,被 Flush+Reload 检出。移除回退后,WC 映射的 Normal NC 内存虽在 SLC 中分配,但用户态 Flush+Reload 无法可靠区分 SLC 命中/未命中的延迟差异,WC 实际不可检测测试 4 — DMA 读取(Device-nGnRnE,绕过全部缓存)
攻击端 (attack DMA 模式):
[DMA] 0x7393e82000 -> 0xcccccccccccccccc (OK)
第 1 轮完成: 400 次读取, 1638400 字节 (50 次循环)
防御端检测:
========== 第 1~5 轮 ==========
[OK] 方法2(PMU): 硬件计数器正常
[OK] 方法3(Canary): 金丝雀完好
[OK] 方法4(PerfX): stat:+0 perf:+0 ext:+0 (正常)
[OK] 方法5(Cache): 60 轮探测无缓存命中 (正常) ← 300 次探测全部零命中!
测试模式 Pagemap PerfEvent PMU REMOTE 缓存时序(子探测) 综合检测率
──────────────────────────────────────────────────────────────────────────────────
无任何读取 (基线) OK OK OK 0/60 (0%) 0% (基线)
有缓存读取 (WB) OK* OK* OK 58/60 (96.7%) ~97%
无缓存读取 (WC) OK* OK* OK 0/60 (0%) ¹ ~0%¹
DMA读取 (Device) 绕过 绕过 绕过 0/60 (0%) 0%
──────────────────────────────────────────────────────────────────────────────────
* Pagemap/PerfEvent 仅对 access_process_vm 触发的缺页有效; 物理直读不触发缺页故 ext:0
¹ 移除 WB 回退后重新测试结果。之前 61.7% 由回退路径导致, 仅第 0 轮有冷启动噪声
KPM 模块的 WC 读取路径曾包含显式的 WB 回退逻辑:
// 已移除的回退代码(修复前)
void *mapped = my_memremap(page_phys, 4096, MEMREMAP_WC);
if (!mapped) {
mapped = my_memremap(page_phys, 4096, MEMREMAP_WB); // WC 失败 → WB
}
// 修复后:memremap 失败直接返回错误,不再静默回退
memremap(MEMREMAP_WC) 的 ARM64 实现路径为 ioremap_prot(phys, size, PROT_NORMAL_NC)。在 Linux 5.4 上,__ioremap 调用 arch_memremap_can_ram_remap(phys_addr, size, flags) 检查物理地址是否为 RAM。
假说(修复前):在测试设备(Qualcomm Snapdragon, Linux 5.4.210)上,WC 模式 61.7% 的命中率有两种可能:
arch_memremap_can_ram_remap 拒绝 NC 映射 → KPM 回退到 WB → 实际运行可缓存模式验证实验:从 Kernel_prctl.c 中移除 read_mem_phys_direct()、write_mem_phys_direct() 和 read_mem_dma() 三处 WB 回退逻辑,重新编译 KPM 模块并测试。
实验结果:移除 WB 回退后,WC 模式检测率从 61.7% 骤降至 ~0%(仅剩第 0 轮冷启动 7/8 命中的一次性噪声,后续 4 轮 × 60 次 = 240 次探测全部零命中)。
结论:假说 B 成立。之前的 WC 模式 61.7% 检测率完全由 WB 回退路径导致。当 memremap(MEMREMAP_WC) 在测试设备上返回 NULL 时,KPM 静默降级为 WB,数据进入 L1/L2 被 Flush+Reload 检出。去除回退后,WC 的 Normal NC 映射虽在 SLC 中分配缓存行,但用户态 Flush+Reload 无法可靠区分 SLC 命中/未命中的延迟差异(差异远小于 L1 命中 vs DRAM),WC 实际与 DMA 一样不可检测。
1. 页表遍历的缓存污染: walk_page_table() 的 PMD/PTE 读取使用 MEMREMAP_WB(仅数据页用了 WC),页表物理页被加载到 CPU 缓存。此外 TLB 硬件页表遍历器在 TLB miss 时通过缓存访问页表,可能预热相关缓存组。
2. 系统缓存(SLC/L3)的不可绕过性: MEMREMAP_WC(Normal Non-Cacheable)只绕过 L1/L2,仍在 SLC 中分配。Qualcomm Snapdragon 上 PoC 位于 SLC 之后,DC CIVAC 刷到 PoC 意味着 SLC 中的数据被保留。防御核后续访问可能从 SLC 获取数据(~30-50 cycles),而非 DRAM(~200 cycles),64 行总时间约 31-50 ticks,恰好低于或接近阈值 50。
ARM SoC 缓存层级
┌─────────────────────────────────┐
│ Core 0 Core 1 │
│ L1I$ L1D$ L1I$ L1D$ │ <- WC 绕过此层
│ Shared L2 Shared L2 │ <- WC 绕过此层
└───────┬────────────┬────────────┘
│ │
┌───────▼────────────▼────────────┐
│ CCI/CHI Interconnect │
│ ┌──────────────────────────┐ │
│ │ System Level Cache (SLC) │ │ <- WC 无法绕过!
│ │ (L3, 2-8 MB) │ │
│ └──────────────────────────┘ │
└───────────────┬─────────────────┘
│
┌───────────────▼─────────────────┐
│ DRAM Controller │ <- PoC 通常在此
└─────────────────────────────────┘
3. 缓存一致性协议的嗅探效应: 即使 WC 不在本地 L1/L2 分配,缓存一致性协议的 snoop 机制仍使持有数据的 SLC 响应并提供数据。防御核的 DC CIVAC 清洗后访问 -> 攻击核的 NC 读取触发 snoop -> SLC 向防御核提供数据 -> 形成"被动共享",导致低于阈值的快速访问。
完全消除残余命中的方向:
| 方法 | 原理 | 代价 |
|---|---|---|
| 页表遍历也用 WC | 消除页表页缓存污染 | 遍历性能大幅下降 |
| 读取后 DC CIVAC 自清洗 | 主动逐出 SLC 中的数据 | 增加 KPM 模块指令开销 |
| 使用 Device 内存类型 (nGnRnE) | 完全绕过所有缓存和写缓冲 | vmap 复杂度增加(已在 v4.0 实现) |
| 跨 Cluster 攻击 | 利用不同 Cluster 的独立 SLC | 需绑定 CPU 亲和性 |
防御端在探测页中写入可识别魔数模式(0xcccc...),攻击端通过三种 prctl 路径均正确读出,确认各读取路径功能正常:
模式 防御写入 攻击读出 验证
─────────────────────────────────────────────────────────────────────────
WB 0x79a74db000: 0xcccccccccccccccc -> 0xcccccccccccccccc OK
WC 0x7556a80000: 0xcccccccccccccccc -> 0xcccccccccccccccc OK
DMA 0x7327288000: 0xcccccccccccccccc -> 0xcccccccccccccccc OK
原理:调用 Linux 内核标准函数 access_process_vm(target_task, addr, buf, len, 0),内核代为完成 VA→PA 转换和页表遍历。
读取链路:
用户态 attack 内核态
│ │
├── prctl(PRCTL_MEM_READ) ──→ before_prctl hook
│ │
│ access_process_vm(target_mm, va, buf, 4096, 0)
│ ├── get_user_pages() ← 触发缺页, 建立页表映射
│ ├── kmap(page) ← 创建临时内核 VA (Normal WB)
│ ├── __copy_to_user() ← 数据经 L1/L2 缓存
│ └── put_page() ← 释放引用
│ │
│ ←── 返回 4096 ──┤
│
└── 用户态收到数据 ←───────────────────┘
内存类型:kmap() 创建的内核映射使用 Normal Cacheable Write-Back(与用户态进程 VA 属性一致),数据必然经过 L1/L2 缓存,且触发缺页修改 PTE Accessed/Dirty 位。
| 检测表面: | 检测方法 | 是否检出 | 原因 |
|---|---|---|---|
| Pagemap Present | ✓ 检出 | get_user_pages() 触发缺页, PTE 从 swap/zero-page 变为 present |
|
| PerfEvent ext: | ✓ 检出 | 缺页事件被 perf_event 记录 | |
| PMU 计数器 | 部分 | mem_access / bus_access 计数增加 |
|
| Canary | ✗ 不检出 | 仅读取不修改 | |
| Cache Timing | ✓ 检出 | WB 属性使数据留在 L1/L2, Flush+Reload ~95%+ |
小结:access_process_vm 是标准的、文档化的内核接口,功能完备但留下大量可检测痕迹,不适合隐蔽读取。
原理:不再调用 access_process_vm,改为手动遍历目标进程的 ARM64 页表获取物理地址,然后用 memremap(phys, MEMREMAP_WB) 创建临时内核映射直接读取。
读取链路:
用户态 attack 内核态
│ │
├── prctl(PRCTL_MEM_READ) ──→ before_prctl hook
│ │
│ ┌─ ① 获取目标进程 mm_struct → pgd
│ ├─ ② walk_page_table(): 手动遍历 PGD→PMD→PTE → 物理地址
│ ├─ ③ memremap(phys, 4096, MEMREMAP_WB) → 临时内核 VA
│ ├─ ④ memcpy(kernel_buf, mapped_va, 4096) → 数据经 L1/L2
│ ├─ ⑤ memunmap(mapped_va) → 释放临时映射
│ └─ ⑥ copy_to_user(user_buf, kernel_buf) → 送回用户态
│ │
│ ←── 返回 4096 ──┤
关键改进 — 绕过 Pagemap:手动遍历目标进程的页表(而非 attack 自己的页表),直接读取 PGD→PMD→PTE 中的物理地址。整个过程不调用 get_user_pages(),因此不触发缺页,PTE 的 Accessed/Dirty 位保持不变,defense 端的 /proc/self/pagemap 看不到任何变化。
关键改进 — 绕过 PerfEvent:没有缺页事件,perf_event_open() 的 ext 计数器始终为 0。
仍可检测的原因 — WB 缓存:memremap(WB) 的内核映射属性为 Normal Cacheable Write-Back,memcpy 读取的数据被分配在 L1/L2 缓存中。防御端的 Flush+Reload 探测这些缓存行时发现命中,产生告警。
原理:将单次读取量从 8 字节扩大到 4096 字节(一整页 = 64 条缓存行),每条缓存行 (64B) 都被加载到 L1/L2,大幅扩大缓存"指纹"。
v2.0: 读取 8 字节 → 污染 1 条缓存行 → 防御端需恰好探测这一条
v2.1: 读取 4096 字节 → 污染 64 条缓存行 → 防御端探测任意一条都能命中
为什么需要信号放大:攻击端也不知道防御端具体探测哪个地址。8 字节读取只污染单个缓存行,防御端 8 个探测页中有 7 个可能完全不受影响。整页读取确保每个探测页的 8 个偏移位置(8×8=64 条缓存行)全部被污染,命中率从随机提升到 ~80%+。
这一版本本身是攻击端的退步(更容易被检测),但验证了缓存时序检测的物理原理——这条检测链是后续所有规避设计的基础。
原理:MEMREMAP_WC (Write-Combine) 在 ARM64 上映射为 Normal Non-Cacheable 内存。与 Normal WB 的关键区别在于 MMU 页表条目的 MAIR 属性索引不同:
ARM64 PTE 内存类型编码 (bits [7:2]):
Normal WB: MAIR[0] = 0xFF (Inner WB-WA / Outer WB-WA) → 经 L1/L2/SLC
Normal NC: MAIR[1] = 0x44 (Inner NC / Outer NC) → 绕过 L1/L2, SLC 仍命中
Device-nGnRnE: MAIR[2] = 0x00 (Device) → 绕过全部缓存
Normal NC 的缓存行为:
┌──────────────┐
│ L1 Cache │ ← Normal NC 绕过此行 (Inner NC)
├──────────────┤
│ L2 Cache │ ← Normal NC 绕过此行 (Outer NC)
├──────────────┤
│ SLC / L3 │ ← Normal NC 仍命中! ARM 架构规定 Normal 内存可被系统缓存透明缓存
├──────────────┤
│ DRAM │
└──────────────┘
ARM 架构规范 (ARMv8-A ARM D13.2.111) 明确:所有 Normal 类型的内存(包括 Non-Cacheable)均可被系统级缓存(SLC)透明缓存。这意味着即使 PT 标记为 NC,硬件仍可能在 SLC 中分配缓存行。这是 ARM 不同于 x86 的关键行为差异。
代码路径:
// prctl hook handler
case PRCTL_MEM_READ_SAFE:
result = read_mem_phys_direct(pid, addr, buf, size, MEMREMAP_WC);
// ↑
// 内核调用 ioremap_prot(phys, size, PROT_NORMAL_NC)
// → ARM64 PTE: AttrIndx 指向 MAIR[1] = 0x44 (NC)
问题:v3.0 的 read_mem_phys_direct() 包含静默降级逻辑:
void *mapped = my_memremap(page_phys, 4096, MEMREMAP_WC);
if (!mapped) {
// 静默回退! 映射失败时自动降级为 WB
mapped = my_memremap(page_phys, 4096, MEMREMAP_WB);
}
当 arch_memremap_can_ram_remap() 拒绝 RAM 页的 NC 映射时,memremap 返回 NULL,KPM 自动回退到 WB。攻击者以为自己在用 WC(规避模式),实际运行的是 WB(可检测模式)。
修复:三处回退全部移除(read_mem_phys_direct、write_mem_phys_direct、read_mem_dma),映射失败直接返回错误,零容忍静默降级。
影响:修复使 WC 检测率从 61.7% 骤降至 ~0%,证实之前的检出完全来自回退路径。
原理:使用 vmap() 对目标物理页创建独立的虚拟地址映射,页表条目标记为 Device-nGnRnE 内存类型。vmap 直接操作页表,不经过 arch_memremap_can_ram_remap() 检查,因此可对任意物理页(包括 RAM)创建 Device 映射——这是 ioremap 做不到的。
为什么不用 ioremap:
// ioremap 路径 — 对 RAM 会失败
__ioremap(phys, size, PROT_DEVICE_nGnRnE)
└── arch_memremap_can_ram_remap(phys, size, MEMREMAP_WB)
→ 检测到 phys 是 RAM → 返回 0 → ioremap 返回 NULL
// vmap 路径 — 无 RAM 检查
vmap(page, 1, VM_MAP, pgprot_device)
└── 直接分配 vmalloc 区域 VA → 填充 PTE → 无 RAM 检查 ✓
读取链路:
用户态 attack 内核态
│ │
├── prctl(PRCTL_MEM_READ_DMA) ──→ before_prctl hook
│ │
│ ┌─ ① walk_page_table() → 物理地址
│ ├─ ② pfn = phys >> 12
│ ├─ ③ page = vmemmap_base + pfn * sizeof(page)
│ ├─ ④ dev_map = vmap(&page, 1, VM_MAP, 0x00E8000000000703)
│ │ └── 在 vmalloc 区创建独立 VA
│ │ └── PTE: AttrIndx[5:2]=0000, bits[7:6]=00 → Device-nGnRnE
│ ├─ ⑤ volatile 逐字节读取 ← Device 总线事务, 不分配缓存行!
│ └─ ⑥ vunmap(dev_map) → 释放 vmalloc 映射
│ │
│ ←── 返回 4096 ──┤
Device-nGnRnE 协议级分析:
| 属性 | 全称 | 含义 | 对检测的影响 |
|---|---|---|---|
| nG | Non-Gathering | 不合并多次内存访问为单次总线事务 | 每次读取独立事务, 无 batching |
| nR | Non-Reordering | 不重排访问顺序 | 严格按程序顺序执行 |
| nE | No Early Ack | 无提前写入确认 | 写入等待到达最终目的地 |
缓存行为 — 全绕过:
┌──────────────┐
│ L1 Cache │ ← Device 事务绕过 (Inner Non-Cacheable + Non-Allocating)
├──────────────┤
│ L2 Cache │ ← Device 事务绕过 (Outer Non-Cacheable + Non-Allocating)
├──────────────┤
│ SLC / L3 │ ← Device 事务绕过! SLC 不缓存 Device 类型访问
├──────────────┤
│ DRAM │ ← Device 事务直接到达, 不经任何缓存层
└──────────────┘
ARM 架构规定 Device 内存不可被任何缓存层级缓存(ARMv8-A ARM D13.2.111),这是硬件保证,不同于 Normal NC 的 "可被 SLC 透明缓存"。
vmap 页表操作细节:
ARM64 PTE 中控制内存类型的字段:
PTE bits [63:12] 输出物理地址
PTE bits [11:3] AF, SH, AP, NS, AttrIndx
PTE bits [7:6] = 00 → Device 内存 ✓
PTE bits [5:2] = 0000 → AttrIndx, 指向 MAIR[0] 即 Device-nGnRnE
PTE bits [1:0] = 11 → 有效页表项 (block/page descriptor)
prot 值 0x00E8000000000703 解码:
0x00E8 0000 0000 0703
= 0000 0000 1110 1000 0000 ... 0000 0111 0000 0011
Bits [63:56]: 0x00 = Upper attributes (UXN/PXN = 0, 用户不可执行/内核不可执行 = 否)
Bits [55:50]: 0b111010 = 软件定义/保留位 (ARM64 特定编码)
Bits [7:6]: 0b00 = Memory Type = Device
Bits [5:2]: 0b0000 = Device Type = nGnRnE
Bits [1:0]: 0b11 = Page Descriptor Valid
与 WC (v3.0) 的本质区别:
v3.0 WC (memremap WC) v4.0 DMA (vmap Device-nGnRnE)
映射方式 memremap + Normal NC vmap + Device-nGnRnE
ARM 内存类型 Normal Non-Cacheable Device-nGnRnE
L1/L2 绕过 ✓ 绕过 ✓
SLC/L3 仍命中 ✗ (架构必然) 绕过 ✓ (硬件保证)
RAM 检查 arch_memremap_can_ram_remap 无 (vmap 直接操作页表)
MESI/MOESI 参与 (SLC 内的共享状态) 不参与 (Device 事务不进入缓存域)
检测率 0% (无回退时) 0%
原始 Makefile 的环境变量 CC=cc(make 默认值)覆盖了交叉编译器检测逻辑。修复:
# 使用 override 强制覆盖 make 内置变量
override CC := $(CROSS_CC)
override CXX := $(subst gcc,g++,$(CC))
早期用 C 语言编写的测试程序直接调用 prctl(PRCTL_MEM_READ, &op, ...) 始终返回 -EINVAL(errno=22),但改用 C++ 的 c_driver 类后成功。关键在于:
__arch_copy_from_user 从用户态拷贝 mem_operation 结构体pid_t(32-bit)和 uint64_t 的结构体可能有不同的隐式填充处理kernel_driver.h 中的 c_driver::readMemSafe() 方法正确对齐了结构体最初的 Soft-Dirty 检测流程是「先清除,后检查」:
clear_soft_dirty() → 清除所有标记
usleep(10000) → 等待
check_page() → 检查 ← 永远为 0!
实际攻击发生在两次检测轮次之间,标记在下一轮被 clear_soft_dirty() 清除。修复为「先检查,后清除」。
CONFIG_MEM_SOFT_DIRTY 未开启测试设备的 Linux 5.4 内核未开启 CONFIG_MEM_SOFT_DIRTY,/proc/self/pagemap bit 55 始终为 0,clear_refs 写入成功但无效果。替代方案使用 /proc/self/pagemap bit 63(present bit)+ MADV_DONTNEED,不依赖任何内核配置。
CPU 缓存时序检测经历了多次迭代才最终成功,每次迭代都解决了一个关键问题。
第一代:单次 8 字节读取 + 低阈值
初始方案模仿经典的 Flush+Reload:每次测量访问 64 个缓存行中一个字节的时间。但攻击端只读取 8 字节(1 个 unsigned long),KPM 模块的 memcpy 只加载 1 个缓存行。防御端遍历全部 64 个缓存行时,1 命中 + 63 未命中 ≈ 120+ ticks,远超基于 L1 命中的阈值(3-12 ticks)。
第二代:高频探测 + usleep 让出 CPU
改用忙等(busy-wait)保持 CPU 占用 → 攻击进程完全没有机会运行。修复为 usleep 主动让出 CPU,攻击进程在 5ms 窗口内获得调度。
第三代:整页读取
改为每次 readMemSafe(target_pid, addr, buf, 4096) 读取整页,KPM 的 memcpy 加载全部 64 个缓存行。防御端测量 64 行全部命中 → 时间大幅降低。
第四代:跨核阈值
最初的阈值设为 hit × 3(约 3-12 ticks),只覆盖本地 L1 命中。但攻击和防御运行在不同 CPU 核上,缓存一致性传输延迟约 50 cycles,64 行约 31 ticks,被误判为未命中。修复为固定阈值 50 ticks,成功覆盖跨核缓存一致性传输。
第五代:PID 传递修复
c_driver 构造函数中用 getPidByName("defense") 查找进程名,但独立测试程序名为 cache_test,导致 driver->pid = -1。修复为构造函数接收 PID 参数,由 main() 传入命令行 PID。
最终参数总结:
| 参数 | 旧值 | 新值 | 原因 |
|---|---|---|---|
| 每次读取大小 | 8 字节 | 4096 字节 | 加载全部 64 个缓存行 |
| 探测间隔 | 50ms/忙等 | 5ms/usleep | 让出 CPU + 更高频率 |
| 阈值 | hit×3 (3-12) | 50 (固定) | 覆盖跨核一致性传输 |
| 探测轮数 | 40 | 60 | 300ms 总窗口 |
| PID 获取 | 按名查找 | 命令行传入 | 适配不同进程名 |
KPM 框架在 pgtable.h 中声明了 linear_voffset、va_bits、page_level、page_shift 等外部变量,但 KPM 模块加载器不会解析这些符号:
[-] KP E unknown symbol: linear_voffset
[-] KP E unknown symbol: va_bits
解决:
linear_voffset → 改为直接读内核 VA 的 PGD 条目,完全不依赖 VA→PA 转换va_bits / page_level → 通过检查 init 进程的 PGD 条目结构运行时探测页表级数page_shift → ARM64 始终为 12(4KB)__virt_to_phys 不是内核导出符号ARM64 上 __virt_to_phys 和 virt_to_phys 都是内联宏而非可链接的函数符号。5 个所需符号中唯一解析失败的:
memremap=Y get_task_mm=Y mmput=Y access_process_vm=Y __virt_to_phys=N ←
解决:利用内核线性映射的特性,PGD 层级直接通过内核 VA 读取,无需物理地址转换。子级页表使用 memremap 映射 PTE 中的物理地址。
probe_mm_pgd_offset 最初使用 init 进程(pid=1)探测 mm_struct->pgd 偏移量,但 init 的所有 PGD 用户空间条目为 0(init 运行在内核模式)。
解决:扫描多个 PID(1000+),验证任意 PGD 条目(不仅第一个)中是否有有效页表项。
| 攻击方式 | Pagemap | PerfEvent | PMU REMOTE | 缓存时序 | Canary | 综合检测率 |
|---|---|---|---|---|---|---|
| 物理直读 (WB) | ✅ 绕过 | ✅ 绕过 | ⚠️ 偶发假阳性 | ✅ 检出 | — | ~78% |
| 物理直读 (WC) | ✅ 绕过 | ✅ 绕过 | ⚠️ 检出 | ✅ 绕过 | — | ~6% |
| 物理直读 (DMA Device) | ✅ 绕过 | ✅ 绕过 | ✅ 绕过 | ✅ 绕过 | — | ~0% |
| 写入攻击 | — | — | — | — | ✅ 检出 | — |
CPU 缓存时序检测(Flush+Reload)是目前最有效的物理内存直读用户层检测手段,其有效性完全取决于攻击端的内存映射类型:
MEMREMAP_WB(可缓存,设计目标为性能):缓存时序以 78% 命中率稳定检出。三条命令 (READ/SAFE/DMA) 均使用同一套手动页表遍历引擎,区别仅在于 memremap 内存类型——WB 提供最高吞吐性能,其缓存行为是 Normal Cacheable 内存的自然属性,并非设计缺陷。WC 和 Device 才是专门为规避缓存时序检测而引入的映射类型
MEMREMAP_WC(Normal Non-Cacheable,不可缓存):缓存时序命中率降至 6%(降幅 92%),仅 PMU REMOTE_ACCESS 仍可检出——但 PMU 在基线测试中存在偶发假阳性(+85),单独依赖不可靠
vmap Device-nGnRnE(DMA 硬件读取):全部 5 种用户层检测方法均无法检出(检测率 ~0%)。Device 类型总线事务完全旁路缓存层级,攻击侧在用户层检测面上取得完全优势
防御需向上迁移:在 DMA 模式下,唯一可行的检测方向是系统级监控——IOMMU/SMMU 审计捕获异常设备地址空间的页表映射操作,或总线级监控捕获 Device 类型事务的异常模式
攻击端从 v1.0 到 v4.0 的演进体现了内核攻击与用户层检测之间"攻高一尺,魔高一丈"的持续对抗:每一步攻击改进都绕过了一部分检测,最终 DMA 模式在现有用户层检测框架下形成完全不可检测的真空地带。
v1.0 KPM: access_process_vm (8字节读)
→ Pagemap + PerfEvent 完全检出(检测率 ~95%+)
v2.0 KPM: 物理直读 + 手动页表遍历 + MEMREMAP_WB (8字节读)
→ 绕过 Pagemap + PerfEvent,但缓存时序信号太弱
v2.1 KPM: 物理直读 + 整页读取 (4096字节, MEMREMAP_WB)
→ 绕过 Pagemap + PerfEvent
→ 缓存时序检测以 75-80% 命中率检出 → 攻防新平衡
v3.0 KPM: 物理直读 + MEMREMAP_WC (无缓存, Non-Cacheable)
→ 缓存时序降至 6%(92% 降幅),仅 PMU 可检出
→ 攻击侧首次取得显著优势
v4.0 KPM: 物理直读 + vmap Device-nGnRnE (DMA 硬件读取)
→ 全部 5 种用户层检测方法均无法检出(检测率 ~0%)
→ 攻击侧在用户层检测面上取得完全优势
→ 防御需向上迁移至系统级监控(SMMU/IOMMU 审计、总线监控)
MEMREMAP_WC — 消除页表页缓存污染(代价:遍历性能大幅下降)DC CIVAC 自清洗 — 主动逐出 SLC 中的数据(代价:增加指令开销)vmap Device-nGnRnE 映射可被 IOMMU 记录)arch/arm64/include/asm/pgtable.hhttps://github.com/bmax121/KernelPatchhttps://github.com/bmax121/APatchrwprocmem33_20250520/(get_task_proc_phy_addr)rwprocmem33_20250520/rwProcMem33-master/rwProcMem33Module/rwProcMem_module/phy_mem.h(get_task_proc_phy_addr、read_ram_physical_addr)access_process_vm 内核实现:mm/memory.c, mm/gup.c本文档随项目代码一起维护,文件路径:/home/zzz/Desktop/kernel_read_detect/article.md
发帖前要善用【论坛搜索】功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。
发表于 2026-6-9 15:24
| ||
|
发表于 2026-6-9 15:40
| ||
RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )
GMT+8, 2026-7-22 03:18
Powered by Discuz!
Copyright © 2001-2020, Tencent Cloud.