首页
社区
课程
招聘
[原创] Android 内核无痕 Hook 框架设计思路和避坑指北
发表于: 2026-7-18 23:09 23956

[原创] Android 内核无痕 Hook 框架设计思路和避坑指北

2026-7-18 23:09
23956

拜读完珍惜佬写的 Android内核无痕Hook理解和感悟 文章以后, 受益匪浅 !!!

非常感谢珍惜佬的文章, 解释的非常清楚, 并且给出了很多实现细节, 非常值得学习和借鉴 !!!

经段一段时间的摸索, 终于完整实现了珍惜佬文章中的无痕 Hook 框架, 这篇文章记录一下整体的设计思路和技术方向,顺便聊点踩坑经历。

整个框架的所有无痕能力都是基于 APatch 模块提供的内核 Hook 能力实现, 感谢 APatch !!!

还要感谢 GitRoy 大佬提供的技术支持 !!!

传统 Hook 能力的弊端这里就不提及了, 珍惜佬已经讲的很清楚了, 在现在的高强度对抗中, 传统 Hook 能力已经有些不足了, 所以我们需要一种新的 Hook 能力来对抗现在的各种高强度检测手段.

所以 无痕 Hook 诞生了 !!!

既然从头造轮子, 那就一次搞定目前的需求吧, 所以框架覆盖了以下方向, 下面会附上原理和一些方案

先上一张整体架构图,感受一下模块关系:

几个关键设计决策:

Daemon 代理模式:KPM 先往 Zygote 注入一个 Daemon SO,由它实现所有 SDK 逻辑并通过 syscall 与内核通信。用户模块只需要 include 一个纯声明头文件,通过 Daemon 传入的函数指针表调用能力。这样用户模块极轻量,多模块零代码重复,符号解析和 Hook 句柄管理都集中在 Daemon 一处。

Zygote 注入:Daemon SO 先注入到 Zygote 常驻,子进程 fork 后再按包名按需加载用户模块。常规 Zygote 状态随 fork 延续;VMA-less 映射及其运行状态由 fork 路径显式恢复,确保子进程得到独立、完整的运行环境。

鉴权:KPM 启动时生成 32 字节随机 session key,所有命令必须携带。Manager 端通过签名锚定自动获取 key——KPM 编译期内置 Manager 签名证书摘要,鉴权请求到达时在内核态校验调用者 APK 签名是否匹配,命中则缓存 uid 走 fast-path,整个过程无需提权。非 Manager 链路(agent / PC 调试)走人工配对码 fallback:一次性短码,限时消费,防止被测信道检测

整个注入链路完全在内核态完成:KPM 读取 Daemon SO 的 ELF 文件、解析重定位、通过页表操作映射幽灵内存、生成 bootstrap 跳板劫持 Zygote 执行流。Daemon 驻留 Zygote 后,fork 出的子进程按包名匹配配置,按需加载用户模块。

注入编排流程大致是:发现 Zygote PID → 读取 ELF → 内核态 ELF 链接器解析 PT_LOAD 段 → 幽灵内存映射到 Zygote 用户态地址空间 → 注册 NEEDED 库到符号解析器 → 重定位(IFUNC 自修复)→ 生成 ARM64 bootstrap 跳板 → task_work_add 劫持执行流。

用户模块(.ghm 文件)由 Manager App 管理,Daemon 通过自带的 ELF 链接器在幽灵内存中加载,调用 ghostEntry(GhostApi*) 入口。模块开发者只需 include 一个纯声明头文件,不链接任何 SDK 库。

Daemon 热重载的设计思路是"清场再进场":先清除 Zygote 上所有已注册的 hook 状态、拆除旧 daemon 幽灵内存映射(含用户态残留 PTE 清理),再重新注入新版本。整个过程不重启 Zygote,开发期迭代效率高。

内核 linker 有个很坑的点, 是 ifunc 的处理, 要么daemon全自实现函数, 要么就模块入口做一下ifunc的修复, 再进行其它操作

这里要感谢 WX 大佬提出的 Shadow Page 方案。下面 Art Hook 使用的 Shadow Data Page 也是由这一思路延伸而来。

这是框架 Inline Hook指令插桩 的核心方案。它为同一 VA 维护两份内容视图:original PFN 保存原始字节,shadow PFN 保存 hook 或插桩后的指令。执行与读取通过页权限和异常路由选择各自的视图:

这种双视图设计将原始代码内容、实际执行内容和读取结果分开管理。常规代码读取路径基于 original PFN 计算校验值,执行路径则进入 shadow PFN;完整性观察的结果由观察路径及其可见范围共同决定。

