摘要 :本文记录了在 ARM64 Android 平台上,构建一个完整的内核驱动内存读写检测 Demo 的全过程。项目包含两个独立运行的程序——仅使用用户层权限的防御端 和拥有 root 权限对接内核驱动进行读写的攻击端 。防御端通过 5 种用户态手段检测外部内存访问,其中 CPU 缓存时序检测(Flush+Reload)是唯一能检测物理内存直读的方法 ;攻击端则从最初的 access_process_vm 被完全检测,演进为手动遍历 ARM64 页表的物理内存直接读取,最终成功绕过 Pagemap Present 和 PerfEvent 交叉通道检测,但仍被缓存时序方法检出。文章详细记录了每种检测方法的原理、实现、调试过程,以及 KPM(Kernel Patch Module)从 access_process_vm 到真正物理直读的进化历程。
在现代移动安全领域,攻击者常通过内核驱动(Kernel Driver)绕过用户空间的权限限制,直接读取或修改目标进程的内存。这类攻击具有极强的隐蔽性,因为传统用户态检测手段无法感知内核层的活动。
本文构建的 Demo 设定如下:
KPM(Kernel Patch Module)是 KernelPatch/APatch 框架下的内核模块系统。本项目使用一个 hook prctl 系统调用的 KPM 模块,通过自定义命令码实现内存读写:
用户态调用方式:
KPM 在 syscall_hook_demo_init 中通过 fp_hook_syscalln(__NR_prctl, 5, before_prctl, 0, 0) 注册 hook。当用户态调用 prctl 时,before_prctl 函数拦截并处理自定义命令,其他命令则透传给原始 prctl。
原始的 prctlhookRWMemory 模块中,PRCTL_MEM_READ 对应的 read_mem_phys 函数虽然名为 "phys",但实际上仍然调用 access_process_vm_safe():
真正的手动物理地址遍历代码(read_phys_addr_direct,使用 memremap 映射物理页)被整个注释掉了,从未编译进模块。
access_process_vm 是内核中用于跨进程内存访问的标准函数(也被 ptrace 和 /proc/pid/mem 使用)。它在内部调用 get_user_pages_remote():
核心发现:get_user_pages 在处理 non-present 页面时,会在目标进程的上下文中触发 minor page fault,分配物理页并设置 PTE。即使后续拷贝失败,这些副作用已经产生。
防御端实现了 5 种完全基于用户态权限的检测方法。所有方法均在防御端程序 defense.c 中实现,其中缓存时序检测另有独立的精简测试程序 cache_test.c。
原理 :
利用 madvise(MADV_DONTNEED) 将监控页变为 non-present 状态(/proc/self/pagemap bit 63 = 0),然后周期性检查该位。如果 access_process_vm 读取了这些页面,get_user_pages 会触发 handle_mm_fault,分配物理页并使 present bit 变为 1。
实现细节 :
关键时序 :必须是先检查,后清除 。早期的实现错误地先 clear_refs 再检查,导致攻击留下的证据在检查前被清除。修正后的流程:
验证实验 :通过直接测试确认了 access_process_vm 读取 DONTNEED 页面时,即使 prctl 返回失败(-EINVAL),仍触发了一次 minor fault 并使页面变为 present:
局限性 :需要内核开启 CONFIG_MEM_SOFT_DIRTY,测试设备上未开启(/proc/self/pagemap bit 55 始终为 0)。但对 present bit(bit 63)的检测不需要任何内核配置,通用性更好。
原理 (来自 vm_rw_detect/detect.c 的研究):
/proc/self/stat 中的 min_flt 字段计数了所有 归属于该进程的 minor faults,包括由外部进程通过 access_process_vm 触发的。而 perf_event(PERF_COUNT_SW_PAGE_FAULTS_MIN) 只在 current == self 时才计数。
两者的差值 = 外部触发的缺页数:
实现 :
实测效果 :攻击使用 access_process_vm 时,ext=+37(37 次外部缺页),方法一(pagemap)同时命中 8/8。攻击使用物理直读时,ext=0,完全绕过。
定期检查堆和 mmap 区域中的魔数值是否被篡改,用于检测写入 攻击。对纯读取攻击无效。
通过 perf_event_open(type=PERF_TYPE_RAW, config=event_code<<8) 监控 ARMv8 PMU 事件:
发现 :
这是本项目最重要的发现:CPU 缓存时序检测是唯一能检测物理内存直读的用户层方法 。
ARMv8-A 架构的 CPU 缓存采用物理地址标记(Physically Tagged),具体有两种实现:
PIPT(Physically Indexed, Physically Tagged) :缓存行索引和标签都用物理地址。绝大多数 ARM L2/L3 缓存采用此设计。优势是完全不存在别名(aliasing)问题。
VIPT(Virtually Indexed, Physically Tagged) :缓存行索引用虚拟地址的低位,标签用物理地址。ARM L1 数据缓存多采用此设计。对于满足 (Cache_Way_Size ≤ Page_Size) 的缓存(如 32KB 4-way = 8KB/way < 4KB page),虚拟索引位 [11:6] 完全落在页内偏移范围内,与物理地址相同,因此也不会产生别名。
核心结论 :无论 PIPT 还是 VIPT,标签比较用的都是物理地址。不同虚拟地址(VA_A 和 VA_B)映射到同一物理地址(PA)时,缓存控制器都能通过物理标签匹配到同一缓存行 。这是整个检测方法的硬件基础。
KPM 模块通过 memremap(phys, 4096, MEMREMAP_WB) 创建临时内核虚拟地址映射。关键在于 MEMREMAP_WB 标志:
ARM64 将内存类型分为三类:
MEMREMAP_WB 映射为 Normal Cacheable Write-Back 类型。CPU 对此类内存的读取会:
如果 KPM 使用 MEMREMAP_WC(Write-Combine,不可缓存),则数据不会进入缓存,本检测方法失效。 但 memremap 的 MEMREMAP_WC 在 ARM64 上实际映射为 Device GRE 类型,对于普通 RAM 的读写性能极差且可能导致对齐异常,实践中极少使用。
ARM 多核 SoC 通过 AMBA CHI(Coherent Hub Interface)或 CCI(Cache Coherent Interconnect)实现跨核缓存一致性。经典 MESI 状态转换:
关键 :KPM 的 memcpy 读取整页(64 个缓存行),每个缓存行被加载到 KPM 所在 CPU 核的 L1/L2 缓存中(状态 E/S)。当防御进程在同一或不同 CPU 核上访问同一物理页时,缓存一致性协议介入:
三种命中延迟都远低于 DRAM 访问 ,这就是缓存时序检测的物理基础。
ARM 架构定义了多个"点"来描述缓存操作的范围:
DC CIVAC(Clean + Invalidate to PoC)保证:
为什么用 CIVAC 而不是 CVAU? CVAU 只清洗到 PoU(本 Cluster 的 L2)。另一个 Cluster 的核可能还持有该缓存行的副本,防御进程切换到该核时可能误判为"命中"。CIVAC 确保全局可见性。
DSB ISH 的必要性 :DSB(Data Synchronization Barrier)确保所有先前的缓存维护操作完成。ISH(Inner Shareable)域是操作系统将用户进程分配到的共享域。在 DC CIVAC 后插入 DSB ISH,保证后续的计时测量是在缓存完全清空之后进行的。
cntvct_el0 的典型频率约 19.2 MHz(由 CNTFRQ_EL0 寄存器指定),每个 tick 约 52 ns。若 CPU 主频为 2 GHz,每个 tick 覆盖约 104 个 CPU 周期。
单次访问的计时困境 :
64 次访问的信号放大 :
64 次访问将信号放大了 64 倍,使不同缓存层级之间的差异从无法区分变为 显著可区分 (2-3 vs 31 vs 123 ticks)。这正是遍历页面全部 64 个缓存行的数学基础。
通过延时放大信号 :
除了空间放大(64 个缓存行),还可以通过时间放大。在测量前通过 usleep(5ms) 等操作延时,这段时间允许:
为什么每页恰好 64 个缓存行? 缓存行大小是 ARM 架构定义的(通常 64 字节),4KB 页面 / 64 字节 = 64 个缓存行。这不是巧合,而是页面大小和缓存行大小定义的结果。
实测数据验证了各缓存层级的理论计算值:
阈值设为 50 ticks:能捕获 L1/L2 命中和跨核缓存一致性传输,同时过滤 L3 和 DRAM 访问。
为提高捕获概率,每个检测轮次内执行 60 次快速探测(每 5ms 一次):
60 轮 × 5ms = 300ms 总探测窗口,攻击每 20-50ms 运行一次,保证至少数次重叠。
关键发现:必须让 KPM 模块读取整页 (4096 字节),而非 8 字节:
攻击使用物理直读(PRCTL_MEM_READ, 整页 4096 字节)时:
数据验证 :防御写入可识别模式 0xcafe0000dead0000 ~ 0xcafe0007dead0000,攻击通过 prctl(PRCTL_MEM_READ) 读出完全一致的数据,100% 确认 KPM 物理直读功能正常。
攻击端使用 kernel_driver.h 中从原始 kernel.h 提取的 c_driver 类:
攻击端通过解析 /proc/<pid>/maps 自动发现目标的匿名 rw-p 映射区域,对每个页面读取 8 字节。
当 KPM 使用 access_process_vm 路径时,99 次读取(792 字节)产生了 37–38 次外部可检测的 minor faults,8/8 的 pagemap 检测页全部命中:
目标:实现真正的物理内存直接读取 ,绕过 access_process_vm 及其所有副作用。
核心函数 walk_page_table() 手动遍历 ARM64 多级页表:
实现(参考 rwProcMem33 的 get_task_proc_phy_addr) :
rwProcMem33 是一个成熟的 Linux ARM64 内核进程内存读写驱动,提供了两种物理地址获取方式:通过 /proc/pid/pagemap 文件读取(get_pagemap_phy_addr)和手动遍历页表(get_task_proc_phy_addr)。后者使用内核标准宏 pgd_offset → p4d_offset → pud_offset → pmd_offset → pte_offset_kernel 遍历页表,并通过 page_to_phys(pte_page(*pte)) 将 PTE 转换为物理地址。物理内存的实际读写则通过 xlate_dev_mem_ptr(ioremap_cache)映射物理页后直接 memcpy。本项目在 KPM 约束下参考了其页表遍历思路,但由于 KPM 无法使用内核标准宏,改为手动计算各级索引并从页表条目中直接提取物理地址。
关键设计决策:
KPM 模块需要内核符号来转换虚拟地址到物理地址,但 ARM64 上 __virt_to_phys 和 virt_to_phys 都是内联宏,不作为内核符号导出:
解决方案 :完全不依赖 VA→PA 转换。PGD 直接读内核 VA(第 0 级),子级页表物理地址来自 PTE 条目本身(通过 memremap 访问),无需任何转换函数。
mm_struct 中的 pgd 指针偏移量因内核配置和 __randomize_layout(randstruct)而异。探测策略从单进程(init, pid=1)扩展为多进程扫描:
问题 :init 进程(pid=1)通常没有用户空间页表映射,其 PGD 中所有用户空间条目为 0,无法验证 pgd 指针的有效性。
解决 :扫描多个 PID(1000, 2000, 500, 1500 等),对每个进程的 mm_struct 按 8 字节步进扫描(0x00–0xFF),检查每个候选内核地址是否指向有效的 PGD 页表(任意 512 个 PGD 条目中有 ≥1 个有效条目):
最终找到 pid=1000, pgd_off=0x48, levels=3。
无回退设计 :如果 g_phys_mem_available == 0,直接返回 -ENODEV,强制暴露初始化失败,便于调试。
KPM 模块演进至 v4.0,攻击端共支持三种读取路径(对应三个 prctl 命令码),加上纯防御端基线对照,共四种测试模式。本节在一次统一的对照实验中横向比较四者的可检测性。
实验环境: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 探测窗口。
测试 1 — 无任何读取(基线)
基线干净:5 轮 × 60 次 = 300 次缓存探测全部零命中 ,无 PMU 假阳性。
测试 2 — 有缓存读取(WB)
测试 3 — 无缓存读取(WC,memremap WC 物理直读,已移除 WB 回退)
对比:移除 WB 回退前后
测试 4 — DMA 读取(Device-nGnRnE,绕过全部缓存)
KPM 模块的 WC 读取路径曾包含显式的 WB 回退逻辑:
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% 的命中率有两种可能:
验证实验 :从 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。
3. 缓存一致性协议的嗅探效应: 即使 WC 不在本地 L1/L2 分配,缓存一致性协议的 snoop 机制仍使持有数据的 SLC 响应并提供数据。防御核的 DC CIVAC 清洗后访问 -> 攻击核的 NC 读取触发 snoop -> SLC 向防御核提供数据 -> 形成"被动共享",导致低于阈值的快速访问。
完全消除残余命中的方向:
防御端在探测页中写入可识别魔数模式(0xcccc...),攻击端通过三种 prctl 路径均正确读出,确认各读取路径功能正常:
原理 :调用 Linux 内核标准函数 access_process_vm(target_task, addr, buf, len, 0),内核代为完成 VA→PA 转换和页表遍历。
读取链路 :
内存类型 :kmap() 创建的内核映射使用 Normal Cacheable Write-Back(与用户态进程 VA 属性一致),数据必然经过 L1/L2 缓存,且触发缺页修改 PTE Accessed/Dirty 位。
检测表面 :
小结 :access_process_vm 是标准的、文档化的内核接口,功能完备但留下大量可检测痕迹,不适合隐蔽读取。
原理 :不再调用 access_process_vm,改为手动遍历目标进程的 ARM64 页表 获取物理地址,然后用 memremap(phys, MEMREMAP_WB) 创建临时内核映射直接读取。
读取链路 :
关键改进 — 绕过 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,大幅扩大缓存"指纹"。
为什么需要信号放大 :攻击端也不知道防御端具体探测哪个地址。8 字节读取只污染单个缓存行,防御端 8 个探测页中有 7 个可能完全不受影响。整页读取确保每个探测页的 8 个偏移位置(8×8=64 条缓存行)全部被污染,命中率从随机提升到 ~80%+。
这一版本本身是攻击端的退步 (更容易被检测),但验证了缓存时序检测的物理原理——这条检测链是后续所有规避设计的基础。
原理 :MEMREMAP_WC (Write-Combine) 在 ARM64 上映射为 Normal Non-Cacheable 内存。与 Normal WB 的关键区别在于 MMU 页表条目的 MAIR 属性索引不同:
Normal NC 的缓存行为 :
ARM 架构规范 (ARMv8-A ARM D13.2.111) 明确:所有 Normal 类型的内存(包括 Non-Cacheable)均可被系统级缓存(SLC)透明缓存 。这意味着即使 PT 标记为 NC,硬件仍可能在 SLC 中分配缓存行。这是 ARM 不同于 x86 的关键行为差异。
代码路径 :
问题 :v3.0 的 read_mem_phys_direct() 包含静默降级逻辑:
当 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 :
读取链路 :
Device-nGnRnE 协议级分析 :
缓存行为 — 全绕过 :
ARM 架构规定 Device 内存不可被任何缓存层级缓存 (ARMv8-A ARM D13.2.111),这是硬件保证,不同于 Normal NC 的 "可被 SLC 透明缓存"。
vmap 页表操作细节 :
ARM64 PTE 中控制内存类型的字段:
prot 值 0x00E8000000000703 解码:
与 WC (v3.0) 的本质区别 :
原始 Makefile 的环境变量 CC=cc(make 默认值)覆盖了交叉编译器检测逻辑。修复:
早期用 C 语言编写的测试程序直接调用 prctl(PRCTL_MEM_READ, &op, ...) 始终返回 -EINVAL(errno=22),但改用 C++ 的 c_driver 类后成功。关键在于:
最初的 Soft-Dirty 检测流程是「先清除,后检查」:
实际攻击发生在两次检测轮次之间,标记在下一轮被 clear_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。
最终参数总结 :
KPM 框架在 pgtable.h 中声明了 linear_voffset、va_bits、page_level、page_shift 等外部变量,但 KPM 模块加载器不会解析这些符号:
解决 :
ARM64 上 __virt_to_phys 和 virt_to_phys 都是内联宏而非可链接的函数符号。5 个所需符号中唯一解析失败的:
解决 :利用内核线性映射的特性,PGD 层级直接通过内核 VA 读取,无需物理地址转换。子级页表使用 memremap 映射 PTE 中的物理地址。
probe_mm_pgd_offset 最初使用 init 进程(pid=1)探测 mm_struct->pgd 偏移量,但 init 的所有 PGD 用户空间条目为 0(init 运行在内核模式)。
解决 :扫描多个 PID(1000+),验证任意 PGD 条目(不仅第一个)中是否有有效页表项。
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 模式在现有用户层检测框架下形成完全不可检测的真空地带。
本文档随项目代码一起维护,文件路径:/home/zzz/Desktop/kernel_read_detect/article.md
命令码
值
功能
PRCTL_MEM_READ
0x4D454D01
内存读取(物理直读 WB,性能优先)
PRCTL_MEM_WRITE
0x4D454D02
内存写入(物理直写 WB)
PRCTL_GET_PID
0x4D454D03
按进程名查找 PID
PRCTL_MEM_READ_SAFE
0x4D454D04
内存读取(WC,绕过 L1/L2)
PRCTL_MEM_WRITE_SAFE
0x4D454D05
内存写入(WC)
PRCTL_MEM_READ_DMA
0x4D454D06
内存读取(vmap Device-nGnRnE,绕过全部缓存)
事件
Event Code
Config
描述
REMOTE_ACCESS
0x31
0x3100
远程 CPU 访问本核缓存
BUS_ACCESS
0x19
0x1900
总线访问事件
L2D_REFILL
0x17
0x1700
L2 数据缓存重填
L1D_REFILL
0x03
0x0300
L1 数据缓存重填
MEM_ACCESS
0x13
0x1300
数据内存访问
DTLB_WALK
0x34
0x3400
数据 TLB 遍历
内存类型
属性
可缓存
用途
Normal WB
Write-Back Cacheable
✅
普通 RAM
Normal WT
Write-Through Cacheable
✅
帧缓冲
Device nGnRnE
Non-Cacheable, Non-Gathering
❌
MMIO 寄存器
场景
测量值 (cntvct ticks)
反推延迟
对应缓存层级
校准 HIT(刚触摸后)
4-6
~4 cycles/行
L1
攻击期间探测命中
50-70
~50 cycles/行
L2/跨核传输
无攻击基线
100-170
~150 cycles/行
DRAM
调度干扰尖峰
2000-11000
—
调度器抢占
缓存层级
单行延迟
64 行总周期
cntvct ticks (19.2MHz)
L1 命中(本地核)
~4 cycles
~256
~2-3
L2/跨核传输
~50 cycles
~3,200
~31
L3(系统缓存)
~100 cycles
~6,400
~62
DRAM
~200 cycles
~12,800
~123
测试模式
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(终极规避)
方法
原理
代价
页表遍历也用 WC
消除页表页缓存污染
遍历性能大幅下降
读取后 DC CIVAC 自清洗
主动逐出 SLC 中的数据
增加 KPM 模块指令开销
使用 Device 内存类型 (nGnRnE)
完全绕过所有缓存和写缓冲
vmap 复杂度增加(已在 v4.0 实现)
跨 Cluster 攻击
利用不同 Cluster 的独立 SLC
需绑定 CPU 亲和性
检测方法
是否检出
原因
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%+
属性
全称
含义
对检测的影响
nG
Non-Gathering
不合并多次内存访问为单次总线事务
每次读取独立事务, 无 batching
nR
Non-Reordering
不重排访问顺序
严格按程序顺序执行
nE
No Early Ack
无提前写入确认
写入等待到达最终目的地
参数
旧值
新值
原因
每次读取大小
8 字节
4096 字节
加载全部 64 个缓存行
探测间隔
50ms/忙等
5ms/usleep
让出 CPU + 更高频率
阈值
hit×3 (3-12)
50 (固定)
覆盖跨核一致性传输
探测轮数
40
60
300ms 总窗口
PID 获取
按名查找
命令行传入
适配不同进程名
[内核课程]《Windows内核攻防实战》!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。
最后于 4天前
被mb_fycbgpri编辑
,原因: