首页
社区
课程
招聘
[原创]一个零字节修改的 ARM64 Android Hook 框架实现
发表于: 22小时前 1003

[原创]一个零字节修改的 ARM64 Android Hook 框架实现

22小时前
1003

代码仓库:<450K9s2c8@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_bridgeart_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 artInvokeInterfaceTrampolineart_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_structvm_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 节那篇启蒙文章。没有"查名片、布陷阱"这个框架性思路,我不会往这个方向做。

代码仓库:<1e4K9s2c8@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
// ptehook_planc_v2.c

#include &lt;compat.h&gt;
#include &lt;kallsyms.h&gt;
#include &lt;hook.h&gt;
#include "dbi_kern.h"
#include "ghost_mm.h"

/* 通过 kallsyms 拿到的内核符号 */
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");

    /* 把这些指针塞进 ghost_mm 模块 */
    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")) {
        /* parse: uxn-hook &lt;pid&gt; <target_va> <replace_va> */
        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")) {
        /* ... */
    }
    /* 省略其他 10 多条命令 */

    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);
// ghost_mm.c

struct ghost_page {
    struct task_struct *task;
    struct mm_struct   *mm;
    unsigned long       vaddr;         // 目标进程用户态 VA
    unsigned long       kaddr;         // 内核 linear-map VA(我们写 shellcode 用)
    unsigned long       pfn;            // 物理页帧号
    uint64_t            installed_pte;  // 已装的 PTE 值
    int                 order;          // 2^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;

    // 1. 分配 2^order 个物理页
    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);

    // 2. 在目标 mm 找空洞 VA
    vaddr = find_hole_near(mm, near, range, 1 << order);
    if (!vaddr) { g_syms.free_pages(kva, order); return -ENOSPC; }

    // 3. 逐页安装 PTE
    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);           // 清 GP (BTI 强制位)
        new_pte &= ~(1UL << 7);            // 清 AP[2] (允许写入 shellcode)

        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) {
            // 回滚已装的 PTE
            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;
        }
    }

    // 4. TLB 刷新
    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;
}
/* 调用前:先读附近一个正常 libart r-xp 页的 PTE 作为模板 */
pop.mode = 0;                // read mode
pop.out_val = &tpte;
fn_apply_to_page_range(mm, libart_page, 0x1000, pte_op_cb, &pop);
/* 然后调 ghost_alloc(..., pte_template=tpte, ...) */
/* 原版 */
if (gap_end - gap_start >= 0x1000) {   // 只要求 1 页空洞
    /* pick this gap */
}
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;
    /* ... iterate VMAs ... */
    if (gap_end - gap_start >= need) {   // 要求连续 num_pages 页
        /* pick */
    }
}
// ptehook_planc_v2.c

struct uxn_hook_slot {
    int used;
    int pid;
    unsigned long target_addr;        // 精确 hook 点
    unsigned long target_page;         // target_addr & ~0xFFF
    unsigned long replace_addr;        // 用户 shellcode VA
    uint64_t saved_pte;                // 原 PTE (unhook 恢复用)
    struct dbi_page_ctx dbi;           // DBI 状态
    struct ghost_page gp;              // DBI 重编译 ghost
    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;

    /* 1. ESR 过滤:只处理 EL0 instruction abort + permission fault */
    ec = (esr >> 26) & 0x3F;
    if (ec != 0x20) return;              /* 不是 EL0 instruction abort */
    ifsc = esr & 0x3F;
    if (ifsc < 0x0C || ifsc > 0x0F) return;   /* 不是 permission fault */

    /* 2. pt_regs 里的 PC 在 +0x100 (A13 kernel 5.4 offset) */
    pc_ptr = (unsigned long *)((char *)regs_vp + 0x100);

    /* 3. 过滤本次 fault 的进程 */
    cur_pid = fn_task_pid_nr_ns ? fn_task_pid_nr_ns(current, 0, 0) : 0;