页表切换以 mm_struct 为作用域。Shadow Page 管理器为同一地址空间内的线程和 CPU 统一维护 PTE 状态、TLB/cache 同步及异常上下文,使执行视图和读取视图在并发访问下保持一致。

关键前提是目标运行内核已启用并支持 FEAT_EPAN(PAN3),从而允许 EL0 使用 execute-only 映射(执行不可读的 PTE 权限组合)。设备能力由 KPM 在内核侧统一探测;用户态 feature ID 寄存器的可见字段随内核 ABI、版本和配置变化。

没有 EPAN 的设备使用单页 --- 仿真路径:原页完全阻断,KPM 在 fault handler 中软件仿真执行指令。仿真器按层级覆盖:PC-relative 指令使用原始 PC 计算,普通 ALU 和访存指令由软件执行,SIMD/FP 等低频指令通过 backup 页单步兜底。

[!WARNING]

除了 Inline REPLACE,Shadow Page 还支持三种指令插桩模式:

插桩命中靠特定的未定义指令编码触发异常,handler 内根据 PC 偏移查表判断归属,不需要读取目标 VA 字节。

这是整个框架隐身能力的根基。

常规 mmap 会在进程地址空间中创建 VMA 元数据,/proc/self/maps 按这些元数据枚举映射。仅过滤 show_map_vma 仍会留下可由 mincore 等路径观察到的映射行为。

幽灵内存的思路是绕过操作系统管理层,直接操作底层页表:

Ghost Mem 将映射信息分为硬件页表和 VMA 元数据两层:CPU 的 MMU 依据 PTE 完成访问,常规 VMA 链表枚举则不会返回这段地址。它的设计目标是收敛常规 VMA 枚举面;页表检查、映射一致性检查和内核态观察属于另一条可见性边界,需要在威胁模型中单独评估。

VMA-less PTE 还需要地址空间保护。get_unmapped_area/mmap_region 的选址依据 VMA,后续普通 mmap 可能选中已由 ghost PTE 占用的 VA。框架在 mmap_region 路径检查重叠,命中时返回 -ENOMEM(fail-closed);其余会改变地址空间布局的路径也纳入目标内核的兼容性清单。

ARM64 CPU 内置了调试寄存器,你把目标地址写进去,CPU 每次执行时硬件比较器自动比对,命中就触发异常。整个过程不改一个字节的内存。

GhostHook 的 HWBP 不经过 perf_event 子系统,而是在内核态直接操作 ARM64 调试寄存器并接管 debug 异常处理路径。这样做的好处是绕过了 perf_event 路径的检测面

HWBP注册。Linux 内核里进程和线程都是 task_struct,HWBP 绑定的是 TID 而非 TGID。只给主线程下断点,子线程走到目标地址根本不会报警。

数量限制是个硬约束:HWBP Manager 在初始化阶段读取目标 CPU 的调试能力,并结合内核已占用的槽位计算可分配断点数。执行断点数量随设备和运行环境变化,适合承载数量有限、需要低侵入监听的热点规则;大规模 Hook 由 Shadow Page 等页级机制承担。单条 HWBP 规则可用于追踪入参与返回值:

HWBP 状态和返回地址栈按 TID 隔离。递归与重入场景通过可用断点槽位或辅助机制维护嵌套层次;函数体中的普通指令保持原生执行,入口、返回及状态转换点由异常处理接管。

如果需要替换执行流(不只是监听),就得引入用户态跳板。跳板里有一个"关闸→调原函数→开闸"的流程,避免调原函数时重复触发断点死循环。LR 寄存器的保护是个极易踩坑的点——BLR 指令会覆盖 X30,得找个 callee-saved 寄存器(比如 X20)当"安全屋"暂存原始 LR。

有了 DBI 引擎以后就可以模拟执行指令实现 trace 了, 这里参考了 Frida Gum 的实现, 重构了一下, 感谢 Firda, 感谢开源 !!!

DBI 主要用于hook跳板的指令重定位和用户态指令模拟, DBI + PTE hook实现起来发现有很多坑, 我就改用shadow page 实现 hook了

把指令搬到新内存(shadow 页 / 跳板)后,所有 PC 相对寻址指令会算错地址。ARM64 指令 32 位塞不下完整 64 位地址,编译器大量使用相对寻址(B/BL/CBZ/ADRP/LDR literal)。搬到新地址后"向前跳 50 步"会跳到完全错误的地方。

