溺的文档
ReZygisk · 第 3 篇 / 共 4 篇

ReZygisk 注入体 injector 与反检测

2026-06-30 · 阅读 5

本文是 ReZygisk 源码解读系列第 3 篇。全系列:

  • 00 总览与整体架构
  • 01 守护进程 zygiskd 与 Root 适配
  • 02 注入机制:monitor 监控与 ptracer 注入
  • 03 注入体 injector 与反检测基石(本篇)
  • 04 外围:IPC 协议、模块脚本、WebUI 与构建

代码引用格式 文件:行号。本篇代码主要位于 loader/src/injector/,并涉及 loader/src/common/elf_util.cifunc_shim.cmisc.c


一、本篇定位

02 篇结束时,libzygisk.so 已经被远程加载进 zygote,并且 ptracer 调用了它的 entry()。从这一刻起,主动权交给了注入体自己。本篇回答:

  • entry() 做了什么,之后注入体如何"潜伏"在 zygote 里;
  • 它怎么 hook 住 zygote 的关键函数,从而在每个 APP fork+specialize 前后插入模块逻辑;
  • Zygisk 模块 API 长什么样(preAppSpecialize/postAppSpecialize/companion/option…),模块如何被加载、调用、卸载;
  • 完成使命后,注入体如何干净地自我卸载,以及为什么"卸载失败时反而要保留自己";
  • 支撑这一切的反检测基石:ELF 解析、IFUNC 垫片、ART 结构探测、std::string 解析、ptrace 痕迹清理、反 atime 检测。

对应文件:

文件 职责
injector/entry.c 注入体唯一入口 entry()
injector/hook.c / hook.h hook 主体:PLT hook、JNI 方法替换、fork/specialize 编排、模块 API、自卸载
injector/jni_hooks.h 生成代码:被拦截的 JNI 方法变体 + 方法表 + do_hook_zygote()
injector/gen_jni_hooks.py jni_hooks.h 的生成器
injector/module.h Zygisk 模块 API/ABI 定义
injector/ptrace_clear.c/h ptrace 痕迹清理(seccomp event)
injector/cpp_strings.c/h libc++ std::string 读取
injector/art_method.h ART ArtMethod 结构探测
common/elf_util.c ELF 解析与符号定位(hook 基础)
common/ifunc_shim.c ARM32 libc 函数垫片
common/misc.c parse_maps_safe 等(反 atime 检测)

二、entry():注入后的第一步

entry()entry.c:9-32)是注入体唯一的导出符号(visibility("default")),由 ptracer 远程调用:

void entry(void *addr, size_t size, int tango_flag) {
  start_addr = addr;      // 保存 libzygisk 自身映射地址
  block_size = size;      // 供日后自我 munmap

  hook_functions();       // ① 安装 PLT hook

  struct kernel_version version = parse_kversion();
  if (version.major > 3 || (version.major == 3 && version.minor >= 8))
    perform_ptrace_message_clear();   // ② 清理 ptrace 痕迹

  if (!rezygiskd_zygote_injected())   // ③ 向 zygiskd 上报"已注入"
    LOGE("ReZygiskd is not running");
}

三步:

  1. hook_functions() 安装 PLT hook(第三节)。
  2. perform_ptrace_message_clear() 抹掉 ptrace 注入残留(第八节),仅内核 ≥ 3.8 执行。
  3. rezygiskd_zygote_injected() 发送 ZygoteInjected——这就是 02 篇里 monitor 收到的 ZYGOTE*_INJECTED 的源头(zygiskd 收到后转发给 monitor)。

重要澄清entry() 读取模块列表、在此加载模块 .so。它只做"装钩子 + 清痕迹 + 报注入"。真正的模块加载发生在后面被 hook 触发的时机(第五节)。这是通读时极易误解的一点。

addr/size 立刻存入全局(hook.h:7-9start_addr/block_size),是为将来自我卸载时 munmap 自己用。tango_flag 仅用于日志区分 [TANGO] 模式。


三、hook 机制:PLT hook + ART 方法表替换

ReZygisk 同时使用两种 hook,分工明确——这点必须理清,它不是单一的 inline hook。

