代码仓库:<2e8K9s2c8@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 开头)。第二步:向上追 X24X24 从 [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 距离 218M
...(已截断)
---
来源: 看雪论坛
原文链接: https://bbs.kanxue.com/thread-290871.htm
[原创]一个零字节修改的 ARM64 Android Hook 框架实现
144 浏览
9 回复
tql
tql
向大佬学习
向大佬学习
学习学习
向大佬学习
太强了
tql
感谢分享