重定位引擎需要逐条扫描,分类处理:

指令膨胀后需要提前计算 offset_map(原始指令索引 → 克隆页偏移),这也是缺页异常路由时"传送地图"的数据来源。

ART 虚拟机里每个 Java 方法对应一个 ArtMethod 结构体,其中 entry_point 指针指向编译后的机器码入口。传统 LSPlant 方案是替换这个指针指向自定义跳板,但反作弊会扫描所有 ArtMethodentry_point,检查指针是否指向合法的 boot.art 或 OAT 文件区域。

GhostHook 的 ART 路径采用 Shadow Data Page 管理 entry_point。原始 ArtMethod 数据页保留原始字段,shadow 副本保存 hook 后的入口;DABT 发生后,访问来源决定本次访问看到的页视图,ART 运行时代码读取 shadow 页,其余读取路径读取原始页。

同页多条 ArtMethod 共享一张 shadow 页,并由增量 patch 管理。页内写入通过差分同步合并:hook 字段保持目标入口,其他字段跟随原始页更新。Shadow Data Page 复用 mm 级页表协调、并发访问和 TLB 一致性机制。

ART hook 引擎(mako)是独立 SHARED lib,不静态链入 Daemon SO。Manager 开关启用时把路径推到 KPM 缓存,Daemon 首次调用 artHook 时 lazy load 进幽灵内存。mako 的跳板池也走幽灵内存(经适配层注入 ghostMalloc(RWX)),不引入常规 maps 区段。shorty 取值用反射 Method.getReturnType + getParameterTypes 自拼,不依赖 libart 内部符号。ArtMethod 字段布局、偏移和可用入口由 Android/ART 版本适配层管理,未匹配版本走失败降级。

定位 Java 方法有两条路径:按 className + methodName + sig 走标准反射,或直接拿反射 Method 对象——后者跳过符号解析,适合 hook 隐藏 API 等不可经标准路径定位的方法。两条路径殊途同归,最终都落到 mako 的 ArtMethod 入口替换上。

反射类加载的设计思路是预解析:在 daemon 初始化时把反射链句柄全部解析为全局缓存,fork 继承,后续调用时不再触发 JNI 查找,把开销压到最小。

这里 mako hook 是我之前写的 art hook 框架, 选择用外挂加载的方式主要是考虑到两个原因:

跨进程内存读写支持无痕模式。内核侧直接走目标进程页表遍历,不经 ptrace、不经 /proc/pid/mem、不设 TracerPid

这条路径也用于 Shadow Page 的跨进程读取:读事务在页表锁、mm 级同步和多 CPU 可见性协议保护下,临时将 PTE 指向 original PFN,完成 GUP 取页后恢复执行视图。TLB 刷新与保留策略由后端按目标内核、CPU 拓扑和访问时序实现。

思路是让内核直接遍历目标进程的 VMA 链表,把结果写到调用方 buffer。整个过程不经 /proc/maps、不设 TracerPid、不创建文件。agent 用它构建 per-pid VMA 缓存,配合 ELF 符号表缓存做地址→符号的反查。

这是 Trace 的记录模式:在 debug 异常处理的 Software Step 分支中加入 trace 步进处理,对目标线程设置单步标志进入连续单步。每条用户态指令触发一次 SS 异常,handler 在异常上下文里采集 PC + 全量 GPR + SP + PSTATE + 时间戳,写入专用 trace ring buffer(覆盖最旧 + 丢弃计数),消费者分批取走。

关键约束:MDSCR_EL1.SS 是 per-CPU 寄存器,目标线程被调度到其他 CPU 时 SS 会丢失。trace 期间必须把目标线程钉在单个 CPU 上(set_cpus_allowed_ptr),trace 结束后恢复原亲和性。

trace 的连续单步与框架其他单步消费者(HWBP 恢复单步、shadow BRK 单步、shadow read 单步、simulate backup 单步)共享同一个 MDSCR.SS 位。trace 在 SS 分发链中具有最高优先级,但不破坏其他机制的 one-shot 单步恢复。

Daemon 内置符号解析器,读 ELF .dynsym 解析符号地址。不走 dl_iterate_phdr——幽灵内存中的 SO 不在动态链接器维护的 link_map 中,常规枚举不会包含它;因此需要自行维护加载模块与符号表信息。

符号解析用于两条路径:Hook 定位(按 SO 名 + 符号名找地址)和 Trace 符号化(批量解析 PC 对应的 {soName, sym, offset} 映射表,随 trace 帧推送到 PC)。