flowchart TB subgraph PLT["① PLT hook (外部库 PLTI)"] P1["fork"] P2["strdup"] P3["property_get"] P4["FileDescriptorInfo::ReopenOrDetach"] end subgraph JNI["② ART 方法表替换 (RegisterNatives)"] J1["nativeForkAndSpecialize"] J2["nativeSpecializeAppProcess"] J3["nativeForkSystemServer"] end PLT -->|"改写调用方 GOT/PLT 条目"| T1["libandroid_runtime.so 的 C 符号"] JNI -->|"RegisterNatives + ArtMethod 取原入口"| T2["com.android.internal.os.Zygote 的 native 方法"]
  • PLT hook(通过外部库 PLTI,PLT-Inject):改写调用方的 GOT/PLT 条目,把对某符号的调用重定向到自定义函数。用于 libc/runtime 的 C 函数。
  • ART 方法表替换:不改函数本体,而是用 ART 自己的 RegisterNatives 把 Java 类的 native 方法表项指向包装函数,并通过读 ArtMethod 拿到"原函数地址"。用于 Zygote 类的 native 方法。

3.1 hook_functions:装上 PLT 钩子

hook_functions()hook.c:1321-1330):

void hook_functions(void) {
  plti_init(&plti_ctx);
  plti_add_lib(&plti_ctx, "libandroid_runtime.so");

  PLT_HOOK_REGISTER("libandroid_runtime.so", fork, false);
  PLT_HOOK_REGISTER("libandroid_runtime.so", strdup, false);
  PLT_HOOK_REGISTER("libandroid_runtime.so", property_get, false);
  PLT_HOOK_REGISTER_SYM("libandroid_runtime.so",
      "_ZNK18FileDescriptorInfo14ReopenOrDetach",
      _ZNK18FileDescriptorInfo14ReopenOrDetach, true);
}

四个钩子各有用途:

钩子 位置 作用
new_fork hook.c:237-239 fork 去重:ReZygisk 自己先 fork 并缓存 pid,ART 内部再 fork 时直接返回缓存值,避免重复 fork
new_strdup hook.c:345-353 时机探针:当参数为 "com.android.internal.os.ZygoteInit" 时触发 initialize_jni_hook()
new_property_get hook.c:369-373 时机探针:首次调用时触发 hook_unloader(),随后自卸载该钩子
new__ZNK...ReopenOrDetach hook.c:268-290 修复 fd 重开:root 模块卸载 overlay 后某些 fd 无法重开会让 zygote abort,这里对 socket、boot-image-methods.artaccess() 失败的文件直接 close 跳过

3.2 JNI 方法表替换

hook_jni_methods()hook.c:381-445)是替换 Zygote native 方法的核心,也是模块 API hook_jni_native_methods 的实现:

void hook_jni_methods(JNIEnv *env, const char *clz, JNINativeMethod *methods, int numMethods) {
  jclass clazz = (*env)->FindClass(env, clz);
  for (int i = 0; i < numMethods; i++) {
    JNINativeMethod *nm = &methods[i];
    jmethodID mid = ... GetMethodID / GetStaticMethodID ...;
    jobject method = (*env)->ToReflectedMethod(env, clazz, mid, is_static);
    jint modifier = (*env)->CallIntMethod(env, method, member_getModifiers);
    if ((modifier & MODIFIER_NATIVE) == 0) { nm->fnPtr = NULL; continue; } // 只 hook native

    void *art_method = amethod_from_reflected_method(env, method);
    if (hooks_count < 32) hooks[hooks_count++] = *nm;   // 收集"新方法"(fnPtr=包装函数)
    void *orig = amethod_get_data((uintptr_t)art_method); // 从 ArtMethod 取原入口
    nm->fnPtr = orig;                                     // 回填原函数地址给调用方
  }
  (*env)->RegisterNatives(env, clazz, hooks, (jint)hooks_count); // 写入新方法表
}

