本文记录一次基于 Pixel 7 / Android 14 / FolkPatch KPM 的 wxshadow-style Hook 复刻实验。文章重点是设计思路、验证路径和问题定位复盘,范围限定为 测试 APP 页级目标、PTE 事务和异常分流。
GitHub: wxshadow hook 原理 POC
最初目标很朴素:拜读了 linux/android 利用shadow内存无痕hook方法 、 Android 内核无痕 Hook 框架设计思路和避坑指北 、 Android内核无痕Hook理解和感悟 几篇文章后,萌生了针对手头的机器,复刻 wxshadow 思路的 KPM 的实验的想法。做着做着发现,原理并不难,都是些计算机组成原理和操作系统的基础知识,真正困难的部分反而集中在 PTE 修改之后的一系列状态管理问题:
这次实验的最终产物为一个 概念验证级别的 wxshadow 思路的复现代码 :目标页由测试 App 主动选定,也就是测试 App 主动调用启动 hook(通常 hook 都是外部侵入性的,但这里不影响我们的验证),KPM 只接受固定 UID、owner TGID、owner mm、token、slot、generation 都匹配的请求。这个边界让实验可以被稳定复现,也让每一次失败都有比较清楚的异常归因;若要扩展到更通用的目标描述符或内核注入框架边界,还需要补齐目标准入、生命周期和检测面验证。
最终成果已经开源到 GitHub,各位有兴趣的佬可以直接在此基础上继续开发,或者根据手头的机器更改更好的实现路线:
开始之前我们先快速过一遍原理,几个大佬的文章我拜读了好几遍,细节分支思路上有些不同,但总体的思路是一致的。通过页 fault 和 页 切换这些用户空间无法直接感知的手段实现 hook。
我们先快速复习一下计算机组成原理和操作系统中非常重要的模块:虚拟内存。
虚拟内存很好的解决了权限管理和内存时空利用率的问题。原理很简单,用一个数据结构记录虚拟内存到物理内存的映射关系。程序只能看到虚拟地址,读/写内存时,物理内存页存在就直接用,不存在就出 页缺失 fault,然后内核出面分配物理页并维护映射关系。重要的是,这一过程是在内核中发生的,用户空间的程序不用特别的手段(比如一些侧信道手段),是没办法直接感知到的。
好了,原理很简单,但实现起来坑就很多了。二分搜索的原理也很简单,但现在让在座的各位立刻手写一个正确的二分搜索算法,估计有不少佬都开始汗流浃背了。
本文把 wxshadow-style Hook 拆成三个问题。
第一部分是 如何触发 hook 安装 。常见路线有硬件断点和 UXN fault。硬件断点命中精确,但资源和上下文管理成本高;UXN fault 通过把目标页临时设为不可执行,在目标代码第一次执行时进入 IABT,再由 KPM 校验 mm + VA + generation 后完成切换。本文选择 UXN 作为安装触发入口。
第二部分是 如何实现执行视图切换 。可选路线包括 visible clone、raw two-PFN 和 BRK/step descriptor。本文先用 visible clone 验证最小闭环,再升级到 raw two-PFN:同一 VA 的 PTE 在 original PFN 和 shadow PFN 之间切换,执行时进入 shadow 页。
第三部分是 如何实现读取视图隐藏 。核心目标是执行看 shadow,读取尽量看 original。本文采用 translation-DABT read-cycle,并进一步验证 Pixel 7 上的 wxshadow-style raw-XOM permission-DABT original-read。
整个实验拆成两层。
第一层是 Lab App。它是我们的测试 App,负责创建受控目标页、触发读写执行、通过 KernelPatch SuperCall 发送命令、记录用户态观测结果。目标地址来源限定为 Lab App 自己分配或映射的页。
第二层是 KPM。KPM 负责:
最后代码里绕不开的结构有这些:
这就是本文里的 shadow page(这个词是从另一位佬 kkkbbb 那里借用过来的,有兴趣的可以去看看 rustfrida 那个项目):执行时切到 shadow PFN,必要时恢复 original view。
这部分将原本简单的页对象升级成能承载更多管理信息的描述符结构。
读这段结构体时,可以按三条线拆开:
这组结构体对应了几个改造点:
有了这层 page-owned descriptor,后面的改造才有承载点。比如 patch record 从全局数组改成 page->patch_records[],GUP/fork/exit 也从 “当前 raw page” 改成 “先找到命中的 slot,再处理这个 slot 的状态”。
先放全图。后面的章节基本都在拆这条执行路径:页准入、PTE 事务、异常路由、original-read window、生命周期清理。
这张图里有三个控制点。
第一,所有入口都先落到 page descriptor。IABT、DABT、GUP、fork、exit、prctl 这些路径都要重新确认 owner mm、page VA 和 generation,命中后才进入对应 handler,其他情况进入 reject path。
第二,PTE 切换只在 page record 状态允许时发生。SOURCE_UXN、SHADOW_RX、ORIGINAL_READ、RESTORED 这些状态和 live PTE 必须对得上,状态提交放在 PTE 事务成功之后。
第三,清理路径和功能路径同等重要。显式 clear、owner exit、owner exec、module unload 都要走 exact-original restore、record release、worker drain 和 module list 检查,最终以 modules=empty、warn_count 保持原值作为收口证据。
最早的稳定路线选择 visible-clone,raw PTE 放到后面推进。也就是先验证执行视图切换,再验证读视图隐藏。
实验的 M4 阶段里,源页是一个固定的 MOV W0,#42; RET,也就是返回数字 42,clone 页是 MOV W0,#99; RET,也就是返回 99。KPM 先把 source PTE 改成 UXN,让第一次执行触发 instruction abort,再在异常处理里把 PC 重定向到 clone 页。
clone 是普通可见映射,隐蔽性很弱。它在早期仍然有用:
后续 raw two-PFN 才开始做同 VA 的 PFN 切换:
这一步证明的是“同一 VA 的执行视图可切换”。证明范围限定在 execution shadow:raw two-PFN 激活后,普通读也会看到 shadow PFN;完整 original-read 隐藏需要额外的读 fault 分流能力。
一开始我也容易把 raw PTE 理解成“找到 PTE,然后写一个新值”。上机测试以后,这个抽象很快不够用。于是后面收敛成一个小事务:
这里有几条硬约束:
这一段的验证同时覆盖返回值和 live-PTE snapshot。held 状态里要能看到 mmap_lock=read、pte_lock=held、live_match=1、live_state=source_uxn、 record_state=source_uxn。行为层再用 42 -> 99 -> 42、read-cycle 99 -> original-read -> 99、最终 modules=empty warn_count 保持原值作为最终的证据。
raw PTE 相关代码被放进 kpm/r0lab_raw_compat.c,主 KPM 文件只通过 r0lab_raw.h 里的小 ABI 调用它。这样做的原因是,目标内核头、mm_struct 布局、PTE bit 和 KernelPatch KPM 的紧凑头文件混在一起会很难维护。
页表 walk 的入口先把 VMA 和 leaf PTE 形态收紧:
这段 walk 代码说明了准入边界。地址必须 4 KiB 对齐,VMA 必须完整覆盖这一页,vma->vm_mm 必须等于目标 mm。各级页表项需要通过 none/bad 检查,pud_sect() 和 pmd_sect() 会挡掉 section mapping,后续只处理 leaf PTE。函数返回时已经拿到 leaf PTE lock,调用者在这个锁内完成 live PTE 检查和替换。
真正替换 PTE 时,顺序固定成 clear、flush、sync、install:
这段替换代码把 break-before-make 落成三个动作:先 ptep_get_and_clear() 清掉旧 PTE,再对目标 VMA 做 flush_tlb_page(),最后 set_pte_at() 安装新 PTE。replacement 指向 shadow PFN 时额外同步 icache alias,让 shadow backing 写入后的 CPU 指令缓存状态与新字节保持一致。返回 EAGAIN 表示 clear 出来的旧 PTE 为空,调用方会把它当作并发变化处理。
状态切换函数只在 live PTE 命中预期以后提交内存状态。以 IABT 后切到 shadow 为例:
这段状态切换代码对应 IABT resume。入口要求 page->state 已经是 R0LAB_RAW_SOURCE_UXN,说明 source 页被主动设置为不可执行。随后在 PTE lock 内读取 live PTE,只接受 source_uxn_pte。替换成功后才更新 active_pte/state,测试脚本才能用 live-PTE snapshot 反查“记录状态”和“页表 现场”是否一致。
早期实现里,很多路径都围绕一个全局 raw page 对象发展。单页 smoke 测试往往丝滑通过,但一旦加入 fork、GUP、prctl、exit、patch record,所有状态都会挤在一个对象里:
这种结构能跑通 demo,但后续排查问题时定位非常头大。
于是后面改成 page-list-first,也就是固定两页 Lab slot,每个 slot 自己持有 generation、PFN、patch records、route counters 和 cleanup state。所有 callback 统一通过 target mm + page VA + generation 做路由。
改成 page-list-first 后,几件事变得清楚:
路由函数也按这个思路做。每个 hook callback 进来以后,先拿一个 r0lab_raw_hook_page_token,命中后进入本页处理,其他情况记录 reject counter 并沿用原路径:
这段 route 代码定了 callback 的统一准入规则。mm + address 先定位 slot;generation 用来过滤旧命令;transitioning 和 read/GUP/fork active flag 用来过滤嵌套 PTE 修改;armed/reserving/clearing/state 用来过滤生命周期窗口。r0lab_raw_saved_identity_matches() 再确认保存下来的 original/source-UXN/shadow PTE 都还指向声明时的 source PFN 或 shadow PFN。只有通过这些检查,callback 才会拿到 token 并进入后续 PTE 操作。route status 里的 route_identity_mismatch 用来记录这类身份不一致事件。
这个 token 让 callback 的安全边界变得具体:命中的是哪个 slot、哪个 generation、是否允许修改 PTE、是否正在 read/GUP/fork window 中,都在进入具体 hook 逻辑前完成检查。后面新增 prctl、syscall、GUP、fork、exit 路由时,都复用这套入口,减少了“某个 callback 自己判断一遍”的分叉。
patch record 也从全局状态移动到页对象里。一次 patch 只允许命中当前 owner mm、当前 slot、当前 generation,并且要求页处在 SHADOW_RX:
这段 admit 代码定死了 patch 入口的约束。调用者必须来自 owner 进程,token、 slot_id、generation、当前 mm 都要匹配。页必须处在 SHADOW_RX,并且 clear、transition、read-cycle、GUP、fork window 相关标志均为 inactive。满足条件后设置 transitioning,后面的 record upsert 和 shadow rebuild 才能作为一个原子阶段推进。
重建 shadow 字节时,先从原始页重新拷贝 range,再按 version 顺序应用活跃 record,最后同步 icache:
这段 rebuild 代码定死了 patch records 的合成规则。它先找出和本次 range 重叠的活跃 record,并按 version 从旧到新排序;然后用 original 页重新铺底,再叠加 seed 和 active records。拷贝时只写交集区间,通过范围约束防止一个窄范围 rebuild 覆盖相邻 patch。最后同步 icache alias,保证 shadow 页执行视图和 patch metadata 一致。
F3 阶段验证了两个 slot 的 patch record 独立性:slot0 和 slot1 可以分别 patch 到不同返回值,释放 slot0 时 slot1 保持原状态,slot0 填满 1024 条记录后第 1025 条返回 ENOSPC,slot1 的 metadata 保持原值。
wxshadow 最麻烦的地方之一,是执行时使用 shadow 页,读取时却只能看到 original 页。
早期主线选择 translation-DABT read-cycle,因为它最容易把故障来源控制在 Lab 边界内:
这个版本能证明“original-read window”这个状态机,但需要 Lab 主动制造 PROT_NONE。
后面我继续把触发源换成更贴近原版 wxshadow 的 raw-XOM read-fault。这里普通用户态 XOM probe 结果为 absent:Pixel 7 的 PROT_EXEC probe 记录为 user_xom_read_fault=absent。真正成立的是 Lab-owned raw PTE 路线:在已经激活的 shadow 页上清掉用户读侧,保留执行侧,让同一 VA 进入 shadow_xom 状态。
执行时读到的是 shadow 代码返回值 99;普通读取同一地址先打到 permission fault,KPM 打开一次 original window,所以读到原始指令字 52800540;下一次执行触发 IABT 后恢复 shadow,返回值又回到 99。
实验 S4 阶段做的是 BRK / single-step 方向。
最早的单点验证只证明固定地址、固定 X1 的写入。后来把它改成 page-owned descriptor:
BRK callback 和 single-step callback 从全局地址判断改为扫描当前 page table 里已 armed 的 descriptor。这样可以验证两页:
这里为了把断点能力限定在 page-owned descriptor 描述的范围内。加了负向测试:
负向 descriptor probe 通过后,才刷新 S4 ABI、BRK-only、step-only、raw-step、raw-reg、descriptor routing 和 full runner。
这次实验里,首次 PTE 修改没有成为主要风险点,反复触发设备重启的环节集中在生命周期。
反复出现的风险主要有三类:
FolkPatch manager 侧对 KPM .exit 返回 busy 采用有限处理策略。等 .exit 里才发现 worker 仍在运行、hook 仍在链上、in-flight handler 仍有计数,卸载窗口已经进入高风险状态。
后面改成这套策略:
显式 clear 是我们主动发命令,时机和线程都可控。owner exit 是目标进程正在 exit_mmap,此时 VMA、页表、mm 引用、hook callback 都处在敏感路径。
实验曾经在两页 owner-exit 路径上遇到 panic。pstore 里能看到 stopped CPU 落在:
这说明问题指向全局 exit_mmap hook 暴露面和 raw page teardown 的危险组合,需要从生命周期状态机处理。于是后面拆成了很多小片:
每一步只改一个变量,出现异常先分类,再进入下一步修改。
调试过程中,status、logcat、boot-id reader、maps reader 都可能成为扰动源。所以后面很多脚本会分成:
这些步骤数量较多,内核态调试里,“我想看一下状态” 本身就可能改变现场。
写这种代码堪比数学证明,既需要逻辑上完备,又需要有完整的测试约束、原因记录等。常规的先跑通再完善在这里并不太适用。
验证拆成三层,完整通过需要同时具备静态契约、单项 smoke 和最终压力门证据。
第一层是静态契约:
它验证前期机制梳理数量、文档锚点、脚本语法和禁用路径。
第二层是单项 device smoke,例如:
第三层是最终 F7 lifecycle stress runner:
这里放几个测试过程中的证据链例子:
嵌套 full runner:
manifest 的价值在于留下每个 phase 的证据索引,用可追溯日志替代终端回忆。后面写文章、复盘、回退,都可以从 manifest 追到具体子测试日志。
读者通常会期待最后来一个任意 PID、任意地址、任意 patch 的实现。但本次实验把实验停在了概念验证。
概念验证级别的输入是:
任意 PID/VA 的输入是:
后者看起来简单,上机前要先回答这些问题:
这些已经超出原理验证的实现,而更接近通用内核注入框架。为了让文章和产物都能被稳定复现,本文选择 typed target descriptor,范围限定在受控 process/address。(人话:做起来太复杂,而且也过不了审)
因此,本实验的边界是:
硬件特性、内核配置或触发源一变,wxshadow-style 设计会出现新的入口。当前 Pixel 7 实验外推到其他设备前需要重新验证。
EPAN 是 Enhanced Privileged Access Never。Linux arm64 的 CONFIG_ARM64_EPAN 说明里把它描述为:让 PAN 可以和 execute-only 映射一起使用。历史上 arm64 曾经支持过用户态 execute-only PTE,但后来在缺少合适硬件保护时,内核 EL1 对这种用户映射的误读不一定会被 PAN 拦住,所以本实验主线曾回退到 PROT_EXEC 隐含 PROT_READ 的保守行为。
如果某台设备同时满足下面条件:
满足这些条件后,hidden-read 设计可以更接近“正常执行 shadow,普通读触发 fault 后切回 original”的形态。它的好处是:不必像本文一样让 Lab App 主动 PROT_NONE 制造 translation-DABT,普通读本身就可能成为 original-read window 的入口。
不过验证门槛会同步抬高:
EPAN 提供的是更好的硬件前提,page ownership、PTE 事务和 lifecycle cleanup 仍然逃不掉。
另一条更贴近 wxshadow 的路线,是直接构造 raw XOM descriptor:例如清掉 PTE_USER、保留执行权限,让同一页保持“EL0 可执行但不可直接读”的状态。
这种设计如果成立,普通读取 shadow 页时会进入 permission-fault 分支,KPM 可以在 data abort 里短暂恢复 original,再在下一次 IABT 或状态机里回到 shadow。
Pixel 7 上这条路线已经被收进 Lab 成品,但边界很窄:
核心代码可以压缩成三段看。
第一段构造 shadow_xom_pte。它沿用 original PTE 的内存属性,PFN 换成 shadow PFN,然后清掉用户读侧并保留 EL0 执行侧:
第二段打开 original-read window。这里要求当前 live PTE 确实还是 shadow_xom_pte,保证异常处理时状态仍由当前 page record 持有:
第三段是异常路由。SHADOW_XOM 只通过显式 flag 放给 permission-DABT 读分支,GUP、fork、普通 patch、syscall、prctl 等旧路径保持原有状态处理:
这条路径只接受当前 Lab App 已声明、已经 raw two-PFN 激活的页。KPM 继续用 owner mm + VA + generation 来筛 fault;ESR/FAR 需要同时满足 WnR=0、permission fault、命中当前 page record,并且当前页处在 SHADOW_XOM。translation-DABT read-cycle 和 write-release 分支只接受 SHADOW_RX。通过后,handler 把 PTE 临时切回 original+UXN 并 skip origin,让读指令原地重试。下一次执行触发 IABT,再恢复 shadow_xom。
permission-DABT 路线解决的是触发源问题。早期版本从:
升级到:
Pixel 7 的 raw-XOM gate 已经证明了这个升级路径。它的价值是减少用户态显式 mprotect(PROT_NONE) 参与,让普通读本身成为 data abort 入口。进入更大主线前还要继续证明:
它只改变 read-cycle 的入口,生命周期难度还在。
如果后续换到支持读侧 fault 的设备,成品形态可以进一步收敛到 iTLB/dTLB split-view:
这个写法更接近 wxshadow 的目标态:同一个 VA 执行时走 shadow,普通读触发 data abort 后短暂回到 original,再由下一次执行异常恢复 shadow。它依赖硬件、 内核配置和异常分类同时过关,需要通过独立 read-fault gate 证明。
我们做过 handle_mm_fault 相关探索,但当前主线只把它放在 observe-only 和 blocked diagnostic 位置。原因是:raw VA 的 PROT_NONE 读在 Pixel 7 上会更早 被 do_mem_abort/badaccess 路径处理,正向进入 handle_mm_fault 的 raw-page read route 保持 blocked 分类。
如果某个内核/VMA 组合能稳定让目标读进入 handle_mm_fault,可以把 original-read window 放到更接近 Linux fault core 的位置。好处是 VMA、fault flags、page-table walk 语义更集中;风险是锁顺序、递归 fault、GUP/fault 重入、VMA 生命周期都会变复杂。可接受的验证顺序应该是:
在这组验证完成之前,handle_mm_fault 保持 hidden-read 探索项状态。
wxshadow 类实现通常会关注外部读:GUP、/proc/pid/mem、ptrace、VMA walk 等路径看到的是 original 还是 shadow。本文只验证了 Lab 边界里的 GUP hook 窗口,实现范围限定在 Lab-bounded GUP smoke。
继续做之前,先把“读”分类,再选择对应 hook:
每一类读的锁、上下文、是否允许睡眠、是否可以 safely retry 都不同。统一隐藏已经超出小补丁范围,会发展成新的检测面工程。
PAC、BTI、MTE 不直接提供 shadow page,但会改变 Hook 的可用设计空间。
PAC/BTI 对 arbitrary patch 和 trampoline 影响最大。如果要把固定 MOV W0,#42; RET 扩展成任意函数 patch,需要处理 BTI landing pad、PAC 保护的返回/间接分支、PC-relative load、literal pool、跨页指令和 异常返回 ABI。PAC/BTI 主要约束 patch engine,使它更像一个受约束的 AArch64 relocator。
MTE 更适合当调试和防错工具:可以帮助暴露 shadow backing、metadata、teardown 里的野指针或 use-after-free 风险。PTE ownership 和读路径隐藏仍然需要独立证明。
如果控制面下移到 hypervisor/EL2,可以在 stage-2 页表里做另一层 shadow:guest 内核看到的是一套页表,stage-2 决定真实物理页。这种设计对 /proc 或 guest page-table walk 的隐藏性可能更强。
但它已经超出 FolkPatch KPM 复刻项目,进入 hypervisor 级内存虚拟化项目范围。验证对象会变成 VMID、stage-2 TLB、guest/host cache coherency、EL2 trap latency、Android 启动链和 SELinux/AVB 兼容性。本文把它作为后续方向,KPM 成品路线仍然聚焦 FolkPatch 环境。
还有一条偏工程化的路线:暂时放弃 page-level hidden-read,转向把 raw page table 和 BRK/step descriptor 扩展成小型 DBI/trampoline 引擎。它能覆盖更多函数形态, 但问题会从“PTE 状态机”转成“指令重写正确性”:
DBI/trampoline 更适合做功能型 Hook 框架,不适合拿来证明 wxshadow 的 shadow page 隐藏性。
这次复刻最大的收获是:内核 Hook 的难点集中在状态转换治理。每个状态转换都要能讲清楚、测出来,并在异常时可回退。
最后稳定下来的原则:
最终产物定位为一个能在固定 Pixel 7/FolkPatch/Lab App 环境里复现 wxshadow 核心状态机的研究样品。通用隐蔽框架属于后续研究方向,并不在本文讨论范围内。这个实验已经证明,page ownership、raw PTE transaction、shadow execution、translation-DABT original-read、raw-XOM permission-DABT original-read、descriptor routing 和 lifecycle cleanup 可以拆成可验证阶段。
能力
状态
说明
固定 Lab 页 IABT 激活
支持
source UXN 后通过 do_mem_abort 进入 shadow
raw two-PFN 同 VA 执行切换
支持
execution view 可切到 shadow PFN
original-read window
支持,受控
通过一次性 translation-DABT read-cycle
raw-XOM / PTE_USER clearing
支持,Pixel 7 Lab 边界
同一 VA 执行走 shadow_xom,普通读触发 permission-DABT 后读 original,下一次 IABT 恢复 shadow
page identity / VMA 观测
支持,守卫态
route 发 token 前校验保存态 PFN/PTE 身份,raw slot live pte 输出 vma_match、vma_flags、pte_match、pfn_match 和 identity_match
两页 page table
支持
slot0/slot1 独立 generation/PFN/state
page-local patch records
支持
每页 1024 条记录,版本化 rebuild
GUP/fork/fault/abort/write/exit 路由
支持,Lab 边界
通过 target-mm + page-record 路由
S4 BRK/step descriptor
支持,Lab 边界
包含两页 routing 和负向 descriptor controls
owner exit / exec / ENOMEM / interrupted arm
支持
F7 lifecycle stress 验证
arbitrary PID/VA
未纳入
本文成品目标限定在 Lab-declared page
通用 iTLB/dTLB split-view
未纳入
当前证明范围是 Pixel 7 Lab-owned raw-XOM 页,通用设备和外部读者需要另设 gate
/proc / ptrace / VMA 隐蔽
未纳入
属于广义检测面治理
tag: wxshadow-f5d2-route-identity-vma-20260725
base tag: wxshadow-final-lab-page-table-20260725
device: Pixel 7 panther / Android 14
kernel: 5.10.198-android13-4-00050-g12f3388846c3-ab11920634
runtime: FolkPatch 50ac6,d01
session
lab_uid
owner_tgid
owner_mm
token
raw_page_slot[N]
slot_id
generation
owner_mm
source_va
source_pfn
shadow_pfn
original_pte
state
patch_records[1024]
route_counters
#define R0LAB_RAW_PAGE_SLOT_CAPACITY 2U
#define R0LAB_PATCH_RECORD_CAPACITY 1024U
#define R0LAB_PATCH_DIRTY_BITMAP_SIZE (R0LAB_RAW_PAGE_SIZE / 8U)
enum r0lab_raw_state {
R0LAB_RAW_EMPTY = 0 ,
R0LAB_RAW_CAPTURED,
R0LAB_RAW_SOURCE_UXN,
R0LAB_RAW_SHADOW_RX,
R0LAB_RAW_ORIGINAL_STEP,
R0LAB_RAW_RESTORED,
R0LAB_RAW_POISONED,
R0LAB_RAW_ORIGINAL_READ,
R0LAB_RAW_SHADOW_XOM,
};
struct r0lab_raw_page {
void *mm;
unsigned long address;
void *shadow_kaddr;
unsigned long source_pfn;
unsigned long shadow_pfn;
unsigned long original_pte;
unsigned long source_uxn_pte;
unsigned long shadow_rx_pte;
unsigned long shadow_xom_pte;
unsigned long active_pte;
unsigned long read_cycle_saved_pte;
unsigned long read_cycle_active;
unsigned long gup_hide_active;
unsigned long fork_hide_active;
unsigned long state;
};
struct r0lab_page_record {
unsigned long source_address;
unsigned long source_pfn;
unsigned long shadow_pfn;
uint64_t generation;
uint8_t backend;
uint8_t state;
};
struct r0lab_patch_record {
uint16_t offset;
uint16_t length;
uint8_t active;
uint64_t version;
void *data;
};
enum r0lab_raw_hook_kind {
R0LAB_RAW_HOOK_NONE = 0 ,
R0LAB_RAW_HOOK_ABORT,
R0LAB_RAW_HOOK_FAULT,
R0LAB_RAW_HOOK_GUP,
R0LAB_RAW_HOOK_FORK,
R0LAB_RAW_HOOK_SYSCALL,
R0LAB_RAW_HOOK_PRCTL,
R0LAB_RAW_HOOK_EXIT,
};
struct r0lab_raw_hook_page_token {
uint16_t slot_id;
uint64_t generation;
struct r0lab_raw_shadow_page *page ;
enum r0lab_raw_hook_kind kind ;
};
struct r0lab_raw_shadow_page {
struct r0lab_page_record record ;
struct r0lab_raw_page raw ;
uint64_t generation;
uint16_t slot_id;
bool reserving;
bool armed;
bool clearing;
bool transitioning;
bool mm_count_owned;
bool mm_users_owned;
bool hook_installed;
bool gup_hook_installed;
bool fork_hook_installed;
bool fault_hook_installed;
bool exit_hook_installed;
bool syscall_hook_installed;
bool prctl_hook_installed;
struct r0lab_patch_record patch_records [R0LAB_PATCH_RECORD_CAPACITY ];
uint16_t patch_rebuild_order[R0LAB_PATCH_RECORD_CAPACITY];
uint8_t patch_dirty[R0LAB_PATCH_DIRTY_BITMAP_SIZE];
uint64_t patch_version;
uint16_t patch_record_slots;
uint16_t patch_active_count;
struct r0lab_raw_hook_route_stats hook_route_stats ;
};
struct r0lab_raw_page_table {
struct r0lab_raw_shadow_page slots [R0LAB_RAW_PAGE_SLOT_CAPACITY ];
uint16_t selected_slot;
struct r0lab_raw_hook_route_stats hook_route_miss_stats ;
};
normal execute -> source PFN -> returns 42
arm raw -> source PTE becomes SOURCE_UXN
IABT -> switch same VA to shadow PFN
shadow exec -> returns 99
clear -> restore original PTE
normal exec -> returns 42
admit page -> snapshot original PTE/PFN -> prepare shadow backing
walk target mm -> take leaf PTE lock -> check live PTE == expected
clear old PTE -> target-mm TLB invalidate
sync executable shadow aliases when needed -> install replacement PTE
commit page state
static int r0lab_raw_walk_locked (struct mm_struct *mm, unsigned long address,
struct vm_area_struct **vma_out,
pte_t **ptep_out, spinlock_t **ptl_out)
{
struct vm_area_struct *vma ;
pgd_t *pgd;
p4d_t *p4d;
pud_t *pud;
pmd_t *pmd;
pte_t *ptep;
spinlock_t *ptl;
if (!mm || !address || (address & (PAGE_SIZE - 1 )) ||
!vma_out || !ptep_out || !ptl_out)
return R0LAB_RAW_EINVAL;
vma = find_vma(mm, address);
if (!vma || address < vma->vm_start ||
address + PAGE_SIZE > vma->vm_end || vma->vm_mm != mm)
return R0LAB_RAW_ENOENT;
pgd = pgd_offset(mm, address);
if (pgd_none(*pgd) || pgd_bad(*pgd))
return R0LAB_RAW_ENOENT;
p4d = p4d_offset(pgd, address);
if (p4d_none(*p4d) || p4d_bad(*p4d))
return R0LAB_RAW_ENOENT;
pud = pud_offset(p4d, address);
if (pud_none(*pud) || pud_bad(*pud) || pud_sect(*pud))
return R0LAB_RAW_ENOENT;
pmd = pmd_offset(pud, address);
if (pmd_none(*pmd) || pmd_bad(*pmd) || pmd_sect(*pmd))
return R0LAB_RAW_ENOENT;
ptep = pte_offset_kernel(pmd, address);
ptl = r0lab_raw_pte_lockptr(pmd);
spin_lock(ptl);
*vma_out = vma;
*ptep_out = ptep;
*ptl_out = ptl;
return 0 ;
}
static int r0lab_raw_replace_locked (struct r0lab_raw_page *page,
struct mm_struct *mm,
struct vm_area_struct *vma,
unsigned long address, pte_t *ptep,
pte_t replacement)
{
pte_t old;
old = ptep_get_and_clear(mm, address, ptep);
flush_tlb_page(vma, address);
if (page && page->shadow_kaddr && page->shadow_pfn &&
pte_present(replacement) &&
pte_pfn(replacement) == page->shadow_pfn) {
r0lab_runtime_sync_icache_aliases(
(unsigned long )page->shadow_kaddr,
(unsigned long )page->shadow_kaddr + PAGE_SIZE);
}
set_pte_at(mm, address, ptep, replacement);
return r0lab_raw_pte_value(old) ? 0 : R0LAB_RAW_EAGAIN;
}
int r0lab_raw_activate_shadow (struct r0lab_raw_page *page)
{
struct mm_struct *mm ;
struct vm_area_struct *vma ;
pte_t *ptep;
spinlock_t *ptl;
pte_t current_pte;
pte_t shadow_rx;
int result;
if (!page || !page->mm || !page->address ||
page->state != R0LAB_RAW_SOURCE_UXN || !page->shadow_rx_pte)
return R0LAB_RAW_EINVAL;
mm = (struct mm_struct *)page->mm;
mmap_read_lock(mm);
result = r0lab_raw_walk_locked(mm, page->address, &vma, &ptep, &ptl);
if (result)
goto out_unlock_mmap;
current_pte = READ_ONCE(*ptep);
if (r0lab_raw_pte_value(current_pte) != page->source_uxn_pte) {
result = R0LAB_RAW_EAGAIN;
goto out_unlock_pte;
}
shadow_rx = r0lab_raw_pte_from_value(page->shadow_rx_pte);
result = r0lab_raw_replace_locked(page, mm, vma, page->address, ptep,
shadow_rx);
if (!result) {
page->active_pte = page->shadow_rx_pte;
page->state = R0LAB_RAW_SHADOW_RX;
}
out_unlock_pte:
spin_unlock(ptl);
out_unlock_mmap:
mmap_read_unlock(mm);
return result;
}
static int r0lab_raw_page_find_for_hook_locked (
enum r0lab_raw_hook_kind kind, void *mm, unsigned long address,
uint64_t generation, unsigned int route_flags,
struct r0lab_raw_hook_page_token *token)
{
struct r0lab_raw_shadow_page *page ;
bool wrong_state;
bool busy;
if (token) {
token->slot_id = R0LAB_RAW_PRIMARY_SLOT;
token->generation = 0 ;
token->page = NULL ;
token->kind = R0LAB_RAW_HOOK_NONE;
}
page = r0lab_raw_page_find_by_mm_addr_locked(mm, address);
if (!page) {
r0lab_raw_hook_route_reject_locked(NULL , true , false , false ,
false , false );
return R0LAB_ENOENT;
}
if (!mm || page->raw.mm != mm) {
r0lab_raw_hook_route_reject_locked(page, false , true , false ,
false , false );
return R0LAB_EPERM;
}
if (generation && page->generation != generation) {
r0lab_raw_hook_route_reject_locked(page, false , false , true ,
false , false );
return R0LAB_EAGAIN;
}
busy = page->transitioning ||
((route_flags & R0LAB_RAW_HOOK_ROUTE_MUTATING) &&
(page->raw.gup_hide_active || page->raw.fork_hide_active ||
page->raw.read_cycle_active));
if (busy) {
r0lab_raw_hook_route_reject_locked(page, false , false , false ,
true , false );
return R0LAB_EBUSY;
}
wrong_state = !page->armed || page->reserving || page->clearing ||
!page->generation ||
!r0lab_raw_hook_route_state_allowed(page, route_flags);
if (wrong_state) {
r0lab_raw_hook_route_reject_locked(page, false , false , false ,
false , true );
return R0LAB_EAGAIN;
}
if (!r0lab_raw_saved_identity_matches(&page->raw)) {
r0lab_raw_hook_route_identity_reject_locked(page);
return R0LAB_EAGAIN;
}
++page->hook_route_stats.route_hits;
if (route_flags & R0LAB_RAW_HOOK_ROUTE_MUTATING)
page->transitioning = true ;
if (token) {
token->slot_id = page->slot_id;
token->generation = page->generation;
token->page = page;
token->kind = kind;
}
return 0 ;
}
static int r0lab_raw_slot_patch_admit_locked (
uint64_t token, uint16_t slot_id, uint64_t generation,
unsigned int offset, unsigned int length, struct mm_struct *current_mm,
struct r0lab_raw_shadow_page **out_page)
{
struct r0lab_raw_shadow_page *page ;
if (slot_id >= R0LAB_RAW_PAGE_SLOT_CAPACITY || !length ||
offset >= R0LAB_RAW_PAGE_SIZE ||
length > R0LAB_RAW_PAGE_SIZE - offset)
return R0LAB_EINVAL;
page = r0lab_raw_page_slot_locked(slot_id);
if (!g_session.active || g_session.token != token ||
g_session.owner_tgid != r0lab_current_tgid() || !current_mm ||
page->raw.mm != current_mm)
return R0LAB_EPERM;
if (!page->armed || page->reserving || page->clearing ||
page->transitioning || page->generation != generation ||
page->raw.state != R0LAB_RAW_SHADOW_RX || !page->raw.shadow_kaddr ||
page->raw.gup_hide_active || page->raw.fork_hide_active ||
page->raw.read_cycle_active)
return R0LAB_EAGAIN;
page->transitioning = true ;
*out_page = page;
return 0 ;
}
static int r0lab_raw_rebuild_patch_range (struct r0lab_raw_shadow_page *page,
unsigned int offset,
unsigned int length)
{
uint16_t *order;
void *source_kaddr;
unsigned int order_count = 0 ;
unsigned int range_end;
unsigned int index;
if (!page || !length || offset >= R0LAB_RAW_PAGE_SIZE ||
length > R0LAB_RAW_PAGE_SIZE - offset)
return R0LAB_EINVAL;
if (!page->raw.shadow_kaddr || !g_sync_icache_aliases)
return R0LAB_ENOSYS;
source_kaddr = r0lab_raw_source_kernel_address(&page->raw);
if (!source_kaddr)
return R0LAB_EFAULT;
order = page->patch_rebuild_order;
range_end = offset + length;
for (index = 0 ; index < page->patch_record_slots; ++index) {
unsigned int insert_at;
if (!r0lab_patch_overlaps(&page->patch_records[index], offset,
length))
continue ;
insert_at = order_count;
while (insert_at &&
page->patch_records[order[insert_at - 1U ]].version >
page->patch_records[index].version) {
order[insert_at] = order[insert_at - 1U ];
--insert_at;
}
order[insert_at] = (uint16_t )index;
++order_count;
}
memcpy ((char *)page->raw.shadow_kaddr + offset,
(char *)source_kaddr + offset, length);
r0lab_raw_apply_seed_range(page, offset, length);
for (index = 0 ; index < order_count; ++index) {
const struct r0lab_patch_record *record =
&page->patch_records[order[index]];
unsigned int record_end = (unsigned int )record->offset +
record->length;
unsigned int copy_start = offset > record->offset ?
offset : record->offset;
unsigned int copy_end = range_end < record_end ?
range_end : record_end;
if (copy_end <= copy_start)
continue ;
memcpy ((char *)page->raw.shadow_kaddr + copy_start,
(char *)record->data + copy_start - record->offset,
copy_end - copy_start);
}
g_sync_icache_aliases((unsigned long )page->raw.shadow_kaddr + offset,
(unsigned long )page->raw.shadow_kaddr + range_end);
return 0 ;
}
[内核课程]《Windows内核攻防实战》!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。
最后于 14小时前
被Ivory0编辑
,原因: