代码仓库:<48dK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6^5K9h3q4G2K9$3q4F1k6$3S2#2j5W2)9J5c8Y4m8@1k6h3S2G2L8$3E0W2M7W2)9J5y4X3N6@1i4K6y4n7i4@1g2r3i4@1u0o6i4K6R3^5c8#2m8x3i4K6u0V1x3W2)9J5k6e0m8Q4c8f1k6Q4b7V1y4Q4z5o6V1`.
环境基线:Xiaomi M2102K1AC / Android 13 (API 33) / Kernel 5.4.233 / APatch v0.12.2 (KernelPatch 框架)
代码共 ~1800 行 KPM + ~3000 行 Python host。文中代码片段可直接复用。
起点是看雪 thread-290718 第 7.2 节提出的"方案 C"骨架:
查名片,找老巢:读取目标 Java 方法 ArtMethod 结构体中的 entry_point_from_quick_compiled_code_ 指针。
布设隐形陷阱:不对这个指针做任何修改。直接拿着真实的指令地址,呼叫内核 KPM 在这里拉起 UXN 高压电网。
这句话给出了方向但没落地。本文记录把它做成生产级框架(能在 aweme 这种 7000+ VMA、高密度的商用 APP 上稳定跑)的全过程,重点是可复现的工程细节:
读完应该能复现整个框架或独立实现同样的机制。
目标效果的一组数据:
以 stealth 为唯一设计目标的 ARM64 Android hook 框架,三条硬红线:
ARM64 的页表项(PTE)是 64 位,相关的几个位:
UXN = 1 时,EL0(用户态)从该页取指令 → Instruction Abort → 进入内核 do_mem_abort。数据读写不受影响。
关键:只翻这 1 个 bit,物理页帧、其他权限、属性都不变。对反作弊读页内容完全透明 —— 他读的还是原字节。
这是 7.2 方案的物理基础。
APatch 把 Linus 的 kernel 补过一个 patch 后,提供自定义内核模块 (KPM) 运行环境:
我们的 KPM 主入口如下:
用户态工具 ptehook_ctl 负责:
Shellcode 和 DBI 重编译代码需要放在目标进程可执行的内存里。普通 mmap 出来的内存:
要规避这些,必须绕过 VMA 子系统,直接在进程页表里插 PTE。
pte_template 参数很关键 —— 是从邻居 VMA 抓来的 PTE 作为模板:
为什么这样做?因为 PTE 里的很多属性(MAIR index、NS、SH 等)来自 CPU 内存模型,直接瞎填会得到 uncached 或错误 shareability 的页。从邻居继承保证我们的 ghost 页和 libart r-xp 在 cache/coherence 行为上一致。
早期版本有个实际上机才发现的 bug:
在 aweme 这种密集进程里,num_pages=8(DBI 最坏展开)时,单页空洞不够。安装第 2 页 PTE 时撞到相邻 VMA → -EEXIST → 回滚 → -ENOSPC。
修复:
do_mem_abort 是 ARM64 内核处理用户态 fault 的入口。我们用 KernelPatch 的 hook_wrap3 把它 wrap 住:
UXN 是按页生效的。目标方法 entry_point 在某个 4KB 页上,但整页的所有函数调用都会触发 fault。比如 art_quick_to_interpreter_bridge 和 art_quick_imt_conflict_trampoline 可能在同一页。不 hook 这些 helper 的时候,他们的调用也会被我们的 UXN 挡住,必须能正确 fallthrough 到原逻辑。
DBI 的任务:把整页代码重编译一份到 ghost,所有 PC-relative 指令修复成能从 ghost 执行。
ADRP 是 PC-relative 的,在 ghost 位置执行时计算结果会错。解法是换成绝对地址加载:
B.cond 的 imm19 只够 ±1MB 范围。ADRP 修复过的代码在 ghost 里比原地址偏移大,原 B.cond 可能超范围。同时如果目标在同一 hooked 页,最好跳到 ghost 里对应位置(而不是原 VA → UXN 反弹)—— 这就是我们最重要的 bugfix(见第 7 节):
整个项目最深的一个坑。第一次崩的时候是 hook aweme 的 MSManager.frameSign,app 跑 5-20s 必崩:
第一步:反汇编崩点
libart.so 里 artInvokeInterfaceTrampoline+0xF8 反汇编:
X24 本应是个 mirror::Object*,但拿到了 0xaa1103e0 —— 完全不像 ART heap 指针(ART 用 TBI tag 0xb4 或 low-ram 0x70 开头)。
第二步:向上追 X24
X24 从 [X20] 读出来,X20 源自 X0。
看崩点 X20 = 0x787b81a058。这是 libart.so 里 cmp x0, x17 这条指令所在的 VA(offset 0x58)。也就是说 X0 入 BL 时是 libart 代码段 VA,根本不是对象。
第三步:看调用点
bl artInvokeInterfaceTrampoline 在 art_quick_imt_conflict_trampoline 里:
按 ART 约定,X17 在整个 trampoline 函数体内都是 IMT key。走到 mov x0, x17 时把 key 放 X0,BL 交给 artInvokeInterfaceTrampoline。
第四步:X17 什么时候不对的?
崩溃时 X17 = 0x787bd3336c = artInvokeInterfaceTrampoline 函数入口。这是 BL 展开成 MOVZ/MOVK X17 + BLR X17 之后的值,已经是 BL 本身污染的。X0 的值在 BL 之前就确定了。
真正要找的是 mov x0, x17 执行那一瞬间 X17 是什么。
X0 at crash = 0x787b81a058 = libart cmp x0, x17 指令地址。这不是巧合。X17 在 mov x0, x17 执行时就已经是 0x787b81a058 了。
第五步:dump ghost 字节
重现崩溃,保留 KPM 里的 ghost 物理页,通过 ghost-read 命令读出 8KB:
对照 offset_map 定位 b #-16 这条循环回跳指令在 ghost 里变成了什么:
装上:X17 = 0x73e761a058(= target page + offset 0x58 = libart 里 cmp x0, x17 的 VA)。
就是这里!b #-16 回跳到 0x21a058。target VA 和 ghost VA 距离 218MB > B 的 ±128MB 范围,DBI 展开成 MOVZ X17 + BR X17。展开过程把 X17 写成了跳转目标的绝对 VA。
BR 后 CPU 跳到原 VA 0x21a058,触发 UXN 反弹 → Pass 3 → ghost 对应位置继续跑。但 X17 已经被污染。后续 cmp x0, x17 / cbz x0 / mov x0, x17 全部用脏值,X0 拿到 0x787b81a058,传给 artInvokeInterfaceTrampoline,崩在 +248。
ARM AAPCS64 规定 X16/X17 是 IPR scratch,函数调用过程中可以被 linker 存根污染。但 ART trampolines 不遵守 AAPCS:它们用 X17 传 IMT key 贯穿整个函数体。DBI 只看指令语义不看 ART 调用约定,就用 X17 做了远跳 scratch。
彻底方案:intra-page 分支的目标翻译到 ghost VA,绝不用 far-jump 展开。
实现要点在 6.5 节的 recomp_bcond 里看得见:前向分支因为 offset_map 还没填,先 emit placeholder,加到 pending 队列,主循环结束后回填。
完整修复涉及 4 个 recomp 函数(B, B.cond, CBZ, TBZ)。recomp_b 的片段:
修复前:aweme 5-20s 必崩。
修复后:
267,735 次 Pass 3 命中,aweme 零崩溃。平均 22k 命中/秒吞吐。
DBI 修好后,又发现一个新问题:jit_watch 动态切换 hook 会导致内核 panic / 设备 reboot。
jit_watch 是后台 Python 线程,轮询 ArtMethod.entry_point。ART 会在运行中把方法从 Nterp 升级到 JIT,entry_point 随之变化 —— 我们的 hook 钉在旧 ep 就失效。watcher 检测到变化会:
aweme 高 Pass 3 流量下(22k/s),这个切换导致内核 panic。
uxn_unhook 做这些事:
tlbi vmalle1is 只保证未来的取指走 PTE(此时 PTE 已恢复原 libart 页)。但CPU 当前正在执行的指令序列是从 I-cache 里来的:
此时 ghost_free 把物理页还给 buddy allocator。Allocator 转手给下一个申请者,写入新数据。I-cache miss 时再从物理页取 → 拿到垃圾指令 → 用户态 UDF 或内核 panic。
原理:
on_each_cpu 通过 kallsyms_lookup_name("on_each_cpu") 在 KPM 初始化时拿到:
10 轮快速 rehook 压测(每 0.6s 改 ArtMethod+0x18,watcher 每次都检测到并重装):
设备 uptime 10+ 分钟无重启,aweme 存活。修复前同样负载必崩。
DEX 文件里方法用 method_idx 索引。ART 加载 DEX 后,每个方法对应一个 ArtMethod 结构体,在 [anon:dalvik-LinearAlloc] VMA 里。设备侧 C 程序用 process_vm_readv 扫 LinearAlloc,按 pattern 过滤:
aweme 的 LinearAlloc ~100MB,扫描 0.17s。
scanner 多候选时需要消歧。比如 method_idx=33853 在 aweme 里返回 5 个候选 —— 因为有多个 class 碰巧都有第 33853 个方法。消歧两层过滤:
Python 侧用 _ep_offset() / _af_offset() 取代所有硬编码:
device_scanner.c 也加了 CLI flag:
Python 侧根据 get_offsets() 构造这些 flag:
hide-vma 之类依赖 linked-list VMA 的命令在 maple tree 内核下直接拒绝:
Makefile.planc 关键行:
KPM unload 时会触发 cleanup_all_state,自动清所有目标进程的 ghost PTE + 物理页:
启动后输出:
另一个终端:
last_far=0x73e761a050 指向 art_quick_imt_conflict_trampoline 入口 —— 说明 aweme 的 IMT 分发密集命中这一页,DBI fallthrough 正常。
aweme 的坑:MSManager.frameSign 只是 wrapper,实际签名由 ms.bd.c.f2.frameSign 执行。java_hook_all 装在 MSManager 上大概率 on_call 不触发。
解法 1:ms.bd.c.g2$a 接口有多个实现类,DEX parse 时找出所有实现类,都 hook:
解法 2:native hook libmetasec_ml.so 的 JNI_OnLoad 里 RegisterNatives 表:
Python 封装返回结构化数据:
Python 自动 chunk:
29 个测试覆盖:
ART 13 Nterp 对"未 JIT 的纯 Java 方法"有极深的 fast path,Java-to-Java 调用不读 entry_point。尝试过:
只能 wait_jit=True 等 ART 自然 JIT(通常 10 次调用左右)。对 trivial 方法(比如 return 0)ART 拒绝 JIT,这种方法目前抓不到。
hide-vma / hide-range 依赖 linked-list VMA,kernel 6.1+ 改成 maple tree 后这两个命令会拒绝执行。核心 hook 路径(UXN / DBI / ghost)不受影响,因为 vm_area_struct 的 vm_start/vm_end 偏移一直稳定。
7.2 trap 模式下不支持。需要 LSPlant 风格的 ArtMethod 克隆(在 ART heap 里 alloc 新 ArtMethod、注册到 class_table、WriteBarrier),跨进程实现 ≈ 9 天工作量,ROI 不够。
按优先级:
不做的:
这个项目走下来最大的感受是 —— "方案 C"骨架简单,工程细节全在坑里。
几个坑的代价:
每一个坑都是 ART / Linux kernel / ARM64 语义交互的细节。特别想强调 DBI 的 X17 clobber —— 写 DBI 是有 textbook 的(怎么重编译 PC-relative),但没人告诉你 ART trampoline 的寄存器约定不遵守 AAPCS。X17 在 AAPCS 里可以随便 clobber,但在 ART 的 imt_conflict_trampoline 里它是神圣的 IMT key 载体。这种隐含约定只能靠实战踩出来。
ptehook 不是 Frida 替代品,它解决的是 Frida 解决不了的一个特定问题 —— stealth。如果要做的反作弊对抗需要零 .so 注入 / 零字节修改 / 零 ptrace 痕迹,这个框架的机制就是为你设计的。如果需要 JS 热更 / 完整 Java onEnter+onLeave / spawn 注入,老老实实用 Frida。
感谢 7.2 节那篇启蒙文章。没有"查名片、布陷阱"这个框架性思路,我不会往这个方向做。
代码仓库:<291K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6^5K9h3q4G2K9$3q4F1k6$3S2#2j5W2)9J5c8Y4m8@1k6h3S2G2L8$3E0W2M7W2)9J5y4X3N6@1i4K6y4n7i4@1g2r3i4@1u0o6i4K6R3^5c8#2m8x3i4K6u0V1x3W2)9J5k6e0m8Q4c8f1k6Q4b7V1y4Q4z5o6W2Q4c8e0y4Q4z5o6m8Q4z5o6u0Q4c8e0k6Q4b7f1y4Q4b7e0u0Q4c8e0S2Q4b7V1k6Q4z5p5g2Q4c8e0g2Q4z5f1u0Q4z5f1g2Q4c8e0g2Q4b7U0S2Q4z5e0k6Q4c8e0c8Q4b7V1q4Q4b7e0c8Q4c8e0k6Q4b7U0g2Q4z5o6q4Q4c8f1k6Q4b7V1y4Q4z5p5y4Q4c8e0c8Q4b7U0W2Q4z5f1k6Q4c8e0k6Q4b7f1y4Q4b7e0u0Q4c8e0S2Q4b7V1k6Q4z5p5f1`. issue / PR 修 bug。
| 指标 |
数值 |
| aweme Pass 3 fault 处理 |
267,735 次 / 30 秒 / 0 崩溃 |
| KPM 代码规模 |
~1800 行 .c |
目标进程 /proc/maps 里的痕迹 |
0 个新 VMA |
目标进程 TracerPid |
0 |
| ArtMethod 字节修改 |
0(默认模式) |
| 单元测试 |
29 个全过 |
| 检测手段 |
我们能否过 |
扫 /proc/pid/maps |
✅ 看不到 |
| 扫 TracerPid / ptrace_scope |
✅ |
dl_iterate_phdr 遍历 .so |
✅ |
CRC / MD5 of .text pages |
✅ 字节未改 |
扫 ArtMethod.entry_point 是否落在合法 oat 段 |
✅ 字段未改 |
扫 ArtMethod.access_flags ACC_NATIVE bit 变化 |
✅ |
/proc/self/pagemap PFN 对比(用户态) |
✅ 只翻 PTE 里 1 个 bit,PFN 不变 |
| Kernel 驱动扫 PTE.UXN |
❌ 能看到(但反作弊一般没这权限) |
| 时序检测(fault 后 μs 级延迟) |
⚠️ 首次取指有延迟,稳态影响小 |
| 维度 |
Frida |
LSPlant |
wxshadow+rustFrida |
ptehook |
注入 .so 到目标 |
✅ libfrida-agent |
✅ Zygisk |
✅ libagent |
❌ |
| ptrace 目标 |
✅ |
❌ |
✅ spawn |
❌ |
改 .text 字节 |
✅ inline |
✅ inline |
✅ (藏 shadow 页) |
❌ |
| 改 ArtMethod 字节 |
— |
✅ entry_point |
— |
❌ (默认) |
/proc/maps 可见 |
✅ |
✅ |
✅ |
❌ |
| JS 脚本热更 |
✅ QuickJS |
❌ |
✅ QuickJS |
❌ |
| Java onEnter+onLeave |
✅ |
✅ |
✅ |
⚠️ Native 支持 |
| 改 Java 入参 |
✅ |
✅ |
✅ |
❌ |
| Nterp-tier 方法 |
✅ |
✅ |
✅ |
⚠️ 需 wait_jit |
| Spawn 注入 |
✅ |
✅ |
✅ |
❌ 只能 attach |
| pagemap PFN 一致 |
❌ |
❌ |
❌ |
✅ |
| 坑 |
发现代价 |
修法 |
| DBI X17 clobber |
3 次尝试 + dump ghost 字节 |
intra-page ghost VA + pending backpatch |
| Ghost UAF |
1 次 kernel reboot + 思考 |
on_each_cpu + ic ialluis 广播 |
| KPM 卸载不清 PTE |
多轮孤儿 PTE 占 gap |
cleanup_all_state 兜底 |
| Scanner 早期 cluster 误匹配 |
多次上机尝试 |
retry + access_flags 双层过滤 |
| ART Nterp 短路 |
ART 源码翻查 |
确认无解,走 wait_jit |
| 想了解 |
读这个 |
| UXN 陷阱怎么工作 |
test_kmod/ptehook_planc_v2.c::before_do_mem_abort |
| Ghost 如何分配 |
test_kmod/ghost_mm.c::ghost_alloc / find_hole_near |
| DBI 重编译每类指令 |
test_kmod/dbi_kern.c::recomp_* |
| DBI intra-page 回填 |
test_kmod/dbi_kern.c::dbi_recompile_page (末尾循环) |
| Ghost UAF 修复 |
test_kmod/ghost_mm.c::ghost_free_drain_ipi |
| shellcode 生成 |
pte_hookctl/shellcode.py::java_uxn_filter |
| Java hook 安装流程 |
pte_hookctl/ptehook/session.py::_install_java |
| ArtMethod 扫描消歧 |
pte_hookctl/ptehook/session.py::_resolve_artmethod |
| JIT 漂移监测 |
pte_hookctl/ptehook/session.py::_jit_watch_loop |
| KPM 卸载清理 |
test_kmod/ptehook_planc_v2.c::cleanup_all_state |
| 跨 Android 版本偏移 |
pte_hookctl/art_offsets.py::_OFFSETS_BY_API |
| 跨 kernel 版本探测 |
test_kmod/ptehook_planc_v2.c::probe_kern_layout |
| 指令 |
mask |
opcode pattern |
| NOP |
0xFFFFFFFF |
0xD503201F |
| B imm26 |
0xFC000000 |
0x14000000 |
| BL imm26 |
0xFC000000 |
0x94000000 |
| B.cond imm19 |
0xFF000010 |
0x54000000 |
| CBZ/CBNZ imm19 |
0x7E000000 |
0x34000000 |
| TBZ/TBNZ imm14 |
0x7E000000 |
0x36000000 |
| ADRP imm21 |
0x9F000000 |
0x90000000 |
| ADR imm21 |
0x9F000000 |
0x10000000 |
| LDR (lit) Wt |
0xFF000000 |
0x18000000 |
| LDR (lit) Xt |
0xFF000000 |
0x58000000 |
| LDRSW (lit) |
0xFF000000 |
0x98000000 |
| PRFM (lit) |
0xFF000000 |
0xD8000000 |
| LDR (lit) S/D/Q |
0x3F000000 |
0x1C000000 |
| BR Xn |
0xFFFFFC1F |
0xD61F0000 | (Rn<<5) |
| BLR Xn |
0xFFFFFC1F |
0xD63F0000 | (Rn<<5) |
| RET |
0xFFFFFFFF |
0xD65F03C0 |
| BTI jc |
0xFFFFFFFF |
0xD50324DF |
| MOVZ Xd, imm16, LSL (hw*16) |
|
0xD2800000 | (hw<<21) | (imm<<5) | Rd |
| MOVK Xd, imm16, LSL (hw*16) |
|
0xF2800000 | (hw<<21) | (imm<<5) | Rd |
bit 63: NSE (非安全扩展)
bit 62-61: 保留 / IGNORED
bit 60-59: Contiguous / DBM
bit 58-55: 软件位(内核可用)
bit 54: UXN (Unprivileged eXecute Never) ← ★我们的陷阱开关
bit 53: PXN (Privileged eXecute Never)
bit 52: Contiguous
bit 51-50: 保留
bit 50: GP (Guarded Page, ARMv8.5 BTI)
bit 49-12: 物理页帧号 PFN
bit 11-10: NG, AF
bit 9-8: SH (Shareability)
bit 7-6: AP (Access Permission)
bit 7 = AP[2]: 0 = RW, 1 = RO
bit 6 = AP[1]: 0 = kernel only, 1 = user+kernel
bit 5: NS
bit 4-2: Attr Index
bit 1-0: Block / Page type
#include <compat.h>
#include <kallsyms.h>
#include <hook.h>
#include "dbi_kern.h"
#include "ghost_mm.h"
typedef int (*access_process_vm_t)(struct task_struct *, unsigned long,
void *, int, unsigned int);
typedef void *(*get_task_mm_t)(struct task_struct *);
typedef void (*mmput_t)(struct mm_struct *);
typedef int (*apply_to_page_range_t)(struct mm_struct *mm, unsigned long addr,
unsigned long size, pte_fn_t fn, void *data);
typedef unsigned long (*get_free_pages_t)(unsigned int, unsigned int);
typedef void (*free_pages_t)(unsigned long, unsigned int);
typedef void *(*find_vma_t)(struct mm_struct *, unsigned long);
typedef void (*on_each_cpu_t)(void (*fn)(void *), void *arg, int wait);
static access_process_vm_t fn_access_process_vm;
static get_task_mm_t fn_get_task_mm;
static mmput_t fn_mmput;
static apply_to_page_range_t fn_apply_to_page_range;
static get_free_pages_t fn_get_free_pages;
static free_pages_t fn_free_pages;
static find_vma_t fn_find_vma;
static const int64_t *ptr_physvirt_offset;
static on_each_cpu_t fn_on_each_cpu;
static void *addr_do_mem_abort;
static long planc2_init(const char *args, const char *event, void *__user r)
{
fn_access_process_vm = (access_process_vm_t)kallsyms_lookup_name("access_process_vm");
fn_get_task_mm = (get_task_mm_t)kallsyms_lookup_name("get_task_mm");
fn_mmput = (mmput_t)kallsyms_lookup_name("mmput");
fn_apply_to_page_range = (apply_to_page_range_t)kallsyms_lookup_name("apply_to_page_range");
fn_get_free_pages = (get_free_pages_t)kallsyms_lookup_name("__get_free_pages");
fn_free_pages = (free_pages_t)kallsyms_lookup_name("free_pages");
fn_find_vma = (find_vma_t)kallsyms_lookup_name("find_vma");
ptr_physvirt_offset = (const int64_t *)kallsyms_lookup_name("physvirt_offset");
fn_on_each_cpu = (on_each_cpu_t)kallsyms_lookup_name("on_each_cpu");
addr_do_mem_abort = (void *)kallsyms_lookup_name("do_mem_abort");
struct ghost_mm_syms gsyms = {
.get_free_pages = fn_get_free_pages,
.free_pages = fn_free_pages,
.find_vma = fn_find_vma,
.apply_to_page_range = fn_apply_to_page_range,
.physvirt_offset_p = ptr_physvirt_offset,
.on_each_cpu = fn_on_each_cpu,
};
ghost_mm_init(&gsyms);
return 0;
}
static long planc2_ctl0(const char *args, char *__user out_msg, int outlen)
{
static char buf[4096];
int off = 0;
const char *p = args;
if (str_starts_with(p, "uxn-hook")) {
int pid = parse_num(&p);
unsigned long target = parse_num(&p);
unsigned long replace = parse_num(&p);
off = cmd_uxn_hook(pid, target, replace, buf, sizeof(buf));
}
else if (str_starts_with(p, "uxn-unhook")) {
}
else if (str_starts_with(p, "ghost-alloc-at")) {
}
else if (str_starts_with(p, "proc-read")) {
}
compat_copy_to_user(out_msg, buf, off < outlen ? off + 1 : outlen);
return 0;
}
KPM_INIT(planc2_init);
KPM_CTL0(planc2_ctl0);
KPM_EXIT(planc2_exit);
struct ghost_page {
struct task_struct *task;
struct mm_struct *mm;
unsigned long vaddr;
unsigned long kaddr;
unsigned long pfn;
uint64_t installed_pte;
int order;
unsigned long alloc_size;
int installed;
};
struct install_ctx {
uint64_t pte_val;
int written;
};
static int install_pte_cb(void *pte_ptr, unsigned long addr, void *data)
{
struct install_ctx *c = (struct install_ctx *)data;
uint64_t *p = (uint64_t *)pte_ptr;
if (*p != 0 && (*p & PTE_VALID)) return -EEXIST;
*p = c->pte_val;
c->written = 1;
return 0;
}
int ghost_alloc(struct task_struct *task, struct mm_struct *mm,
unsigned long near, unsigned long range,
uint64_t pte_template, int num_pages,
struct ghost_page *out)
{
unsigned long kva;
unsigned long vaddr;
uint64_t pa_base, new_pte;
int order = pages_to_order(num_pages);
int i, ret;
kva = g_syms.get_free_pages(GFP_KERNEL | GFP_ZERO, order);
if (!kva) return -ENOMEM;
pa_base = (uint64_t)((int64_t)kva + *g_syms.physvirt_offset_p);
vaddr = find_hole_near(mm, near, range, 1 << order);
if (!vaddr) { g_syms.free_pages(kva, order); return -ENOSPC; }
for (i = 0; i < (1 << order); i++) {
uint64_t page_pa = pa_base + (uint64_t)i * 0x1000;
struct install_ctx ictx;
new_pte = (pte_template & ~ARM64_PFN_MASK) | (page_pa & ARM64_PFN_MASK);
new_pte |= PTE_VALID | PTE_TYPE_PAGE | PTE_AF;
new_pte &= ~PTE_UXN;
new_pte &= ~(1UL << 50);
new_pte &= ~(1UL << 7);
ictx.pte_val = new_pte;
ictx.written = 0;
ret = g_syms.apply_to_page_range(mm, vaddr + i * 0x1000, 0x1000,
install_pte_cb, &ictx);
if (ret || !ictx.written) {
int j;
for (j = 0; j < i; j++) {
struct clear_ctx cctx = { .cleared = 0 };
g_syms.apply_to_page_range(mm, vaddr + j * 0x1000, 0x1000,
clear_pte_cb, &cctx);
}
g_syms.free_pages(kva, order);
return -EFAULT;
}
}
asm volatile(
"dsb ishst\n\t"
"tlbi vmalle1is\n\t"
"dsb ish\n\t"
"isb\n\t"
::: "memory"
);
out->task = task;
out->mm = mm;
out->vaddr = vaddr;
out->kaddr = kva;
out->pfn = pa_base >> 12;
out->installed_pte = new_pte;
out->order = order;
out->alloc_size = (1UL << order) * 0x1000;
out->installed = 1;
return 0;
}
pop.mode = 0;
pop.out_val = &tpte;
fn_apply_to_page_range(mm, libart_page, 0x1000, pte_op_cb, &pop);
if (gap_end - gap_start >= 0x1000) {
}
static unsigned long find_hole_near(struct mm_struct *mm, unsigned long near,
unsigned long range, int num_pages)
{
unsigned long need = (unsigned long)num_pages * 0x1000;
if (gap_end - gap_start >= need) {
}
}
struct uxn_hook_slot {
int used;
int pid;
unsigned long target_addr;
unsigned long target_page;
unsigned long replace_addr;
uint64_t saved_pte;
struct dbi_page_ctx dbi;
struct ghost_page gp;
unsigned long fault_hits;
unsigned long pass3_hits;
unsigned long last_pass3_far;
unsigned long last_pass3_new_pc;
};
#define UXN_HOOK_MAX 16
static struct uxn_hook_slot uxn_hooks[UXN_HOOK_MAX];
static void before_do_mem_abort(hook_fargs3_t *fargs, void *udata)
{
unsigned long far = fargs->arg0;
unsigned long esr = fargs->arg1;
void *regs_vp = (void *)fargs->arg2;
unsigned int ec, ifsc;
unsigned long *pc_ptr;
uint64_t new_pc;
int i, cur_pid;
ec = (esr >> 26) & 0x3F;
if (ec != 0x20) return;
ifsc = esr & 0x3F;
if (ifsc < 0x0C || ifsc > 0x0F) return;
pc_ptr = (unsigned long *)((char *)regs_vp + 0x100);
cur_pid = fn_task_pid_nr_ns ? fn_task_pid_nr_ns(current, 0, 0) : 0;
for (i = 0; i < UXN_HOOK_MAX; i++) {
struct uxn_hook_slot *s = &uxn_hooks[i];
if (!s->used) continue;
if (cur_pid && s->pid != cur_pid) continue;
if ((far & ~3UL) == (s->target_addr & ~3UL)) {
*pc_ptr = s->replace_addr;
fargs->skip_origin = 1;
s->fault_hits++;
return;
}
}
for (i = 0; i < UXN_HOOK_MAX; i++) {
struct uxn_hook_slot *s = &uxn_hooks[i];
if (!s->used) continue;
if (cur_pid && s->pid != cur_pid) continue;
if (s->target_page != (far & ~0xFFFUL)) continue;
new_pc = dbi_target_to_ghost_pc(&s->dbi, far);
if (new_pc) {
*pc_ptr = new_pc;
fargs->skip_origin = 1;
s->fault_hits++;
s->pass3_hits++;
s->last_pass3_far = far;
s->last_pass3_new_pc = new_pc;
return;
}
}
}
if (!g_hook_installed) {
hook_wrap3(addr_do_mem_abort, before_do_mem_abort, after_do_mem_abort, 0);
g_hook_installed = 1;
}
static int cmd_uxn_hook(int pid, unsigned long target, unsigned long replace,
char *buf, int bsize)
{
struct task_struct *task;
struct mm_struct *mm;
struct uxn_hook_slot *s;
unsigned long target_page = target & ~0xFFFUL;
struct pte_op pop;
uint64_t tpte;
int slot, ghost_pages, r;
reap_dead_uxn_slots();
if (find_uxn_slot_by_target(pid, target) >= 0) return fail("already hooked");
slot = find_uxn_slot();
s = &uxn_hooks[slot];
task = find_task(pid);
mm = fn_get_task_mm(task);
r = fn_access_process_vm(task, target_page, s->orig_buf,
DBI_TARGET_SIZE, FOLL_FORCE);
pop.mode = 0;
pop.out_val = &tpte;
fn_apply_to_page_range(mm, target_page, 0x1000, pte_op_cb, &pop);
s->dbi.target_page = target_page;
s->dbi.ghost_page = 0;
s->dbi.orig = s->orig_buf;
s->dbi.ghost = s->ghost_buf;
s->dbi.ghost_capacity = DBI_GHOST_MAX_INSNS;
dbi_recompile_page(&s->dbi);
ghost_pages = (s->dbi.ghost_count * 4 + 0xFFF) >> 12;
r = ghost_alloc(task, mm, target_page, 512UL << 20, tpte,
ghost_pages, &s->gp);
if (r == -ENOSPC) {
r = ghost_alloc(task, mm, target_page, 8UL << 30, tpte,
ghost_pages, &s->gp);
}
s->dbi.ghost_page = s->gp.vaddr;
dbi_recompile_page(&s->dbi);
ghost_write(&s->gp, 0, s->ghost_buf, s->dbi.ghost_count * 4);
ghost_sync_icache(&s->gp);
s->used = 1;
s->pid = pid;
s->target_addr = target;
s->target_page = target_page;
s->replace_addr = replace;
s->saved_pte = tpte;
s->fault_hits = s->pass3_hits = 0;
asm volatile("dmb ish" ::: "memory");
if (!g_hook_installed) {
hook_wrap3(addr_do_mem_abort, before_do_mem_abort, after_do_mem_abort, 0);
g_hook_installed = 1;
}
pop.mode = 1;
fn_apply_to_page_range(mm, target_page, 0x1000, pte_op_cb, &pop);
asm volatile(
"dsb ishst\n\t"
"tlbi vmalle1is\n\t"
"dsb ish\n\t"
"isb\n\t"
::: "memory"
);
fn_mmput(mm);
return ok("slot=%d ghost=0x%lx backup=0x%lx", slot, s->gp.vaddr,
dbi_target_to_ghost_pc(&s->dbi, target));
}
#define DBI_TARGET_SIZE 4096
#define DBI_TARGET_INSNS 1024
#define DBI_GHOST_MAX_INSNS 8192
struct dbi_page_ctx {
uint64_t target_page;
uint64_t ghost_page;
const uint32_t *orig;
uint32_t *ghost;
int ghost_capacity;
int ghost_count;
uint16_t offset_map[DBI_TARGET_INSNS];
struct dbi_pending_branch pending[512];
int n_pending;
int fixed, expanded, passthrough, failed, intra_page_fixed;
};
struct dbi_pending_branch {
int ghost_idx;
uint32_t enc_template;
uint16_t target_tidx;
uint8_t kind;
};
static int recomp_b(struct dbi_page_ctx *ctx, uint32_t insn, uint64_t orig_pc);
static int recomp_bl(struct dbi_page_ctx *ctx, uint32_t insn, uint64_t orig_pc);
static int recomp_bcond(struct dbi_page_ctx *ctx, uint32_t insn, uint64_t orig_pc);
static int recomp_cbz(struct dbi_page_ctx *ctx, uint32_t insn, uint64_t orig_pc);
static int recomp_tbz(struct dbi_page_ctx *ctx, uint32_t insn, uint64_t orig_pc);
static int recomp_adrp(struct dbi_page_ctx *ctx, uint32_t insn, uint64_t orig_pc);
static int recomp_adr(struct dbi_page_ctx *ctx, uint32_t insn, uint64_t orig_pc);
static int recomp_ldr_lit(struct dbi_page_ctx *ctx, uint32_t insn, uint64_t orig_pc,
int variant);
static int recomp_ldr_vec_lit(struct dbi_page_ctx *ctx, uint32_t insn, uint64_t orig_pc);
int dbi_recompile_page(struct dbi_page_ctx *ctx)
{
int i;
ctx->ghost_count = 0;
ctx->fixed = ctx->expanded = ctx->passthrough = ctx->failed = 0;
ctx->intra_page_fixed = 0;
ctx->n_pending = 0;
for (i = 0; i < DBI_TARGET_INSNS; i++) {
uint32_t insn = ctx->orig[i];
uint64_t orig_pc = ctx->target_page + (uint64_t)(i * 4);
int prev_ghost_idx = ctx->ghost_count;
int rc = 0;
ctx->offset_map[i] = (uint16_t)prev_ghost_idx;
if (insn == 0xD503201F) { ctx->passthrough++; rc = emit(ctx, insn); }
else if ((insn & 0xFC000000) == 0x14000000) rc = recomp_b(ctx, insn, orig_pc);
else if ((insn & 0xFC000000) == 0x94000000) rc = recomp_bl(ctx, insn, orig_pc);
else if ((insn & 0xFF000010) == 0x54000000) rc = recomp_bcond(ctx, insn, orig_pc);
else if ((insn & 0x7E000000) == 0x34000000) rc = recomp_cbz(ctx, insn, orig_pc);
else if ((insn & 0x7E000000) == 0x36000000) rc = recomp_tbz(ctx, insn, orig_pc);
else if ((insn & 0x9F000000) == 0x90000000) rc = recomp_adrp(ctx, insn, orig_pc);
else if ((insn & 0x9F000000) == 0x10000000) rc = recomp_adr(ctx, insn, orig_pc);
else if ((insn & 0xFF000000) == 0x18000000) rc = recomp_ldr_lit(ctx, insn, orig_pc, 0);
else if ((insn & 0xFF000000) == 0x58000000) rc = recomp_ldr_lit(ctx, insn, orig_pc, 1);
else if ((insn & 0xFF000000) == 0x98000000) rc = recomp_ldr_lit(ctx, insn, orig_pc, 2);
else if ((insn & 0xFF000000) == 0xD8000000) { ctx->expanded++; rc = emit(ctx, 0xD503201F); }
else if ((insn & 0x3F000000) == 0x1C000000) rc = recomp_ldr_vec_lit(ctx, insn, orig_pc);
else { ctx->passthrough++; rc = emit(ctx, insn); }
if (rc < 0) {
ctx->failed++;
ctx->ghost_count = prev_ghost_idx;
emit(ctx, 0xD503201F);
}
}
for (int p = 0; p < ctx->n_pending; p++) {
struct dbi_pending_branch *b = &ctx->pending[p];
uint64_t target_ghost = ctx->ghost_page + (uint64_t)ctx->offset_map[b->target_tidx] * 4;
uint64_t patch_pc = ctx->ghost_page + (uint64_t)b->ghost_idx * 4;
int64_t delta = (int64_t)(target_ghost - patch_pc);
uint32_t enc;
if (b->kind == 0 || b->kind == 1) {
int64_t imm19 = delta / 4;
enc = b->enc_template | (((uint32_t)imm19 & 0x7FFFFU) << 5);
} else if (b->kind == 2) {
int64_t imm14 = delta / 4;
enc = b->enc_template | (((uint32_t)imm14 & 0x3FFFU) << 5);
} else {
int64_t imm26 = delta / 4;
enc = b->enc_template | ((uint32_t)imm26 & 0x03FFFFFFU);
}
ctx->ghost[b->ghost_idx] = enc;
}
return 0;
}
uint64_t dbi_target_to_ghost_pc(const struct dbi_page_ctx *ctx,
uint64_t target_pc)
{
if (target_pc < ctx->target_page) return 0;
uint64_t off = target_pc - ctx->target_page;
if (off >= DBI_TARGET_SIZE) return 0;
unsigned idx = (unsigned)(off >> 2);
return ctx->ghost_page + (uint64_t)ctx->offset_map[idx] * 4;
}
static int recomp_adrp(struct dbi_page_ctx *ctx, uint32_t insn, uint64_t orig_pc)
{
uint32_t rd = insn & 0x1FU;
uint64_t immlo = (insn >> 29) & 0x3U;
uint64_t immhi = (insn >> 5) & 0x7FFFFU;
int64_t imm21 = sign_extend64((immhi << 2) | immlo, 21);
uint64_t target = (orig_pc & ~0xFFFULL) + ((uint64_t)imm21 << 12);
ctx->expanded++;
return emit_load_addr(ctx, rd, target);
}
static int emit_load_addr(struct dbi_page_ctx *ctx, uint32_t rd, uint64_t addr)
{
uint32_t movs[4];
int n = emit_mov_imm64(rd, addr, movs, 4);
if (n < 0) return -1;
for (int i = 0; i < n; i++) {
if (emit(ctx, movs[i]) < 0) return -1;
}
return 0;
}
static int emit_mov_imm64(uint32_t rd, uint64_t imm, uint32_t *out, int max_insns)
{
int n = 0, first = 1;
for (int shift = 0; shift < 64; shift += 16) {
uint16_t chunk = (uint16_t)((imm >> shift) & 0xFFFFU);
uint32_t hw = (uint32_t)(shift / 16);
if (chunk == 0) {
if (!first) continue;
if (imm != 0) continue;
}
if (n >= max_insns) return -1;
if (first) {
out[n++] = 0xD2800000U | (hw << 21) | ((uint32_t)chunk << 5) | (rd & 0x1FU);
first = 0;
} else {
out[n++] = 0xF2800000U | (hw << 21) | ((uint32_t)chunk << 5) | (rd & 0x1FU);
}
}
if (first) {
out[n++] = 0xD2800000U | (rd & 0x1FU);
}
return n;
}