机制:

  • 用反射 Member.getModifiers() 校验 Modifier.NATIVE只 hook 真正的 native 方法
  • amethod_get_data(art_method)art_method.h:103-105)读出 ArtMethod 的原生入口(即原函数地址),回填进 nm->fnPtr——这样调用方(jni_hooks.h 里的包装函数)就能拿到"原函数"去调用。
  • 真正的替换由 RegisterNatives 完成:hooks[] 里存的是新包装函数。

initialize_jni_hook()hook.c:450-518)在 3.1 的 strdup 探针触发时运行:取得 JNIEnv、解析 Member.getModifiersModifier.NATIVE 常量、amethod_init(env) 探测 ArtMethod 布局(第九节)、置 can_hook_jni = true,最后调 do_hook_zygote(env)(在 jni_hooks.h 中)。


四、hook 的时机:两个探针串起整条链

注入体面临的难题是时序:JNI hook 需要 ART/JNIEnv 就绪,模块加载需要 libart 已载入且在 system_server fork 之前。ReZygisk 用两个 PLT 探针精确卡时机:

sequenceDiagram autonumber participant Z as zygote (ZygoteInit 启动中) participant H as libzygisk hooks participant D as zygiskd Note over Z: entry() 已装好 PLT 钩子 Z->>H: strdup("com.android.internal.os.ZygoteInit") H->>H: initialize_jni_hook() → do_hook_zygote() Note over H: 替换 nativeForkAndSpecialize 等方法表 Z->>H: property_get(...) 首次调用 H->>H: hook_unloader() H->>H: 加 libart.so, hook pthread_attr_setstacksize H->>D: ReadModules → 取各模块 .so 路径 H->>H: load_modules_only() 用 CSOLoader 加载模块 Note over H: 此时在 system_server fork 之前,<br/>模块得以扩散到所有后代进程 Z->>H: (后续) nativeForkAndSpecialize 被调用 Note over H: 进入模块生命周期回调 (第六节)

hook_unloader()hook.c:1332-1349):

static void hook_unloader(void) {
  plti_add_lib(&plti_ctx, "libart.so");
  PLT_HOOK_REGISTER("libart.so", pthread_attr_setstacksize, false);  // 自卸载用(第七节)
  hook_unregister("libandroid_runtime.so", "property_get", false, &old_property_get);
  load_modules_only();   // ← 模块在这里加载
}

load_modules_only()hook.c:934-991):发送 ReadModules 拿到各模块 .so 路径,用 csoloader_load 逐个加载(仍是绕过系统 linker 的自定义加载器),取符号 zygisk_module_entry 作入口;某模块加载失败则发 RemoveModule 通知 zygiskd 同步移除,保持两端列表一致。


五、被拦截的 JNI 方法与生成器

5.1 拦截清单

所有被 hook 的方法都在类 com/android/internal/os/Zygotejni_hooks.h:426):