VMA 缓存经内核无痕读取,ELF 符号表缓存从磁盘文件解析一次后复用(优先全量符号表),排序后二分查找。

反作弊检测 HWBP 的手段已经非常成熟,主要通过 ptraceperf_event_open 施展"连环五步杀":读寄存器查空位、满载占坑测试、读写一致性校验、越界诱导陷阱(故意设 7 个断点看是否返回 -ENOSPC)、主动触发测试。

应对思路是在内核层实现硬件调试寄存器的"假账本":拦截 ptrace 对调试寄存器的读写请求,反作弊全程跟假账本交互,真实 CPU 物理寄存器不受干扰。假账本需要精确模拟内核行为——包括越界报错、读写一致性等,细节不到位反而暴露。

Zygote 注入的 Daemon、跳板和 Shadow Page 使用 VMA-less PTE。Fork 处理器将它们视为显式继承对象:在目标内核验证过的 fork 窗口内,为子进程恢复所需映射、运行状态和元数据。

四类对象的继承策略各不相同:

线程 clone(共享 mm)不需要克隆——只有新进程才需要安装 ghost 内存。

有时模块需要在目标进程注入自定义 Java 类——比如运行时生成的代理类或 Hook 回调类。设计上走内存 DEX 加载路线:dex 字节不落盘,直接在幽灵内存中包装为 ByteBuffer,反射构造 ClassLoader。

关键设计点:dex 字节先拷贝到独立的幽灵内存分配,使数据生命周期与用户 SO 的 ELF 映射解耦——直接包装指针会让 buffer 生命周期耦合 SO 映射,拷贝后 dex loader 自主管理,卸载时确定释放。

parent ClassLoader 的选择影响可见范围:挂载 App ClassLoader 可经委托解析 App/framework 类;隔离加载则只有 bootstrap 类 + 自身 dex 类可见。这条路径完全在 daemon 侧实现,与 ART hook 引擎是否启用无关。

这两类拦截器都基于 Shadow Hook 实现,不引入 GOT trampoline。Binder 路径直接拦截 transactNative 的 native 入口;Linker 路径通过 Bionic 版本适配层解析可用 dlopen 入口。两者的映射可见性以常规 VMA 枚举为边界,页表及内核态观察路径纳入独立的安全评估。

Binder Transact 拦截:shadow hook transactNative 的 native 函数地址,在 transact 前后按 interface token 分发 before/after 回调。ART 适配层负责按版本定位 ArtMethod 与 native 入口,不将任何字段偏移视为稳定 ABI。读取 interface token 依赖 libbinder.so / libutils.so 的 native 函数,因此由一层 Parcel 工具桥接。

Binder 拦截当前覆盖 client 侧。服务端采用 C++ 虚函数分派,扩展服务端覆盖时需要解析具体实现目标或设置独立拦截点。binder call log 作为可选开关,默认关闭,避免热路径开销。

Linker dlopen 拦截:shadow hook linker 的可用 dlopen 入口,在 SO 加载前后按 lib name 子串匹配分发回调。内部符号名和调用链由 Bionic 版本适配层维护,未命中入口时该能力保持关闭。

两项能力均懒加载:首次调用才触发 init,per-process 独立,fork 后由映射继承处理器恢复。

用户模块的 ghostEntry 在进程启动早期执行,目标 SO 则可能在后续才加载。SO 生命周期分为两段:ELF 的 .init/.init_array 由 linker 构造函数路径执行,JNI_OnLoad 由 ART 在 dlopen 返回后查找并调用。两段都位于 System.loadLibrary 的返回之前,框架为它们分别设置拦截点。

Linker 构造函数路径和 ART 的 JNI_OnLoad 路径各自建立拦截点。linker/ART 内部符号及调用链由版本特征识别;幽灵内存中的 SO 不在 dl_iterate_phdr 的常规枚举范围内,符号解析器据此维护独立的模块记录,并在需要时回退到完整 ELF 符号表。

JNI 动态注册的 trace 同理:JNIEnv 函数表中的 native 方法注册入口按固定偏移就能取到,无需解析 ART 内部符号,在注册时拦截匹配的方法即可 trace 其函数指针。

目标 SO 使用花指令混淆控制流:在真实指令间插入 junk bytes,通过 br x_reg(间接分支寄存器)跳转到 SO 内部的真实代码地址。静态反汇编器看到 br 时无法解析目标地址,从 junk bytes 起点反汇编会产生错误指令序列。

GTrace 采用动态仿真执行的方案,天然跟随 br 跳转——trace 日志中的指令序列已经是去花后的真实执行路径。trace 日志通过标注内部分支目标和 PC 间断区间,使自动化分析工具可直接提取去花后的真实执行路径,无需从 PC 非连续推断。

同一 SO 中不同函数的 trace 场景:先 traceStart(funcA)traceStart(funcB),之后 A 和 B 可能在不同时刻被调用。如果 per-trace 配置(writer、traceMode、memReadMode 等)存在全局变量中,第二次调用会覆盖前者的配置,导致 trace 数据写错文件。

解法是将 code handler 的 per-trace 配置从全局变量重构为 per-handle 上下文,通过仿真引擎的 userData 传递给所有 hook 回调,实现多 handle 独立运行。由于仿真执行是同步阻塞的,两个 trace 不可能真正并发,但 shadow hook 是异步触发的——per-handle 化后各自独立,互不干扰。

这部分聊几个让人头疼的细节,给各位大佬省点时间。

多页 vmap 的页数组构造


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

最后于 2026-8-2 12:32 被SharkFall编辑 ,原因:
收藏
免费 264
打赏
分享
最新回复 (222)
雪    币: 6675
活跃值: (16869)
能力值: ( LV9,RANK:270 )
在线值:
发帖
回帖
粉丝
2
火钳刘明
2026-7-19 00:10
0
雪    币: 596
活跃值: (839)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
3
珍惜Any 火钳刘明
欢迎珍惜佬!
2026-7-19 00:18
0
雪    币: 140
活跃值: (961)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
火钳刘明
2026-7-19 00:26
2
雪    币: 39
活跃值: (428)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
5
牛牛牛牛,学习了!!
2026-7-19 00:39
0
雪    币: 366
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
6
cool
2026-7-19 00:44
0
雪    币: 9110
活跃值: (5888)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
7
666
2026-7-19 00:45
0
雪    币: 265
活跃值: (60)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
8
mark
2026-7-19 01:51
0
雪    币: 4024
活跃值: (5299)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
9
过来学习一下
2026-7-19 01:53
0
雪    币: 42
活跃值: (591)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
10
大佬想问一下,你的实现支持art热页hook吗
2026-7-19 02:29
0
雪    币: 5
活跃值: (1830)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
11
666
2026-7-19 05:38
0
雪    币: 6
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
12
感谢分享
2026-7-19 06:46
0
雪    币: 4169
活跃值: (7839)
能力值: ( LV5,RANK:60 )
在线值:
发帖
回帖
粉丝
13
真狠啊
2026-7-19 07:11
0
雪    币: 4981
活跃值: (6235)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
14
6
2026-7-19 07:38
0
雪    币: 246
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
15

111

最后于 2026-7-19 08:36 被mb_cfjwplfo编辑 ,原因:
2026-7-19 08:36
0
雪    币: 246
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
16
PPKun 大佬想问一下,你的实现支持art热页hook吗
应该不可以,大佬可以加个好友交流一下,我也是卡在了libart上,感觉不能dbi只能通过pte页表rw/x切换实现
2026-7-19 08:37
0
雪    币: 596
活跃值: (839)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
17
PPKun 大佬想问一下,你的实现支持art热页hook吗
用shadow page实现的hook,目前测试起来没什么问题
2026-7-19 09:18
0
雪    币: 596
活跃值: (839)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
18
mb_cfjwplfo 应该不可以,大佬可以加个好友交流一下,我也是卡在了libart上,感觉不能dbi只能通过pte页表rw/x切换实现
dbi hook实现起来坑太多了,我放弃了,改用shadow hook实现的
2026-7-19 09:19
0
雪    币: 246
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
19
SharkFall dbi hook实现起来坑太多了[em_055],我放弃了,改用shadow hook实现的
理想化情况下dbi很牛比,但现实很骨感
2026-7-19 10:40
0
雪    币: 54
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
20
666666666666666
2026-7-19 10:55
0
雪    币: 596
活跃值: (839)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
21
mb_cfjwplfo 理想化情况下dbi很牛比,但现实很骨感
2026-7-19 10:58
0
雪    币: 209
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
22
666
2026-7-19 11:11
0
雪    币: 378
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
23
666
2026-7-19 13:36
0
雪    币: 224
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
24
2026-7-19 13:46
0
雪    币: 411
活跃值: (611)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
25
66
2026-7-19 16:17
0
游客
登录 | 注册 方可回帖
返回