    /* Pass 2: 精确 target 匹配 → 我们的 shellcode */
    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;     /* ← 改 PC */
            fargs->skip_origin = 1;        /* 告诉 KernelPatch: 别走原 handler */
            s->fault_hits++;
            return;
        }
    }

    /* Pass 3: 同页其他 FAR → DBI 重编译版本 */
    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;
        }
    }

    /* 不匹配:放行给原 do_mem_abort(会 SIGSEGV user process) */
}

/* 首次 uxn_hook 安装时挂 hook_wrap3 */
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;

    /* 0. 老 slot 清理 + slot 查找 */
    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);

    /* 1. 读目标页 4KB 原字节 → s->orig_buf */
    r = fn_access_process_vm(task, target_page, s->orig_buf,
                              DBI_TARGET_SIZE, FOLL_FORCE);

    /* 2. 读 target PTE (unhook 恢复用) */
    pop.mode = 0;
    pop.out_val = &tpte;
    fn_apply_to_page_range(mm, target_page, 0x1000, pte_op_cb, &pop);

    /* 3. pre-DBI 算 ghost 需要多少页 */
    s->dbi.target_page = target_page;
    s->dbi.ghost_page  = 0;              /* 第一趟用 dummy 算 size */
    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;

    /* 4. 分配 DBI ghost(先试 ±512MB 近 libart,失败则 ±8GB) */
    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);
    }

    /* 5. 二次 DBI,用真 ghost_page */
    s->dbi.ghost_page = s->gp.vaddr;
    dbi_recompile_page(&s->dbi);

    /* 6. 把重编译字节写入 ghost */
    ghost_write(&s->gp, 0, s->ghost_buf, s->dbi.ghost_count * 4);
    ghost_sync_icache(&s->gp);

    /* 7. 保存 slot 状态 */
    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");

    /* 8. 首次装 hook 时挂 do_mem_abort */
    if (!g_hook_installed) {
        hook_wrap3(addr_do_mem_abort, before_do_mem_abort, after_do_mem_abort, 0);
        g_hook_installed = 1;
    }

    /* 9. 在 target PTE 设 UXN=1 + TLB flush */
    pop.mode = 1;   /* set UXN */
    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));
}
// dbi_kern.h

#define DBI_TARGET_SIZE      4096
#define DBI_TARGET_INSNS     1024
#define DBI_GHOST_MAX_INSNS  8192       // 最坏展开 8x

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;

    /* 核心:target word idx → ghost word idx */
    uint16_t offset_map[DBI_TARGET_INSNS];

    /* 前向 intra-page 条件跳转的回填队列 */
    struct dbi_pending_branch pending[512];
    int n_pending;

    int fixed, expanded, passthrough, failed, intra_page_fixed;
};

struct dbi_pending_branch {
    int       ghost_idx;         // ghost 中的字节位置(需回填)
    uint32_t  enc_template;      // 不带 imm 的模板
    uint16_t  target_tidx;       // target page 内的 word idx
    uint8_t   kind;              // 0=B.cond 1=CBZ 2=TBZ 3=B
};
// dbi_kern.c

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;

        /* NOP */
        if (insn == 0xD503201F) { ctx->passthrough++; rc = emit(ctx, insn); }
        /* B imm26 */
        else if ((insn & 0xFC000000) == 0x14000000) rc = recomp_b(ctx, insn, orig_pc);
        /* BL imm26 */
        else if ((insn & 0xFC000000) == 0x94000000) rc = recomp_bl(ctx, insn, orig_pc);
        /* B.cond */
        else if ((insn & 0xFF000010) == 0x54000000) rc = recomp_bcond(ctx, insn, orig_pc);
        /* CBZ/CBNZ */
        else if ((insn & 0x7E000000) == 0x34000000) rc = recomp_cbz(ctx, insn, orig_pc);
        /* TBZ/TBNZ */
        else if ((insn & 0x7E000000) == 0x36000000) rc = recomp_tbz(ctx, insn, orig_pc);
        /* ADRP */
        else if ((insn & 0x9F000000) == 0x90000000) rc = recomp_adrp(ctx, insn, orig_pc);
        /* ADR */
        else if ((insn & 0x9F000000) == 0x10000000) rc = recomp_adr(ctx, insn, orig_pc);
        /* LDR (literal) 32-bit W reg */
        else if ((insn & 0xFF000000) == 0x18000000) rc = recomp_ldr_lit(ctx, insn, orig_pc, 0);
        /* LDR (literal) 64-bit X reg */
        else if ((insn & 0xFF000000) == 0x58000000) rc = recomp_ldr_lit(ctx, insn, orig_pc, 1);
        /* LDRSW (literal) 32-bit sign-extended to X */
        else if ((insn & 0xFF000000) == 0x98000000) rc = recomp_ldr_lit(ctx, insn, orig_pc, 2);
        /* PRFM (literal) → drop to NOP */
        else if ((insn & 0xFF000000) == 0xD8000000) { ctx->expanded++; rc = emit(ctx, 0xD503201F); }
        /* SIMD LDR (literal) S/D/Q reg */
        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);       /* NOP 填位 */
        }
    }

    /* 回填前向 intra-page 条件跳转 */
    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) {        /* B.cond, CBZ: imm19 */
            int64_t imm19 = delta / 4;
            enc = b->enc_template | (((uint32_t)imm19 & 0x7FFFFU) << 5);
        } else if (b->kind == 2) {                  /* TBZ: imm14 */
            int64_t imm14 = delta / 4;
            enc = b->enc_template | (((uint32_t)imm14 & 0x3FFFU) << 5);
        } else {                                     /* B: imm26 */
            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);      /* MOVZ/MOVK 4 条展成绝对加载 */
}

/* emit_load_addr:4 条 MOV 加载 64-bit 立即数到 rd */
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;              /* 首条但 imm 非零 → 继续找首个非零 */
        }
        if (n >= max_insns) return -1;

        if (first) {
            /* MOVZ Xd, #chunk, LSL #(hw*16) : opc=00, bit[30:29] */
            out[n++] = 0xD2800000U | (hw << 21) | ((uint32_t)chunk << 5) | (rd & 0x1FU);
            first = 0;
        } else {
            /* MOVK Xd, #chunk, LSL #(hw*16) : opc=11 */
            out[n++] = 0xF2800000U | (hw << 21) | ((uint32_t)chunk << 5) | (rd & 0x1FU);
        }
    }
    if (first) {
        /* imm == 0 的边界 */
        out[n++] = 0xD2800000U | (rd & 0x1FU);
    }
    return n;
}

[培训]《冰与火的战歌:Windows内核攻防实战》!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。

收藏
免费 53
支持
分享
最新回复 (21)
雪    币: 211
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
2
66
22小时前
0
雪    币: 200
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
3
tql
21小时前
0
雪    币: 104
活跃值: (8262)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
tql
21小时前
0
雪    币: 16
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
5
1
20小时前
0
雪    币: 7
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
6
向大佬学习
19小时前
0
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
7
向大佬学习
18小时前
0
雪    币: 8679
活跃值: (5368)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
8
学习学习
18小时前
0
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
9
666
17小时前
0
雪    币: 2670
活跃值: (5190)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
10
666
17小时前
0
雪    币: 95
活跃值: (2787)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
11
6666666666
16小时前
0
雪    币: 4
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
12
666666666666
16小时前
0
雪    币: 0
活跃值: (1398)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
13
6666666666
15小时前
0
雪    币: 0
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
14
6
14小时前
0
雪    币: 2432
活跃值: (4126)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
15
666666
14小时前
0
雪    币: 47
活跃值: (1911)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
16
66666
14小时前
0
雪    币: 155
活跃值: (4486)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
17
向大佬学习 
13小时前
0
雪    币: 59
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
18
666
13小时前
0
雪    币: 208
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
19
6666
12小时前
0
雪    币: 138
活跃值: (882)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
20
666666
11小时前
0
雪    币: 15
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
21
太强了
11小时前
0
雪    币: 4477
活跃值: (4276)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
22
tql
10小时前
0
游客
登录 | 注册 方可回帖
返回