方法 变体数 说明
nativeForkAndSpecialize 12(jni_hooks.h:192-254 fork+特化 APP;变体覆盖 Android L→U、三星、GrapheneOS
nativeSpecializeAppProcess 6(jni_hooks.h:348-380 仅特化(已 fork 的进程)
nativeForkSystemServer 2(jni_hooks.h:408-420 fork system_server

变体之多,是为应对碎片化的 ROM:不同 Android 版本、三星(在 se_info 后插入两个匿名 jint)、GrapheneOS(末尾多一个 jlongArray)的 JNI 签名各不相同。do_hook_zygote()jni_hooks.h:422-458)在运行时对每类方法遍历整张表逐一尝试,选用第一个匹配成功(fnPtr 被回填)的变体。

5.2 gen_jni_hooks.py 生成了什么

gen_jni_hooks.py 是代码生成器,为每个变体生成(gen_jni_hooks.py:236-270):

  1. static void *<base>_orig——保存原函数;
  2. typedef <ret> (*<base>_fn)(JNIEnv*, jclass, ...)——变参函数指针类型,让所有变体共用一个调用类型;
  3. 每变体一个 no_stack_protector 的包装函数;
  4. <base>_methods[] 方法表 + 计数。

包装函数体的模板(gen_jni_hooks.py:97-111):

// 生成形如 nativeForkAndSpecialize_l 的函数体:
struct app_specialize_args_v5 args = { .uid = &uid, .gid = &gid, ... };  // 参数全部取地址
args.<新增字段> = &<参数>;        // 仅该版本新增的字段才赋值
struct zygisk_context ctx;
rz_init(&ctx, env, &args);
rz_nativeForkAndSpecialize_pre(&ctx);   // 前置回调
<orig>(env, clazz, <原参数列表>);        // 调原函数
rz_nativeForkAndSpecialize_post(&ctx);  // 后置回调
rz_cleanup(&ctx);

两个关键设计:

  • 参数全部以指针收入 app_specialize_args_v5(如 .uid = &uid),使模块的 pre 回调能修改 uid/gid/niceName 等特化参数。
  • no_stack_protector:这些函数被 ART 当 native 方法直接调用,栈布局必须严格可控,禁用栈 canary 避免干扰。

六、模块生命周期

6.1 fork/specialize 编排

nativeForkAndSpecialize 为例,pre/post 回调编排在 hook.c

flowchart TB PRE["rz_nativeForkAndSpecialize_pre<br/>(hook.c:1188)"] --> FORK["rz_fork_pre<br/>(自行 fork, 记录 fd)"] FORK --> CHILD{"子进程?"} CHILD -->|是| APRE["rz_app_specialize_pre<br/>(hook.c:1033)"] APRE --> FLAGS["isolated uid 修正<br/>+ GetProcessFlags 查询"] FLAGS --> MPRE["rz_run_modules_pre<br/>逐模块 pre_app_specialize"] MPRE --> SAN["rz_sanitize_fds<br/>关闭泄漏 fd"] SAN --> ORIG["调原 nativeForkAndSpecialize"] ORIG --> APOST["rz_app_specialize_post<br/>rz_run_modules_post"] APOST --> CLEAN["rz_cleanup<br/>还原 JNI/PLT, 触发自卸载"]
  • rz_run_modules_prehook.c:993-1000):对每个模块先 rz_module_call_on_load(调 zygisk_module_entry 完成注册),再按模块声明的标志调 pre_app_specializepre_server_specialize
  • rz_run_modules_posthook.c:1002-1031):调 post 回调;之后按模块的 unload 标志决定 csoloader_unload(卸载)或 csoloader_abandon(保留)。

6.2 参数处理:isolated uid 与 fd 清理

  • niceName:pre 阶段 GetStringUTFChars 取出存入 ctx->process,post 阶段释放(hook.c:1149/1154/1189)。
  • isolated uid 修正hook.c:1048-1076):对隔离服务(IS_ISOLATED_SERVICE(uid),即 90000 ≤ uid < 1000000stat 其 app 数据目录取真实 app uid 再传给 zygiskd,防止隔离进程绕过 DenyList。随后 rezygiskd_get_process_flags(uid, process) 拿 01 篇所述的进程标志。
  • fd 清理rz_fork_pre 记录 + rz_sanitize_fdshook.c:824-927):记录 fork 前打开的 fd,合并模块通过 exempt_fd 申请豁免的 fd 进 fds_to_ignore,关闭其余"泄漏" fd,防止特化后崩溃。

七、模块 API 与自卸载

7.1 模块 API/ABI(module.h)

API 版本 REZYGISK_API_VERSION = 5module.h:12)。三个核心结构:

// module.h:120-128  对应 zygisk::ModuleBase 的 vtable
struct rezygisk_abi {
  long api_version;
  void *impl;
  void (*pre_app_specialize)(void *, void *);
  void (*post_app_specialize)(void *, const void *);
  void (*pre_server_specialize)(void *, void *);
  void (*post_server_specialize)(void *, const void *);
};

// module.h:130-149  对应 zygisk::Api
struct rezygisk_api {
  void *impl;
  bool (*register_module)(struct rezygisk_api *, struct rezygisk_abi const *);
  void (*hook_jni_native_methods)(JNIEnv *, const char *, JNINativeMethod *, int);
  union { /* v3- */ void (*plt_hook_register)(...);     /* v4 */ void (*plt_hook_register_v4)(dev_t, ino_t, ...); };
  union { /* v3- */ void (*plt_hook_exclude)(...);      /* v4 */ void (*exempt_fd)(int); };
  bool (*plt_hook_commit)();
  int  (*connect_companion)(void *);
  void (*set_option)(void *, enum rezygisk_options opt);
  int  (*get_module_dir)(void *);    // v2+
  uint32_t (*get_flags)();           // v2+
};

