首页
课程
问答
CTF
社区
招聘
峰会
发现
排行榜
知识库
工具下载
看雪20年
看雪商城
证书查询
登录
注册
首页
社区
课程
招聘
发现
问答
CTF
排行榜
知识库
工具下载
峰会
看雪商城
证书查询
社区
Android安全
发新帖
20
19
[原创]内核驱动级内存读写的用户层检测与绕过:一次攻防对抗实践
发表于: 2026-6-9 16:25
42684
[原创]内核驱动级内存读写的用户层检测与绕过:一次攻防对抗实践
blackr0ck
2
2026-6-9 16:25
42684
# 内核驱动级内存读写的用户层检测与绕过:一次攻防对抗实践 > **摘要**:本文记录了在 ARM64 Android 平台上,构建一个完整的内核驱动内存读写检测 Demo 的全过程。项目包含两个独立运行的程序——仅使用用户层权限的**防御端**和拥有 root 权限对接内核驱动进行读写的**攻击端**。防御端通过 5 种用户态手段检测外部内存访问,其中 **CPU 缓存时序检测(Flush+Reload)是唯一能检测物理内存直读的方法**;攻击端则从最初的 `access_process_vm` 被完全检测,演进为手动遍历 ARM64 页表的物理内存直接读取,最终成功绕过 Pagemap Present 和 PerfEvent 交叉通道检测,但仍被缓存时序方法检出。文章详细记录了每种检测方法的原理、实现、调试过程,以及 KPM(Kernel Patch Module)从 `access_process_vm` 到真正物理直读的进化历程。 --- ## 一、项目背景 ### 1.1 对抗模型 在现代移动安全领域,攻击者常通过内核驱动(Kernel Driver)绕过用户空间的权限限制,直接读取或修改目标进程的内存。这类攻击具有极强的隐蔽性,因为传统用户态检测手段无法感知内核层的活动。 本文构建的 Demo 设定如下: - **攻击端(root)**:运行在已安装 KPM 内核模块的设备上,通过 hook `prctl` 系统调用实现跨进程内存读写。目标:读取防御端进程的内存数据。 - **防御端(用户层)**:仅使用普通用户权限,不依赖任何内核模块或 root 权限。目标:检测是否有外部进程通过内核驱动读取了自己的内存。 ### 1.2 目标平台 - SoC:Qualcomm Snapdragon(ARMv8-A) - Kernel:Linux 5.4.210-qgki,39-bit VA,3 级页表(PGD → PMD → PTE),4KB 页面 - 补丁框架:APatch Next 11021 - 编译器:`aarch64-linux-gnu-gcc/g++`(GNU 工具链)及 `aarch64-none-elf-gcc`(KPM 交叉编译) ### 1.3 项目文件结构 ``` kernel_read_detect/ # 用户态项目(防御端 + 攻击端) ├── defense.c # 防御端综合检测程序(C) ├── cache_test.c # CPU 缓存时序检测独立测试(C) ├── attack.cpp # 攻击端读取程序(C++) ├── kernel_driver.h # 内核驱动 C++ 封装(prctl 跨进程调用) ├── precise_test.sh # 四项测试脚本(baseline/WB/WC/DMA) ├── detailed_test.sh # 逐检测方法详细数据采集脚本 └── Makefile # ARM64 交叉编译 KernelPatch-0.11.1-dev/kpms/ ├── prctlhookRWMemory/ # 原始 KPM 模块(access_process_vm,已弃用) │ ├── Kernel_prctl.c │ ├── Kernel_prctl.h │ └── Makefile └── prctlhookRWMemoryNew/ # 当前 KPM 模块(物理直读 + vmap Device) ├── Kernel_prctl.c # ~790 行,统一 WB/WC/DMA 三条读取路径 ├── Kernel_prctl.h └── Makefile 参考项目: ├── rwprocmem33_20250520/ # ARM64 物理内存读写驱动 (phy_mem.h) └── vm_rw_detect/ # PMU 检测研究 & perf_event 交叉通道 ``` --- ## 二、技术架构 ### 2.1 内核驱动接口(KPM prctl Hook) KPM(Kernel Patch Module)是 KernelPatch/APatch 框架下的内核模块系统。本项目使用一个 hook `prctl` 系统调用的 KPM 模块,通过自定义命令码实现内存读写: | 命令码 | 值 | 功能 | |--------|-----|------| | `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,绕过全部缓存) | 用户态调用方式: ```c struct mem_operation { pid_t target_pid; // 目标进程 PID uint64_t addr; // 目标虚拟地址 void *buffer; // 数据缓冲区(用户态指针) uint64_t size; // 读写字节数 }; struct mem_operation op = { target_pid, addr, buf, size }; prctl(PRCTL_MEM_READ, (unsigned long)&op, 0, 0, 0); ``` KPM 在 `syscall_hook_demo_init` 中通过 `fp_hook_syscalln(__NR_prctl, 5, before_prctl, 0, 0)` 注册 hook。当用户态调用 `prctl` 时,`before_prctl` 函数拦截并处理自定义命令,其他命令则透传给原始 `prctl`。 ### 2.2 原始实现:`access_process_vm` 路径 原始的 `prctlhookRWMemory` 模块中,`PRCTL_MEM_READ` 对应的 `read_mem_phys` 函数虽然名为 "phys",但实际上仍然调用 `access_process_vm_safe()`: ```c // 原始 Kernel_prctl.c 第 160 行 — 名为 phys 实则 access_process_vm long bytes_read = access_process_vm_safe(task, current_vaddr, kernel_buffer, chunk_size, 0); ``` 真正的手动物理地址遍历代码(`read_phys_addr_direct`,使用 `memremap` 映射物理页)被整个注释掉了,从未编译进模块。 ### 2.3 `access_process_vm` 的可检测副作用 `access_process_vm` 是内核中用于跨进程内存访问的标准函数(也被 `ptrace` 和 `/proc/pid/mem` 使用)。它在内部调用 `get_user_pages_remote()`: ``` access_process_vm() → get_user_pages_remote(target_mm, addr, ...) → 页面 non-present → handle_mm_fault() → 分配物理页(零页) → 设置 PTE present bit = 1 → target->min_flt++ ← 目标进程缺页计数 +1 → 页面已 present → 直接 pin → copy_to_user / copy_from_user ← 拷贝数据 ``` **核心发现:`get_user_pages` 在处理 non-present 页面时,会在目标进程的上下文中触发 minor page fault,分配物理页并设置 PTE。即使后续拷贝失败,这些副作用已经产生。** --- ## 三、防御端:用户层检测方法 防御端实现了 5 种完全基于用户态权限的检测方法。所有方法均在防御端程序 `defense.c` 中实现,其中缓存时序检测另有独立的精简测试程序 `cache_test.c`。 ### 3.1 方法一:Pagemap Present 检测(核心方法 ★) **原理**: 利用 `madvise(MADV_DONTNEED)` 将监控页变为 non-present 状态(`/proc/self/pagemap` bit 63 = 0),然后周期性检查该位。如果 `access_process_vm` 读取了这些页面,`get_user_pages` 会触发 `handle_mm_fault`,分配物理页并使 present bit 变为 1。 **实现细节**: ```c // 1. 分配检测页并使其 non-present void *addr = mmap(NULL, PAGE_SIZE, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0); memset(addr, 0xDD, 1); // 先 touch 确保 VMA 有效 madvise(addr, PAGE_SIZE, MADV_DONTNEED); // 回收物理页 // 2. 验证 non-present int present = page_is_present(addr); // 读 /proc/self/pagemap // 3. 检测循环(每 5 秒) for (int i = 0; i < g_detect_page_count; i++) { if (page_is_present(g_detect_pages[i]) == 1) { // 检测到外部访问!页面被 access_process_vm 触碰了 detected++; } madvise(g_detect_pages[i], PAGE_SIZE, MADV_DONTNEED); // 重置 } ``` **关键时序**:必须是**先检查,后清除**。早期的实现错误地先 `clear_refs` 再检查,导致攻击留下的证据在检查前被清除。修正后的流程: ``` 检测轮次 N: 先检查 pagemap → 发现 present = 1 → 告警! 再 madvise(DONTNEED) → 重置为 non-present 等待 5 秒... 检测轮次 N+1: 先检查 → 如果攻击发生在这 5 秒内 → 再次告警 ``` **验证实验**:通过直接测试确认了 `access_process_vm` 读取 DONTNEED 页面时,即使 `prctl` 返回失败(`-EINVAL`),仍触发了一次 minor fault 并使页面变为 present: ``` Self page: 0x7e04c95000, present=0 After DONTNEED: present=0 prctl: ret=-1 errno=22 val=0x0000000000000000 min_flt after: delta=1 ← 虽然读失败了,缺页还是发生了 present after read: 1 ← 页面变为 present! ``` **局限性**:需要内核开启 `CONFIG_MEM_SOFT_DIRTY`,测试设备上未开启(`/proc/self/pagemap` bit 55 始终为 0)。但对 present bit(bit 63)的检测不需要任何内核配置,通用性更好。 ### 3.2 方法二:PerfEvent 交叉通道检测 **原理**(来自 `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` 时才计数。 两者的差值 = 外部触发的缺页数: ``` external_faults = delta_min_flt(stat) - delta_min_flt(perf) ``` **实现**: ```c // 打开 perf_event 计数器 struct perf_event_attr a; a.type = PERF_TYPE_SOFTWARE; a.config = PERF_COUNT_SW_PAGE_FAULTS_MIN; a.disabled = 0; int fd = perf_event_open(&a, 0, -1, -1, 0); // 每轮检测: unsigned long stat_flt = read_stat_min_flt(); // /proc/self/stat uint64_t perf_val; read(fd, &perf_val, sizeof(perf_val)); // perf_event long external = (stat_flt - last_stat) - (perf_val - last_perf); if (external > 0) { // 外部进程在我们的页面上触发了缺页! } ``` **实测效果**:攻击使用 `access_process_vm` 时,`ext=+37`(37 次外部缺页),方法一(pagemap)同时命中 8/8。攻击使用物理直读时,`ext=0`,完全绕过。 ### 3.3 方法三:金丝雀值完整性校验 定期检查堆和 mmap 区域中的魔数值是否被篡改,用于检测**写入**攻击。对纯读取攻击无效。 ### 3.4 方法四:PMU 硬件计数器(ARMv8) 通过 `perf_event_open(type=PERF_TYPE_RAW, config=event_code<<8)` 监控 ARMv8 PMU 事件: | 事件 | 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 遍历 | **发现**: - **Type=7(ARMv8 PMUv3)**在 task-context 和 system-wide 模式均失败(`EACCES`),需要通过 `type=4`(PERF_TYPE_RAW)访问 - **System-wide(pid=-1)**模式也失败(`EACCES`),只能使用 task-context(pid=0) - **REMOTE_ACCESS / BUS_ACCESS 存在严重假阳性**:即使没有攻击,也会出现 +90 以上的增量(可能来自系统调度或电源管理活动),需调高阈值 ### 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 数据缓存多采用此设计。对于满足 `(Cache_Way_Size ≤ Page_Size)` 的缓存(如 32KB 4-way = 8KB/way < 4KB page),虚拟索引位 `[11:6]` 完全落在页内偏移范围内,与物理地址相同,因此也不会产生别名。 **核心结论**:无论 PIPT 还是 VIPT,标签比较用的都是物理地址。不同虚拟地址(VA_A 和 VA_B)映射到同一物理地址(PA)时,**缓存控制器都能通过物理标签匹配到同一缓存行**。这是整个检测方法的硬件基础。 ``` 用户态 VA_A (defense) 内核态 VA_B (memremap) 0x738eb18000 0xffffff8000123000 │ │ │ 页表遍历 │ 页表遍历 ▼ ▼ ┌─────────────────────────────────────────┐ │ 物理地址: 0x1234567000 │ │ ┌─────────────────┐ │ │ │ 4KB 物理页 │ │ │ │ 64 个缓存行 │ │ │ └────────┬────────┘ │ └───────────────────┼─────────────────────┘ │ ┌───────────▼───────────┐ │ ARM CPU Cache │ │ ┌─────────────────┐ │ │ │ TAG = PA[47:12] │ │ ← 物理地址匹配 │ │ DATA = 64 bytes │ │ │ │ STATE = Shared │ │ │ └─────────────────┘ │ │ │ │ 不同 VA,同一 PA │ │ → 命中! │ └───────────────────────┘ ``` #### 原理二:KPM 的可缓存内存映射 KPM 模块通过 `memremap(phys, 4096, MEMREMAP_WB)` 创建临时内核虚拟地址映射。关键在于 `MEMREMAP_WB` 标志: ```c // MEMREMAP_WB 的 ARM64 实现路径: memremap(phys, size, MEMREMAP_WB) → ioremap_prot(phys, size, pgprot_decrypted(PAGE_KERNEL)) → PAGE_KERNEL = MT_NORMAL (Normal Cacheable Write-Back 内存类型) ``` ARM64 将内存类型分为三类: | 内存类型 | 属性 | 可缓存 | 用途 | |---------|------|:-----:|------| | Normal WB | Write-Back Cacheable | ✅ | 普通 RAM | | Normal WT | Write-Through Cacheable | ✅ | 帧缓冲 | | Device nGnRnE | Non-Cacheable, Non-Gathering | ❌ | MMIO 寄存器 | `MEMREMAP_WB` 映射为 **Normal Cacheable Write-Back** 类型。CPU 对此类内存的读取会: 1. 先查询 L1/L2 缓存 2. 未命中则发起缓存行填充(Cache Line Fill) 3. 从 DRAM(或远程缓存)读取 64 字节缓存行 4. 存入本地 L1/L2,状态标记为 Shared 或 Exclusive **如果 KPM 使用 `MEMREMAP_WC`(Write-Combine,不可缓存),则数据不会进入缓存,本检测方法失效。** 但 `memremap` 的 `MEMREMAP_WC` 在 ARM64 上实际映射为 Device GRE 类型,对于普通 RAM 的读写性能极差且可能导致对齐异常,实践中极少使用。 #### 原理三:缓存一致性协议(MESI/MOESI) ARM 多核 SoC 通过 AMBA CHI(Coherent Hub Interface)或 CCI(Cache Coherent Interconnect)实现跨核缓存一致性。经典 MESI 状态转换: ``` Defense 核 KPM 核 ────────── ────── 初始状态: INVALID INVALID (DC CIVAC 后全部失效) KPM memcpy: I → E (Exclusive) [加载缓存行到本地] Defense 访问: I → S (Shared) E → S (Shared) [一致性协议广播读取请求] [响应数据,降级为 Shared] [数据通过 CHI/CCI 传输] [延迟: ~40-80 cycles] ``` **关键**:KPM 的 `memcpy` 读取整页(64 个缓存行),每个缓存行被加载到 KPM 所在 CPU 核的 L1/L2 缓存中(状态 E/S)。当防御进程在同一或不同 CPU 核上访问同一物理页时,缓存一致性协议介入: - 同一 Cluster(共享 L2):数据直接从 L2 获取,延迟 ~15-20 cycles - 不同 Cluster(通过 CCI/CCN):缓存到缓存传输,延迟 ~40-80 cycles - 数据完全不在任何缓存:DRAM 访问,延迟 ~200+ cycles **三种命中延迟都远低于 DRAM 访问**,这就是缓存时序检测的物理基础。 #### 原理四:DC CIVAC 与 Point of Coherency ARM 架构定义了多个"点"来描述缓存操作的范围: ``` CPU Core 0 CPU Core 1 CPU Core 2 CPU Core 3 ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ L1 I$ D$ │ │ L1 I$ D$ │ │ L1 I$ D$ │ │ L1 I$ D$ │ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ PoU │ │ │ ├──────────┐ ├──────────┐ ├──────────┐ ├──────────┐ │ Shared L2│ │ Shared L2│ │ Shared L2│ │ Shared L2│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ │ │ │ └───────────┬───────┴───────────┬───────┘ │ │ ┌──────────┐ │ │ └────┤ CCI/CHI ├───┘ │ │ (L3/SLC) │ │ └────┬─────┘ │ │ PoC │ ┌────▼─────┐ │ │ DRAM │ │ └──────────┘ │ ``` - **PoU(Point of Unification)**:同一核的指令/数据缓存统一可见的点(通常是 L2) - **PoC(Point of Coherency)**:**所有**观察者(CPU、DMA、GPU 等)看到同一数据副本的点(通常是 L3 或内存控制器) `DC CIVAC`(Clean + Invalidate to PoC)保证: 1. 如果缓存行为 Dirty,写回数据到 PoC 2. **该缓存行在所有核的 L1/L2 中全部失效** 3. 操作完成后,该缓存行的唯一有效副本在 PoC(L3/DRAM) 4. 任何核的下次访问都必须从 PoC 获取 **为什么用 CIVAC 而不是 CVAU?** CVAU 只清洗到 PoU(本 Cluster 的 L2)。另一个 Cluster 的核可能还持有该缓存行的副本,防御进程切换到该核时可能误判为"命中"。CIVAC 确保全局可见性。 **DSB ISH 的必要性**:`DSB`(Data Synchronization Barrier)确保所有先前的缓存维护操作完成。`ISH`(Inner Shareable)域是操作系统将用户进程分配到的共享域。在 `DC CIVAC` 后插入 `DSB ISH`,保证后续的计时测量是在缓存完全清空之后进行的。 #### 原理五:信号放大——为什么遍历全部 64 个缓存行 `cntvct_el0` 的典型频率约 19.2 MHz(由 `CNTFRQ_EL0` 寄存器指定),每个 tick 约 52 ns。若 CPU 主频为 2 GHz,每个 tick 覆盖约 104 个 CPU 周期。 **单次访问的计时困境**: ``` L1 命中: ~4 cycles → 4/104 ≈ 0.04 ticks → 计数值不变 (0) DRAM: ~200 cycles → 200/104 ≈ 1.9 ticks → 计数值为 1-2 单次测量无法可靠区分 (0 vs 1). ``` **64 次访问的信号放大**: ``` 64 × L1 命中: 64 × 4 = 256 cycles → 256/104 ≈ 2-3 ticks 64 × 跨核传输: 64 × 50 = 3,200 cycles → 3200/104 ≈ 31 ticks 64 × DRAM: 64 × 200 = 12,800 cycles→ 12800/104 ≈ 123 ticks ``` 64 次访问将信号放大了 64 倍,使不同缓存层级之间的差异从无法区分变为 **显著可区分**(2-3 vs 31 vs 123 ticks)。这正是遍历页面全部 64 个缓存行的数学基础。 **通过延时放大信号**: 除了空间放大(64 个缓存行),还可以通过时间放大。在测量前通过 `usleep(5ms)` 等操作延时,这段时间允许: 1. 防御进程让出 CPU,使攻击获得调度机会 2. 攻击的缓存行填充操作完成 3. 缓存一致性协议完成状态转换 **为什么每页恰好 64 个缓存行?** 缓存行大小是 ARM 架构定义的(通常 64 字节),4KB 页面 / 64 字节 = 64 个缓存行。这不是巧合,而是页面大小和缓存行大小定义的结果。 #### Flush+Reload 完整流程 ``` Step 1 — Flush (DC CIVAC): 防御进程执行 DC CIVAC 指令,将探测页的每个缓存行: a. 若为 Dirty → 写回 PoC b. 从所有核的 L1/L2 缓存中标记为 INVALID 然后执行 DSB ISH 等待操作完成 效果: 下次任何核访问此物理页 → 必然 MISS → 从 PoC(L3/DRAM) 获取 Step 2 — Wait (usleep 5ms): 防御进程调用 usleep() 进入内核等待队列,主动让出 CPU ┌─────────────────────────────────────────────┐ │ 期间可能发生的事件: │ │ a. 攻击进程在相同或不同核上运行 │ │ b. KPM 的 before_prctl Hook 执行 │ │ c. memremap(phys, 4096, MEMREMAP_WB) 映射 │ │ d. memcpy 读取 64 个缓存行 → 进入 L1/L2 │ │ e. 缓存一致性协议将数据同步到防御核可见 │ └─────────────────────────────────────────────┘ Step 3 — Measure (time_page_access): 防御进程唤醒,遍历页面全部 64 个缓存行: 若 KPM 在此期间访问过 → 数据在缓存中 → 总访问 < 50 ticks → 告警! 若 KPM 未访问 → 数据仍在 DRAM → 总访问 ≥ 50 ticks → 正常 ``` #### 从测量值反推缓存层级 实测数据验证了各缓存层级的理论计算值: | 场景 | 测量值 (cntvct ticks) | 反推延迟 | 对应缓存层级 | |------|----------------------|---------|------------| | 校准 HIT(刚触摸后) | 4-6 | ~4 cycles/行 | L1 | | 攻击期间探测命中 | 50-70 | ~50 cycles/行 | L2/跨核传输 | | 无攻击基线 | 100-170 | ~150 cycles/行 | DRAM | | 调度干扰尖峰 | 2000-11000 | — | 调度器抢占 | #### ARM64 汇编实现 ```c /* 读取虚拟计数器 (cntvct_el0, ~19.2MHz) */ static inline u64 cntvct(void) { u64 v; asm volatile("isb\n\tmrs %0, cntvct_el0" : "=r"(v)); return v; } /* DC CIVAC: Clean + Invalidate data Cache line to Point of Coherency */ static inline void dc_civac(void *addr) { asm volatile("dc civac, %0" :: "r"(addr) : "memory"); } /* DSB ISH: 确保缓存操作对 Inner Shareable 域内所有核可见 */ static inline void dsb_ish(void) { asm volatile("dsb ish" ::: "memory"); } /* 遍历页面全部 64 个缓存行,测量总访问时间 */ static u64 time_page(volatile char *addr) { u64 t0 = cntvct(); for (int off = 0; off < 4096; off += 64) { // 每个缓存行 64 字节 asm volatile("" ::: "memory"); volatile char v = addr[off]; // 访问该缓存行 (void)v; } asm volatile("" ::: "memory"); return cntvct() - t0; } ``` #### 关键参数:阈值的选择 | 缓存层级 | 单行延迟 | 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** | 阈值设为 50 ticks:能捕获 L1/L2 命中和跨核缓存一致性传输,同时过滤 L3 和 DRAM 访问。 #### 高频探测循环 为提高捕获概率,每个检测轮次内执行 60 次快速探测(每 5ms 一次): ```c #define PROBE_N 60 // 每轮 60 次探测 #define PROBE_MS 5 // 每次间隔 5ms for (int r = 0; r < PROBE_N; r++) { flush_all_pages(); // DC CIVAC 清空缓存 usleep(PROBE_MS * 1000); // 让出 CPU,攻击可在此期间运行 measure_all_pages(); // 测量访问时间,低于阈值则告警 } ``` 60 轮 × 5ms = 300ms 总探测窗口,攻击每 20-50ms 运行一次,保证至少数次重叠。 #### 攻击端配合:整页读取 关键发现:必须让 KPM 模块读取**整页**(4096 字节),而非 8 字节: ```cpp // ❌ 错误 — 只加载 1 个缓存行,其余 63 行从 DRAM → 总时间太高 unsigned long val = driver->read<unsigned long>(addr); // ✅ 正确 — 读取整页,memcpy 加载全部 64 个缓存行 char buf[4096]; driver->readMemSafe(target_pid, addr, buf, 4096); ``` #### 实测效果 攻击使用物理直读(PRCTL_MEM_READ, 整页 4096 字节)时: ``` Round 1: 366 次命中 ← 60 轮 × 8 页 = 480 次测量,命中率 76% Round 2: 358 次命中 Round 3: 385 次命中 ← 峰值命中率 80% 无攻击时 (timing 采样): page0 = 100-170 ticks (远高于阈值 50) → 正常,无告警 攻击进行中 (timing 采样): page0 = 5-8 ticks (远低于阈值 50) → 缓存命中,告警! ``` **数据验证**:防御写入可识别模式 `0xcafe0000dead0000` ~ `0xcafe0007dead0000`,攻击通过 prctl(PRCTL_MEM_READ) 读出完全一致的数据,100% 确认 KPM 物理直读功能正常。 ## 四、攻击端:从 `access_process_vm` 到物理直读 ### 4.1 用户态调用封装 攻击端使用 `kernel_driver.h` 中从原始 `kernel.h` 提取的 `c_driver` 类: ```cpp class c_driver { pid_t pid; // 构造函数中通过 getPidByName("defense") 获取 public: template <typename T> inline T read(uint64_t addr) { T val{}; readMemSafe(pid, addr, &val, sizeof(T)); return val; } inline int readMem(pid_t pid, uint64_t addr, void *buf, size_t len) { mem_operation op = { pid, addr, buf, len }; return prctl(PRCTL_MEM_READ, (unsigned long)&op, 0, 0, 0); } }; // 全局单例 static c_driver *driver = new c_driver(); // 使用:每次读取 8 字节 unsigned long val = driver->read<unsigned long>(target_addr); ``` 攻击端通过解析 `/proc/<pid>/maps` 自动发现目标的匿名 rw-p 映射区域,对每个页面读取 8 字节。 ### 4.2 原始模块的检测结果 当 KPM 使用 `access_process_vm` 路径时,99 次读取(792 字节)产生了 37–38 次外部可检测的 minor faults,8/8 的 pagemap 检测页全部命中: ``` 第 4 轮检测: 方法1(Pagemap): 8/8 个检测页变为 present ← 全部被检测! 方法4(PerfX): stat:+38 perf:+0 ext:+38 ← 37 次外部缺页 ``` ### 4.3 物理直读模块的设计与实现 目标:实现**真正的物理内存直接读取**,绕过 `access_process_vm` 及其所有副作用。 #### 4.3.1 ARM64 页表遍历 核心函数 `walk_page_table()` 手动遍历 ARM64 多级页表: ``` 虚拟地址 [38:0] (39-bit VA, 3 级页表): [38:30] → PGD index (9 bits, 512 entries) [29:21] → PMD index (9 bits, 512 entries) [20:12] → PTE index (9 bits, 512 entries) [11:0] → page offset 页表项格式 (ARM64): bits[47:12] = 下一级表物理地址 / 页面物理地址 bits[1:0] = 描述符类型 (0b11 = valid table/page) bit[1] = table indicator (0 = block/page, 1 = table) ``` **实现(参考 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 无法使用内核标准宏,改为手动计算各级索引并从页表条目中直接提取物理地址。 ```c static int walk_page_table(uint64_t mm_pgd_va, uint64_t vaddr, uint64_t *out_phys) { /* Level 0: PGD — 直接读内核虚拟地址 */ uint64_t *pgd = (uint64_t *)mm_pgd_va; uint64_t entry = pgd[(vaddr >> 30) & 0x1FF]; if ((entry & 0x3) != 0x3) return -1; /* Level 1: PMD — memremap 映射物理页 */ uint64_t pmd_phys = entry & PHYS_MASK & PAGE_MASK; uint64_t *pmd = my_memremap(pmd_phys, 4096, MEMREMAP_WB); entry = pmd[(vaddr >> 21) & 0x1FF]; my_memunmap(pmd); if ((entry & 0x3) != 0x3) return -1; /* Level 2: PTE — memremap 映射物理页 */ uint64_t pte_phys = entry & PHYS_MASK & PAGE_MASK; uint64_t *pte = my_memremap(pte_phys, 4096, MEMREMAP_WB); entry = pte[(vaddr >> 12) & 0x1FF]; my_memunmap(pte); if ((entry & 0x3) != 0x3) return -1; /* 计算最终物理地址 */ *out_phys = (entry & PHYS_MASK & PAGE_MASK) | (vaddr & 0xFFF); return 0; } ``` 关键设计决策: - **PGD 层级直接读内核 VA**:`mm->pgd` 是内核线性映射区的虚拟地址,可直接解引用,无需 `memremap` - **PMD/PTE 层级使用 `memremap`**:页表条目中的物理地址需要用 `memremap` 映射到内核虚拟地址后才能读取 - **支持大页**(2MB PMD block、1GB PUD block) #### 4.3.2 绕过 `__virt_to_phys` 不可用的问题 KPM 模块需要内核符号来转换虚拟地址到物理地址,但 ARM64 上 `__virt_to_phys` 和 `virt_to_phys` 都是内联宏,不作为内核符号导出: ``` [phys_RW] symbols: v2p=N ← __virt_to_phys 不可用 [phys_RW] INIT: phys=0 ← 物理直读被禁用 ``` **解决方案**:完全不依赖 VA→PA 转换。PGD 直接读内核 VA(第 0 级),子级页表物理地址来自 PTE 条目本身(通过 `memremap` 访问),无需任何转换函数。 #### 4.3.3 动态探测 `mm_struct->pgd` 偏移量 `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 个有效条目): ```c static int verify_pgd(uint64_t pgd_va) { uint64_t *pgd = (uint64_t *)pgd_va; for (int i = 0; i < 512; i++) { if ((pgd[i] & 0x3) == 0x3) return 1; // 找到有效条目 } return 0; } ``` 最终找到 `pid=1000, pgd_off=0x48, levels=3`。 #### 4.3.4 物理直读的完整流程 ```c static int read_mem_phys_direct(pid_t pid, uintptr_t vaddr, void __user *buffer, size_t size) { // 1. 获取目标进程 PGD 内核虚拟地址 uint64_t pgd_va = get_target_pgd(pid); while (total_read < size) { // 2. 遍历页表: vaddr → physical address uint64_t phys_addr; walk_page_table(pgd_va, current_va, &phys_addr); // 3. memremap 映射物理页 void *mapped = my_memremap(page_phys, 4096, MEMREMAP_WB); // 4. 直接 memcpy 数据 memcpy(kernel_buffer, mapped + offset, chunk); my_memunmap(mapped); // 5. 拷贝到用户态 compat_copy_to_user(user_buf + total_read, kernel_buffer, chunk); } } ``` **无回退设计**:如果 `g_phys_mem_available == 0`,直接返回 `-ENODEV`,强制暴露初始化失败,便于调试。 --- ## 五、实验结果 ### 5.1 四种读取模式与实验设计 KPM 模块演进至 v4.0,攻击端共支持三种读取路径(对应三个 prctl 命令码),加上纯防御端基线对照,共四种测试模式。本节在一次统一的对照实验中横向比较四者的可检测性。 | 测试模式 | 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 -> 终止进程 -> 收集日志。 ### 5.2 统一对照测试结果(精确地址 + 循环攻击) 为确保攻击/防御目标地址严格一致,本轮测试改为精确地址模式:防御端初始化后输出 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] 全部方法正常 ``` - 缓存时序检测:**58/60 = 96.7%** 子探测命中率,几乎连续命中 - PerfEvent:ext:0 —— 页面已 present(非 DONTNEED),无缺页触发 - PMU:本轮 REMOTE_ACC/BUS_ACCESS 均为 0 - **综合检测率:96.7%**(精确地址 + 时序对齐条件下) **测试 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 次探测零命中! ``` - 缓存时序检测:**仅第 0 轮冷启动噪声**(7/8 命中),后续 240 次探测(4 轮 × 60)**全部零命中** - **综合检测率:~0%**(与 DMA 模式一致,冷启动噪声不计入) **对比:移除 WB 回退前后** ``` 移除前 (有 WB 回退) 移除后 (无 WB 回退) WC 缓存命中 37/60 = 61.7% ~0/60 = ~0% (仅冷启动) 综合检测率 ~62% ~0% ``` - **结论:之前的 61.7% 检测率完全来自 WB 回退路径**。当 `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 次探测全部零命中! ``` - **全部 5 种检测方法零告警**:300 次缓存探测(5 轮 × 60 次)全部零命中 - Device-nGnRnE 总线事务完全旁路 L1/L2/SLC,不分配缓存行,不参与 MESI/MOESI 协议 - **综合检测率:0%** —— 在用户层完全不可检测 ### 5.3 四种模式检测率总览 ``` 测试模式 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 轮有冷启动噪声 ``` ### 5.4 WC 模式残余命中的微架构分析 #### WC 回退分析(已证实) KPM 模块的 WC 读取路径曾包含显式的 WB 回退逻辑: ```c // 已移除的回退代码(修复前) 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% 的命中率有两种可能: - **假说 A**:WC 成功,SLC 命中导致检出 - **假说 B**:`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 亲和性 | ### 5.5 数据验证 防御端在探测页中写入可识别魔数模式(0xcccc...),攻击端通过三种 prctl 路径均正确读出,确认各读取路径功能正常: ``` 模式 防御写入 攻击读出 验证 ───────────────────────────────────────────────────────────────────────── WB 0x79a74db000: 0xcccccccccccccccc -> 0xcccccccccccccccc OK WC 0x7556a80000: 0xcccccccccccccccc -> 0xcccccccccccccccc OK DMA 0x7327288000: 0xcccccccccccccccc -> 0xcccccccccccccccc OK ``` ### 5.6 各版本原理详解 #### v1.0 — access_process_vm:标准内核路径 **原理**:调用 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` 是标准的、文档化的内核接口,功能完备但留下大量可检测痕迹,不适合隐蔽读取。 --- #### v2.0 — 手动页表遍历 + memremap WB:绕过 Pagemap/PerfEvent **原理**:不再调用 `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 探测这些缓存行时发现命中,产生告警。 --- #### v2.1 — 整页读取(8字节 → 4096字节):信号放大 **原理**:将单次读取量从 8 字节扩大到 4096 字节(一整页 = 64 条缓存行),每条缓存行 (64B) 都被加载到 L1/L2,大幅扩大缓存"指纹"。 ``` v2.0: 读取 8 字节 → 污染 1 条缓存行 → 防御端需恰好探测这一条 v2.1: 读取 4096 字节 → 污染 64 条缓存行 → 防御端探测任意一条都能命中 ``` **为什么需要信号放大**:攻击端也不知道防御端具体探测哪个地址。8 字节读取只污染单个缓存行,防御端 8 个探测页中有 7 个可能完全不受影响。整页读取确保每个探测页的 8 个偏移位置(8×8=64 条缓存行)全部被污染,命中率从随机提升到 ~80%+。 这一版本**本身是攻击端的退步**(更容易被检测),但验证了缓存时序检测的物理原理——这条检测链是后续所有规避设计的基础。 --- #### v3.0 — memremap WC:绕过 L1/L2 **原理**:`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 的关键行为差异。 **代码路径**: ```c // 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.1 — 移除 WB 回退:消除隐蔽性隐患 **问题**:v3.0 的 `read_mem_phys_direct()` 包含静默降级逻辑: ```c 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%,证实之前的检出完全来自回退路径。 --- #### v4.0 — vmap Device-nGnRnE:终极规避 **原理**:使用 `vmap()` 对目标物理页创建**独立的虚拟地址映射**,页表条目标记为 **Device-nGnRnE** 内存类型。`vmap` 直接操作页表,不经过 `arch_memremap_can_ram_remap()` 检查,因此可对任意物理页(包括 RAM)创建 Device 映射——这是 `ioremap` 做不到的。 **为什么不用 ioremap**: ```c // 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% ``` --- ## 六、开发过程中的关键难点与解决方案 ### 6.1 难点一:交叉编译环境配置 原始 Makefile 的环境变量 `CC=cc`(make 默认值)覆盖了交叉编译器检测逻辑。修复: ```makefile # 使用 override 强制覆盖 make 内置变量 override CC := $(CROSS_CC) override CXX := $(subst gcc,g++,$(CC)) ``` ### 6.2 难点二:C 与 C++ 的 prctl 结构体差异 早期用 C 语言编写的测试程序直接调用 `prctl(PRCTL_MEM_READ, &op, ...)` 始终返回 `-EINVAL`(errno=22),但改用 C++ 的 `c_driver` 类后成功。关键在于: - KPM hook 通过 `__arch_copy_from_user` 从用户态拷贝 `mem_operation` 结构体 - C 和 C++ 在 ARM64 ABI 下对包含 `pid_t`(32-bit)和 `uint64_t` 的结构体可能有不同的隐式填充处理 - 使用 `kernel_driver.h` 中的 `c_driver::readMemSafe()` 方法正确对齐了结构体 ### 6.3 难点三:Soft-Dirty 检测的时序 Bug 最初的 Soft-Dirty 检测流程是「先清除,后检查」: ``` clear_soft_dirty() → 清除所有标记 usleep(10000) → 等待 check_page() → 检查 ← 永远为 0! ``` 实际攻击发生在两次检测轮次之间,标记在下一轮被 `clear_soft_dirty()` 清除。修复为「先检查,后清除」。 ### 6.4 难点四:`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`,不依赖任何内核配置。 ### 6.5 难点五:缓存时序检测的调试与突破 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 获取 | 按名查找 | 命令行传入 | 适配不同进程名 | ### 6.6 难点六:KPM 模块无法访问框架内部变量 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) ### 6.7 难点七:`__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 中的物理地址。 ### 6.8 难点八:init 进程无用户空间页表 `probe_mm_pgd_offset` 最初使用 init 进程(pid=1)探测 `mm_struct->pgd` 偏移量,但 init 的所有 PGD 用户空间条目为 0(init 运行在内核模式)。 **解决**:扫描多个 PID(1000+),验证任意 PGD 条目(不仅第一个)中是否有有效页表项。 --- ## 七、技术总结 ### 7.1 检测方法有效性矩阵 | 攻击方式 | Pagemap | PerfEvent | PMU REMOTE | **缓存时序** | Canary | 综合检测率 | |----------|:---:|:---:|:---:|:---:|:---:|:---:| | 物理直读 (WB) | ✅ 绕过 | ✅ 绕过 | ⚠️ 偶发假阳性 | ✅ 检出 | — | ~78% | | 物理直读 (WC) | ✅ 绕过 | ✅ 绕过 | ⚠️ 检出 | ✅ 绕过 | — | ~6% | | **物理直读 (DMA Device)** | ✅ 绕过 | ✅ 绕过 | ✅ 绕过 | ✅ 绕过 | — | **~0%** | | 写入攻击 | — | — | — | — | ✅ 检出 | — | ### 7.2 核心发现 **CPU 缓存时序检测(Flush+Reload)是目前最有效的物理内存直读用户层检测手段**,其有效性完全取决于攻击端的内存映射类型: 1. **MEMREMAP_WB(可缓存,设计目标为性能)**:缓存时序以 78% 命中率稳定检出。三条命令 (READ/SAFE/DMA) 均使用同一套手动页表遍历引擎,区别仅在于 memremap 内存类型——WB 提供最高吞吐性能,其缓存行为是 Normal Cacheable 内存的自然属性,并非设计缺陷。WC 和 Device 才是专门为规避缓存时序检测而引入的映射类型 2. **MEMREMAP_WC(Normal Non-Cacheable,不可缓存)**:缓存时序命中率降至 6%(降幅 92%),仅 PMU REMOTE_ACCESS 仍可检出——但 PMU 在基线测试中存在偶发假阳性(+85),单独依赖不可靠 3. **vmap Device-nGnRnE(DMA 硬件读取)**:全部 5 种用户层检测方法均无法检出(检测率 ~0%)。Device 类型总线事务完全旁路缓存层级,攻击侧在用户层检测面上取得完全优势 4. **防御需向上迁移**:在 DMA 模式下,唯一可行的检测方向是系统级监控——IOMMU/SMMU 审计捕获异常设备地址空间的页表映射操作,或总线级监控捕获 Device 类型事务的异常模式 攻击端从 v1.0 到 v4.0 的演进体现了内核攻击与用户层检测之间"攻高一尺,魔高一丈"的持续对抗:每一步攻击改进都绕过了一部分检测,最终 DMA 模式在现有用户层检测框架下形成完全不可检测的真空地带。 ### 7.3 攻防演进路线 ``` 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 审计、总线监控) ``` ### 7.4 未来方向 1. **攻击端进一步规避**: - WC 模式页表遍历改用 `MEMREMAP_WC` — 消除页表页缓存污染(代价:遍历性能大幅下降) - 读取后 `DC CIVAC` 自清洗 — 主动逐出 SLC 中的数据(代价:增加指令开销) - 跨 Cluster 攻击(big→LITTLE)— 利用不同 Cluster 的独立 SLC 规避缓存一致性嗅探 2. **防御端检测增强**: - 缩短探测间隔至 1ms,进一步提高命中率 - 结合 PMU + 缓存时序 + PerfEvent 做多模态交叉验证,降低假阳性(基线测试显示 PMU 单独使用存在 +85 的偶发假阳性) - 在多个 CPU 核心同时部署探测页,增加跨核覆盖 3. **PMU 检测的降噪**:REMOTE_ACCESS 的系统级假阳性可通过动态基线校准或白名单过滤 4. **系统级监控(对抗 DMA 模式的关键方向)**: - IOMMU/SMMU 审计:捕获异常设备地址空间的页表映射操作(`vmap` Device-nGnRnE 映射可被 IOMMU 记录) - 总线级监控:检测异常 Device 类型总线事务的频率和模式 - 利用 ARMv8.5 MTE(Memory Tagging Extension):设备支持时,内存标签验证可检测未授权的物理内存访问 --- ## 参考资料 - Linux Kernel 5.4 ARM64 页表实现:`arch/arm64/include/asm/pgtable.h` - KernelPatch KPM 框架:`<mark class="encrypted">a79K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6T1L8h3q4^5x3e0t1I4i4K6u0r3d9$3g2J5L8X3g2D9f1r3q4@1j5$3S2Q4y4U0l9`.</mark> - APatch Next:`<mark class="encrypted">b96K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6T1L8h3q4^5x3e0t1I4i4K6u0r3b7g2m8S2N6r3y4Z5i4K6j5H3</mark> - rwProcMem33 物理内存读写:`rwprocmem33_20250520/`(`get_task_proc_phy_addr`) - rwProcMem33 物理内存读写驱动:`rwprocmem33_20250520/rwProcMem33-master/rwProcMem33Module/rwProcMem_module/phy_mem.h`(`get_task_proc_phy_addr`、`read_ram_physical_addr`) - PMU 事件编码:ARM Architecture Reference Manual ARMv8, Chapter D7 - `access_process_vm` 内核实现:`mm/memory.c`, `mm/gup.c` --- *本文档随项目代码一起维护,文件路径:`/home/zzz/Desktop/kernel_read_detect/article.md`*
登录后可查看完整内容
传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!!
最后于
2026-6-9 16:26 被blackr0ck编辑 ,原因:
#基础理论
#程序开发
#系统相关
#源码框架
#其他
收藏
・
20
点赞
・
19
打赏
分享
分享到微信
分享到QQ
分享到微博
赞赏记录
参与人
雪币
留言
时间
Istaroth
感谢你的积极参与,期待更多精彩内容!
2026-9-5 17:28
无事不觉
非常支持你的观点!
2026-7-20 16:07
lingyi223
你的分享对大家帮助很大,非常感谢!
2026-7-15 11:38
镜中人24
感谢你的积极参与,期待更多精彩内容!
2026-7-15 09:10
wx_kx71647
感谢你的贡献,论坛因你而更加精彩!
2026-6-29 20:03
mb_heldrckv
为你点赞!
2026-6-28 12:55
zhczf
这个讨论对我很有帮助,谢谢!
2026-6-27 15:55
mb_stuvicca
你的帖子非常有用,感谢分享!
2026-6-23 17:21
mb_gosotzva
为你点赞!
2026-6-20 19:38
zhongdong
为你点赞!
2026-6-18 17:57
sk97
谢谢你的细致分析,受益匪浅!
2026-6-11 17:22
yuanyouran
+1
非常支持你的观点!
2026-6-11 14:28
我的小拇指啊
感谢你分享这么好的资源!
2026-6-10 22:56
东方玻璃
你的帖子非常有用,感谢分享!
2026-6-10 12:33
mb_iesvmzqc
非常支持你的观点!
2026-6-10 10:39
doduhuang
你的分享对大家帮助很大,非常感谢!
2026-6-10 10:16
马来
你的分享对大家帮助很大,非常感谢!
2026-6-10 10:10
安卓逆向test
感谢你的贡献,论坛因你而更加精彩!
2026-6-10 10:07
Yangser
你的帖子非常有用,感谢分享!
2026-6-10 09:50
查看更多
赞赏
×
1 雪花
5 雪花
10 雪花
20 雪花
50 雪花
80 雪花
100 雪花
150 雪花
200 雪花
支付方式:
微信支付
赞赏留言:
快捷留言
感谢分享~
精品文章~
原创内容~
精彩转帖~
助人为乐~
感谢分享~
最新回复
(
11
)
曦曦大牛子
雪 币:
20
能力值:
( LV1,RANK:0 )
在线值:
发帖
0
回帖
8
粉丝
0
关注
私信
曦曦大牛子
2
楼
牛而逼之
2026-6-9 22:20
0
Yangser
雪 币:
1953
活跃值:
(4173)
能力值:
( LV4,RANK:50 )
在线值:
发帖
11
回帖
129
粉丝
217
关注
私信
Yangser
3
楼
已严肃学习
2026-6-10 09:53
0
wx_插曲
雪 币:
411
活跃值:
(861)
能力值:
( LV2,RANK:10 )
在线值:
发帖
2
回帖
316
粉丝
24
关注
私信
wx_插曲
4
楼
已严肃学习
2026-6-10 12:26
0
墨穹呢
雪 币:
4704
活跃值:
(8122)
能力值:
( LV3,RANK:20 )
在线值:
发帖
2
回帖
224
粉丝
18
关注
私信
墨穹呢
5
楼
感谢分享
2026-6-12 00:07
0
hyz111
雪 币:
151
活跃值:
(1098)
能力值:
( LV2,RANK:10 )
在线值:
发帖
3
回帖
47
粉丝
2
关注
私信
hyz111
6
楼
严肃学习
2026-6-16 14:06
0
Midsw
雪 币:
29
活跃值:
(1200)
能力值:
( LV2,RANK:10 )
在线值:
发帖
0
回帖
114
粉丝
1
关注
私信
Midsw
7
楼
牛而壁纸。
2026-6-16 15:32
0
lixianhuei
雪 币:
1688
活跃值:
(2335)
能力值:
( LV2,RANK:10 )
在线值:
发帖
0
回帖
128
粉丝
0
关注
私信
lixianhuei
8
楼
6666
2026-6-28 16:47
0
Lthis
雪 币:
180
活跃值:
(770)
能力值:
( LV4,RANK:50 )
在线值:
发帖
4
回帖
162
粉丝
5
关注
私信
Lthis
1
9
楼
看得出来,有了AI后,大家的学习速度、文章完整度都强了不少
2026-6-29 18:10
0
djog
雪 币:
204
活跃值:
(175)
能力值:
( LV2,RANK:10 )
在线值:
发帖
0
回帖
68
粉丝
0
关注
私信
djog
10
楼
本文档随项目代码一起维护,文件路径:/home/zzz/Desktop/kernel_read_detect/article.md
问一下 这些文档在哪里可以得到????
2026-6-29 23:54
0
灵幻空间
雪 币:
1198
活跃值:
(3192)
能力值:
( LV4,RANK:40 )
在线值:
发帖
13
回帖
91
粉丝
47
关注
私信
灵幻空间
11
楼
可以的不错
22小时前
0
一只鸭子
雪 币:
331
活跃值:
(915)
能力值:
( LV3,RANK:20 )
在线值:
发帖
15
回帖
57
粉丝
74
关注
私信
一只鸭子
12
楼
超级大手子
12小时前
0
游客
登录
|
注册
方可回帖
回帖
表情
雪币赚取及消费
高级回复
返回
blackr0ck
2
8
发帖
16
回帖
90
RANK
关注
私信
他的文章
[原创]一加ACE5至尊版 kernel 6.6.89尝试利用GhostLock / IonStack 进行root提权
48485
[原创]领域特定虚拟机DSVM指令完整还原
3806
[原创]APatch KPM 开发:绕过 DirtySepolicy / Duck Detector 的 SELinux Root 检测
3057
[原创]ARM64 页表断点——基于 PTE 缺页异常的透明拦截、上下文捕获与寄存器实时修改
15255
[原创]内核驱动级内存读写的用户层检测与绕过:一次攻防对抗实践
42683
关于我们
联系我们
企业服务
看雪公众号
专注于PC、移动、智能设备安全研究及逆向工程的开发者社区
看原图
赞赏
×
雪币:
+
留言:
快捷留言
为你点赞!
返回
顶部