union 用于在不同 API 版本间复用同一槽位(v3 的 plt_hook_register 与 v4 的 plt_hook_register_v4 占同一位置)。

模块 option(module.h:99-118):

选项 含义
FORCE_DENYLIST_UNMOUNT 0 在 preSpecialize 设置:令 ReZygisk 为该进程卸载 root 挂载
DLCLOSE_MODULE_LIBRARY 1 postSpecialize 后卸载模块库(若模块还留有 hook 引用则危险)

版本兼容rezygisk_module_registerhook.c:782-811)按模块声明的 api_version 装配函数指针表;module.h:173-301 的内联分发函数把新版 app_specialize_args_v5 转换成模块期望的 v1/v4 结构,保持对旧模块的 ABI 兼容,且对缺失的回调判空跳过(原版 Zygisk 缺函数会空指针解引用,这里更稳健)。

模块 ID 用 ENCODE_ID/DECODE_IDhook.c:724-726)编码成不透明指针,所有 API 入口都校验范围,防止模块乱传 impl

7.2 自卸载:should_unmap_zygisk

注入体完成使命后要从 zygote 内消失(否则自身就是检测特征)。但卸载有个致命前提:所有 hook 必须先成功还原,否则方法表/PLT 还指向 libzygisk 内存,munmap 后再调用就 segfault。

ReZygisk 用一个全局标志 should_unmap_zygisk 把控:

flowchart TB C["rz_cleanup (hook.c:1220)"] --> C1["should_unmap_zygisk = true"] C1 --> C2["还原 JNI 方法表<br/>RegisterNatives(原方法)"] C2 --> C3{"还原成功?"} C3 -->|否| F1["should_unmap_zygisk = false"] C3 -->|是| OK1["继续"] OK1 & F1 --> EN["enable_unloader = true"] EN --> PA["new_pthread_attr_setstacksize<br/>(hook.c:302) 被调用"] PA --> UH["unhook_functions() 还原 PLT"] UH --> C4{"PLT 还原成功?<br/>(should_unmap_zygisk 仍为 true?)"} C4 -->|否| SKIP["跳过 munmap<br/>保留 libzygisk 存活"] C4 -->|是| MM["musttail munmap(start_addr, block_size)<br/>自我卸载"]
  • JNI 还原失败should_unmap_zygisk = falsehook.c:1235)。
  • PLT 还原失败hook_unregister 里置 should_unmap_zygisk = falsehook.c:1299)。
  • 真正的 munmap 由 hook 在 pthread_attr_setstacksize 里执行(hook.c:302-342)——选这个函数是因为它在 VM 守护线程启动前后被调用,能在 APP 跑任何代码前完成卸载,且签名兼容 [[clang::musttail]] 尾调用(munmap 自己所在的内存,必须尾调用,否则返回时栈已不存在)。
// hook.c:316-339(节选)自卸载的核心保护
if (should_unmap_zygisk) {
  unhook_functions();              // 还原剩余 PLT hook
  csoloader_deinit();
  if (!should_unmap_zygisk) {      // unhook 期间可能被置 false
    LOGW("Failed to unmap libzygisk.so, skipping munmap");
    enable_unloader = false;
    free(zygisk_modules); plti_deinit(&plti_ctx);
    return res;                     // 放弃卸载,但进程继续存活
  }
  free(zygisk_modules); plti_deinit(&plti_ctx);
  [[clang::musttail]] return munmap(start_addr, block_size);  // 尾调用自我卸载
}

这正是仓库近期提交 "fix: not unload ReZygisk if unhook failed" 的含义:宁可让 libzygisk 残留在内存里(增加一点被检测的风险),也绝不能在 hook 没还原干净时 munmap 自己——后者会直接让 APP 崩溃。可用性 > 隐蔽性,这是个正确的权衡。

7.3 companion 连接

模块通过 api.connect_companion(impl)hook.c:728-738)→ rezygiskd_connect_companion(index)daemon.c,发送 RequestCompanionSocket)拿到与 companion 直连的 socket fd。这就接上了 01 篇第六节描述的 companion 接力流程。


八、反检测基石之一:ptrace_clear

注入用了 ptrace,内核会记录"最后一次 ptrace 事件信息",可能被检测。perform_ptrace_message_clearptrace_clear.c:39-108)主动触发一次受控的 seccomp 事件来覆盖它:

flowchart TB A["检查 /proc/self/status<br/>有无 Seccomp_filters: 行"] --> B{"可见?(内核 >= 5.10)"} B -->|是| SKIP["放弃 (此手法失效)"] B -->|否| R["从 /dev/urandom 取 4 个随机数<br/>args[0] |= 0x10000"] R --> F["安装一次性 seccomp BPF:<br/>仅当 exit_group 且 4 参数全等随机值<br/>→ SECCOMP_RET_TRACE"] F --> S["syscall(exit_group, 随机参数)"] S --> E["命中过滤器 → PTRACE_EVENT_SECCOMP<br/>(tracer 会 skip 该 syscall, 不真退出)"] E --> DONE["副作用: 刷新了内核 ptrace event message<br/>掩盖原注入痕迹"]

精妙之处:用一个带随机参数、必不会被正常代码触发的 exit_group 去命中自装的 seccomp 过滤器,从而刷新内核里的 ptrace event 信息,但因为 tracer 会跳过这个 syscall,进程并不会真的退出。内核 ≥ 5.10 时 seccomp 过滤器对外可见,此手法失效,于是直接放弃(ptrace_clear.c:17-44)。


九、反检测基石之二:ELF 解析与符号定位

hook 要改谁、远程要调用哪个函数,都依赖"在内存里找到符号地址"。elf_util.c 提供这个能力。

9.1 思路

运行时内存里通常看不到 .symtab、节头表,所以 ReZygisk 的做法是(elf_util.c):

flowchart LR A["dl_iterate_phdr<br/>找库的加载基址 base"] --> B["从磁盘文件 mmap 整个 ELF"] B --> C["解析节表/符号表/哈希表"] C --> D["查符号 → 得 st_value(文件视角)"] D --> E["运行时地址 = base + st_value - bias"] E --> F{"STT_GNU_IFUNC?"} F -->|是| G["调 resolver 得真实地址"] F -->|否| H["直接返回"]

9.2 三种符号查找

统一入口 getSymbOffsetelf_util.c:656-669)按 GNU 哈希 → SysV 哈希 → 线性扫描 顺序查找:

  • GNU 哈希GnuLookupelf_util.c:497-576):先用 bloom filter 快速排除,再沿 chain 比对。
  • SysV 哈希ElfLookupelf_util.c:578-596)。
  • 线性查找LinearLookupelf_util.c:598-654):.symtab 通常无哈希表,只能逐个扫,支持前缀匹配(hook C++ mangled 符号时用)。

getSymbAddresself_util.c:774-787)把偏移换算成运行时地址:base + offset - bias,并对 IFUNC 符号调用其 resolver(handle_indirect_symbolelf_util.c:746-772,按架构以不同签名调用,处理 AT_HWCAP 等)。

02 篇里 ptracer 的 find_func_addr 正是基于此:本地解析偏移 + 远程基址 = 目标进程里的函数地址。


十、反检测基石之三:IFUNC 垫片、std::string 与 ArtMethod

10.1 ifunc_shim:ARM32 的 libc 垫片

ifunc_shim.c(仅 #ifdef __arm__ 编译)解决 Tango 场景的一个棘手问题:注入 32 位 app_process 时(从 64 位宿主),还无法解析 ARM32 的 IFUNC 符号(那需要在目标环境真正执行 resolver)。而现代 bionic 的 memcpy/strcmp/... 多是 STT_GNU_IFUNC

解法:在本编译单元里用纯 C 提供这些函数的最小实现visibility("hidden")),让链接器把调用解析到本地定义,从而完全避免从 libc 导入它们ifunc_shim.c:19-27 等)。还覆写了 __strcpy_chk/__memset_chk_FORTIFY_SOURCE 会生成的变体,并 #undef _FORTIFY_SOURCE 防止改写自身定义。

配合近期提交 "improve: only compile Tango-related code for AArch64",这类垫片只在相关构建里参与。

10.2 cpp_strings:读 libc++ std::string

注入体是纯 C,但要 hook 的 FileDescriptorInfo::ReopenOrDetach 参数里含 C++ std::string(文件路径)。cpp_strings.c 按 libc++ 的内存 ABI 手工解析(cpp_strings.c:20-42):

static inline bool is_short_string(const unsigned char *bytes) {
  return (bytes[0] & 1) == 0;   // 小端:首字节 LSB=0 即短模式(SSO)
}
const char *read_std_string(const void *p) {
  const unsigned char *bytes = p;
  if (is_short_string(bytes)) return (const char *)(bytes + 1);       // 短:数据紧跟首字节
  return *(const char **)((const char *)p + LONG_DATA_OFFSET);        // 长:取指针字段
}

处理了 libc++ 的 SSO(小字符串优化)布局,假定 libc++ + 小端(对 Android 成立)。

10.3 art_method:探测 ArtMethod 布局

ART 里每个 Java 方法是一个 ArtMethod,其末尾字段含 native 入口。3.2 节的 hook_jni_methods 要读这个入口拿"原函数地址"。但 ArtMethod 的布局随 Android 版本变化,于是 amethod_initart_method.h:27-101)用一个巧妙的运行时探测:

// 取 Throwable 的两个相邻构造器,用地址差推算结构尺寸
art_method_size   = second_ctor_addr - first_ctor_addr;
entry_point_offset = art_method_size - sizeof(void *);   // 末字段 = entry point
data_offset        = entry_point_offset - sizeof(void *); // 倒数第二 = data(原入口)

amethod_get_dataart_method.h:103-105)据此读出原 native 入口。这种"用相邻对象地址差推断结构大小"的手法,免去了为每个 Android 版本硬编码偏移。

10.4 parse_maps_safe:反 atime 检测

/proc/<pid>/maps 会更新该文件的 access time,APP 可用 stat() 检测"有人窥探过我的内存布局"。parse_maps_safemisc.c:58)的对策:fork 一个子进程,由子进程打开 maps 并经 SCM_RIGHTS 把 fd 传回父进程读取——更新的是子进程那份 maps 的 atime,父进程的不受影响,从而不可检测。父子用 1 字节握手协调生命周期,最后 waitpid 回收避免僵尸。


十一、小结

注入体 libzygisk.so 是 ReZygisk 在 zygote 内的"常驻特工",本篇要点:

  1. entry() 三步走:装 PLT hook → 清 ptrace 痕迹 → 报告注入。它本身不加载模块。
  2. 双 hook 体系:PLT hook(PLTI)改 C 函数调用,ART RegisterNatives 替换 Zygote native 方法;后者靠读 ArtMethod 拿原入口。
  3. 两个时机探针strdup("...ZygoteInit") 触发 JNI hook 安装;property_get 首次调用触发模块加载(在 system_server fork 前,确保扩散)。
  4. 碎片化适配gen_jni_hooks.py 为 12+6+2 个 JNI 变体(含三星/GrapheneOS)生成包装,运行时按签名择一。
  5. 模块生命周期:pre/post × app/server specialize 四回调,参数以指针传入可被模块修改;companion 经 zygiskd 接力直连。
  6. 可用性优先的自卸载:只有所有 hook 还原成功才 munmap 自己;任一失败则保留存活,宁可留痕也不让 APP 崩。
  7. 反检测基石:ELF 自解析定位符号、ARM32 IFUNC 垫片、ArtMethod 结构探测、libc++ string 解析、seccomp 事件清理、反 atime 的 maps 读取。

下一篇(04)走出 native 代码,看 ReZygisk 的外围:三类 socket 的 IPC 协议全景、模块安装/开机启动脚本链、WebUI 与构建系统。

评论

  • 还没有评论,来说点什么吧。

无需注册或登录,填个昵称即可评论。