Lazy loaded image
【看雪转载】基于最新版 Magisk Zygisk,移植实现一个带 WebUI、支持 Android 16 的 Zygisk 等效模块r0zygisk(一)
Words 18724Read Time 47 min
2026-7-13
2026-7-13
type
Post
status
Published
date
Jul 13, 2026
slug
summary
基于最新版 MagiskZygisk ,整理并移植到 r0zygisk 的完整过程,同时把 KernelSU / SukiSU 环境下的 WebUI 一并补齐。
tags
r0zygisk
看雪
category
转载
icon
password
网址
上次编辑时间
Jul 13, 2026 07:27 AM
comment
AI 总结

Vibe Coding 从源码阅读到模块开发:基于最新版 Magisk Zygisk,移植实现一个带 WebUI、支持 Android 16 的 Zygisk 等效模块r0zygisk(一)

这篇文章主要记录了基于最新版 MagiskZygisk ,整理并移植到 r0zygisk 的完整过程,同时把 KernelSU / SukiSU 环境下的 WebUI 一并补齐。
完整源码和发布包已经开源在 GitHub Releases:
整个过程可以归纳成八条主线:
  1. 先解决项目本身无法构建的问题。
  1. 再修复生成 zip 后 SukiSU / KernelSU 无法安装的问题(缺少正确目录的 libzygisk.so )。
  1. 然后迁移 r0zygisk 的 Zygisk API,以及 Android 16 / Baklava 相关 JNI 签名。
  1. 接着梳理 zygiskd 的执行链路,为后续排查 daemon / loader / companion 问题建立上下文。
  1. 再重构 WebUI,并围绕真实刷入反馈连续修复 exec 、状态显示和 SukiSU 模块描述污染问题。
  1. 完成第一版阶段性收敛,固化可复现的构建与刷入结果。
  1. 对 Hunter 检测链路做逆向定位,明确命中点在 native 侧 checkZygisk 分支。
  1. 结合迁移记录完成第二版大修:切换注入模式并统一命名为 r0z

目录

  1. 项目背景与环境
  1. 先确认最新版 Magisk Zygisk 是否已适配 Android 16
  1. 借助 code-panorama + Codex 梳理 Zygisk 源码
  1. 开始移植:把最新版 Zygisk 迁到 r0zygisk
  1. WebUI 重构与联调记录
  1. 最终状态
  1. 第二版大修:通过 Hunter 的 Zygisk 检测
  1. 结合 AI:使用新的注入方式完成第二版代码
  1. 总结

一、项目背景与环境

关于 MagiskZygiskNext 老版本源码的阅读,这次我用到了一个开源库:
https://github.com/xuanyuanzhifeng/code-panorama
这个工具对源码阅读和结构定位帮助很大。
文章整体也使用了 AI 辅助完成,具体环境是: codex cli + gpt5.4 模型。 !
结果测试覆盖了 Android 14 到 Android 16 机型。
项目初始目录结构如下: !
r0zygisk 是参考 ZygiskNext 的内容继续做的,使用 Android Studio 编译,开发环境如下。
Android Gradle Plugin 和 Gradle Wrapper 版本:
  • Android Gradle Plugin: 8.9.1
  • Gradle Wrapper: 8.11.1
JDK 配置在 gradle.properties 中:
  • org.gradle.java.home=/Library/Java/JavaVirtualMachines/jdk-17.jdk/Contents/Home
项目打包过程中,前期遇到的问题主要有这些:
  1. Gradle 使用 Java 8,改为项目级固定 JDK 17。
  1. Java/Gradle 访问 Google Maven TLS 失败,增加国内 Maven 镜像。
  1. 中文路径导致 Gradle/JVM 编码问题,增加 UTF-8 JVM 参数。
  1. 当前目录不是 Git 仓库,版本号读取失败,增加 fallback。
  1. 本机缺少项目指定 NDK,改为本机已安装的 NDK 26.1.10909125
  1. 源码包缺少 lsplt 子模块,按 .gitmodules 克隆补齐。
  1. Rust 缺少 Android 交叉编译 target,用 rustup target add 安装四个目标平台。
开发环境准备好之后,就开始进入正式工作:基于最新版 MagiskZygisk ,做一个支持 Android 16 的 r0zygisk

二、先确认最新版 Magisk Zygisk 是否已适配 Android 16

动手移植之前,先确认当前 Magisk-master 这版到底有没有完成 Android 16 适配。
我先让 AI 帮我判断这版 Magisk-master 的适配情况,提问如下:
notion image
得到的分析结果如下:
通过对 Magisk 仓库的查看和 AI 分析结果,可以确认这个版本已经适配 Android 16,那么后续就可以直接基于这版 Zygisk 来做迁移。

三、借助 code-panorama + Codex 梳理 Zygisk 源码

3.1 分析过程

在开始改代码之前,先把最新版 Magisk Zygisk 的执行流程和关键入口梳理清楚。
阅读 Magisk 源码时,我先把前面提到的开源库本地安装并运行起来: !
点一下生成全景图,就开始分析了: !
生成结果如下: !
借助这个视图,可以快速定位主要入口。
由于我主要关注的是 zygisk ,因此后续单独针对这部分代码继续下钻: !
使用这个库后,项目入口和主要结构会清晰很多。具体使用过程这里就不展开了。
在这个基础上,我再结合 codex 继续分析代码。
/Magisk-master/native/src/core/zygisk 目录下,打开 codex,给它下指令:
notion image
等待 AI 执行: !
结果: !

3.2 Magisk Zygisk 源码执行流程与加载过程逐段详解

#### 3.2.0 分析标题与范围
本文基于当前目录 /native/src/core/zygisk 中的源码编写,目标是把 Zygisk 的“如何被加载、如何接管 Zygote、如何加载模块、如何与 magiskd/zygiskd 协作、最后如何清理卸载”串成一条完整链路,并按文件解释代码。
目录内文件职责:
| 文件 | 作用 | | --- | --- | | zygisk.hpp | Zygisk 内部公共声明、native bridge 常量、日志宏、NativeBridge ABI 结构 | | entry.cpp | Zygisk 的两个 C++ 入口:NativeBridge 注入口、zygisk companion 进程入口 | | hook.cpp | bootstrap 核心:PLT hook、JNI hook、真实 native bridge 续载、Zygisk 自卸载 | | module.hpp | 模块 ABI、API 表、specialize 参数对象、 ZygiskContext / ZygiskModule 类型定义 | | module.cpp | 模块加载、API 适配、pre/post specialize 调度、FD 清理、denylist/unmount 处理 | | api.hpp | 面向 Zygisk 模块作者的公开 API 头文件 | | jni_hooks.hpp | 由脚本生成的 Zygote JNI native 方法包装器 | | gen_jni_hooks.py | 生成 jni_hooks.hpp 的脚本,维护不同 Android/OEM JNI 签名 | | daemon.rs | magiskd 侧 Zygisk 状态、属性注入、模块 fd 下发、companion 连接管理 | | mod.rs | Rust 模块入口,导出 C++ 调用的 exec_companion_entry |

#### 3.2.1 总执行链路
##### 3.2.1.1 启用阶段:magiskd 写入 native bridge 属性
Zygisk 不是通过修改 Zygote 可执行文件直接进入的,而是借用了 Android native bridge 加载机制。
Rust 侧 ZygiskState::set_prop() 修改属性:
写入策略:
  1. 如果原属性为空或 "0" ,写成 libzygisk.so
  1. 如果原本已有 native bridge,则写成 libzygisk.so 加上原值。
因此 Zygote 启动 native bridge 时会优先加载 libzygisk.so 。如果系统原本就配置了 native bridge,Zygisk 后续会从属性字符串里剥出原值并重新加载真实 native bridge,避免破坏原系统行为。
##### 3.2.1.2 注入阶段:Zygote 加载 libzygisk.so
Android 的 LoadNativeBridge 会查找动态库中的导出符号 NativeBridgeItf 。Zygisk 在 entry.cpp 中导出同名符号:
isCompatibleWith() 被调用时,Zygisk 已经进入 Zygote 进程。这里执行:
  1. zygisk_logging() 初始化日志。
  1. hook_entry() 开始 bootstrap。
  1. 返回 false ,表示自己不是真正要使用的 native bridge。
##### 3.2.1.3 Bootstrap 阶段:安装早期 PLT hook
hook_entry() 创建全局 HookContext ,调用 HookContext::hook_plt()
第一批 PLT hook 挂在两个库上:
| 目标库 | 被 hook 的符号 | 目的 | | --- | --- | --- | | libnativebridge.so | dlclose | 捕获 native bridge 加载流程结束点,拿到 runtime callbacks,并补载真实 native bridge | | libandroid_runtime.so | strdup | 捕获 com.android.internal.os.ZygoteInit 字符串,触发 JNI 方法替换 | | libandroid_runtime.so | fork | Zygisk 提前 fork 后,让原始逻辑看到缓存 pid,避免重复 fork | | libandroid_runtime.so | unshare | 进入新 mount namespace 后执行 denylist unmount | | libandroid_runtime.so | selinux_android_setcontext | secontext 切换前预取 logd | | libandroid_runtime.so | __android_log_close | fork/specialize 过程中控制 log fd 关闭 |
##### 3.2.1.4 接管 Zygote JNI:替换 specialize native 方法
AndroidRuntime 启动 Java 层 ZygoteInit 前会处理类名字符串。 strdup("com.android.internal.os.ZygoteInit") 触发 Zygisk 的 new_strdup() ,调用 hook_zygote_jni()
hook_zygote_jni() 做三件事:
  1. 找到当前 JavaVMJNIEnv
  1. 用 NativeBridgeRuntimeCallbacks 枚举 com/android/internal/os/Zygote 已注册 native 方法。
  1. RegisterNatives 替换这些目标方法:
  • nativeForkAndSpecialize
  • nativeSpecializeAppProcess
  • nativeForkSystemServer
替换后的函数来自 jni_hooks.hpp 。它们都是包装器:先构造 ZygiskContext ,执行 pre 回调,再调用原始 JNI 函数,最后执行 post 回调。
##### 3.2.1.5 App/system\_server fork 与模块加载
当 Zygote 要创建 app 或 system\_server 时,流程进入 Zygisk 包装器:
  1. 构造 AppSpecializeArgs_v5ServerSpecializeArgs_v1
  1. 构造栈上 ZygiskContext ,全局 g_ctx 指向它。
  1. pre 阶段:
  • 对 fork 型方法,Zygisk 先调用真实 fork()
  • 子进程向 magiskd 查询当前 uid/process 的 flags。
  • 如果进程不在 enforced denylist 且不是 Magisk app,则接收模块 so 的 fd。
  • 使用 android_dlopen_ext(..., ANDROID_DLEXT_USE_LIBRARY_FD, ...) 从 fd 加载模块。
  • 调用模块 zygisk_module_entryonLoadpreAppSpecializepreServerSpecialize
  1. 调用原始 Zygote native specialize 函数。
  1. post 阶段:
  • 调用模块 postAppSpecializepostServerSpecialize
  • 根据模块选项可能 dlclose 模块。
  • 恢复 Zygote JNI hook。
  • 安装自卸载 hook。
##### 3.2.1.6 companion 进程
模块如果需要 root 权限,可以调用 Api::connectCompanion() 。这不会让 app 进程变成 root,而是:
  1. 模块进程向 magiskd 发 ZygiskRequest::ConnectCompanion
  1. magiskd 按 32/64 位选择或启动一个 zygiskd
  1. zygiskd 预先加载所有模块 companion entry。
  1. 每次模块请求 companion 时,magiskd 把 client fd 转交给对应 zygiskd
  1. zygiskd 调用目标模块的 zygisk_companion_entry(client)
##### 3.2.1.7 清理与自卸载
子进程 specialize 完成后,Zygisk 不希望一直留在 app/system\_server 进程中。析构 ZygiskContext 时:
  1. 清掉模块 API 表。
  1. 标记 g_hook->should_unmap = true
  1. 恢复 Zygote JNI 方法。
  1. libart.so 上 hook pthread_attr_destroy
后续 JVM 创建线程时会调用 pthread_attr_destroy 。Zygisk 在这个安全时机:
  1. 恢复早期 PLT hook。
  1. musttail 跳转到 dlclose(self_handle)
  1. 避免 dlclose 返回到已经被 unmap 的 Zygisk 代码。

#### 3.2.2 zygisk.hpp 逐行/逐段解释
##### 行 1-4:基础包含
#pragma once 防止头文件重复包含。 jni.h 提供 JNI 类型, core.hpp 提供 Magisk core 内部工具、日志、IPC 等声明。
##### 行 6-8:关键常量
ZYGISKLDR 是 Zygisk loader 库名: libzygisk.so
NBPROP 是 native bridge 属性名: ro.dalvik.vm.native.bridge 。Rust 与 C++ 两边都使用同一语义。
##### 行 9-19:按 ABI 区分日志前缀
64 位编译时日志前缀为 zygisk64 ,32 位为 zygisk32 。这样同一设备有双 Zygote 时,logcat 中可以区分是哪一套代码在运行。
##### 行 21-23:极详细日志开关
ZLOGV 默认被定义成空操作。取消注释可把 verbose 日志转成 debug 日志,但正常构建不启用,避免刷屏和性能影响。
##### 行 25-26:跨文件函数声明
hook_entry() 是 native bridge 回调进入 bootstrap 的入口。
hookJniNativeMethods() 是 Zygisk API 暴露给模块的 JNI hook 实现入口,内部会转给当前全局 HookContext
##### 行 28-42:NativeBridge ABI 结构
这两个结构是 Android native bridge 头文件中的 ABI 子集:
NativeBridgeRuntimeCallbacks 提供三类能力:
  1. 通过 method id 获取 shorty。
  1. 查询某个 class 的 native 方法数量。
  1. 导出某个 class 的 native 方法表。
Zygisk 依赖它枚举并替换 Zygote native 方法。
NativeBridgeCallbacks 是 native bridge 动态库导出的 NativeBridgeItf 结构。Zygisk 只填了版本、padding、 isCompatibleWith

#### 3.2.3 entry.cpp 逐行/逐段解释
##### 行 1-9:系统与内部头文件
android/dlext.h 用于从 fd 加载 so。 dlfcn.h 用于 dlsympoll.h 用于 companion daemon 等待请求。
##### 行 13-14:companion entry 类型
comp_entry 是模块 companion 函数类型: void (*)(int)
exec_companion_entry 是 Rust 侧 mod.rs 导出的 C ABI 函数。C++ 不直接创建线程,而是把 companion 请求交给 Rust 线程池执行。
##### 行 16-19:zygiskd 参数校验
zygiskd(int socket) 是 root companion daemon 主循环。开头要求:
  1. 当前 uid 必须是 root。
  1. 传入 socket fd 必须有效。
不满足直接退出。
##### 行 20-26:设置进程名与日志
64 位进程命名为 zygiskd64 ,32 位为 zygiskd32 。这和 magiskd 中按 ABI 维护两个 socket 对应。
##### 行 28-49:加载模块 companion entry
从 magiskd 传来的 socket 中接收一组模块 fd。每个 fd:
  1. fstat 确认是普通文件。
  1. 使用 android_dlopen_ext("/jit-cache", RTLD_LAZY, &info) 从 fd 加载。
  1. 查找符号 zygisk_companion_entry
  1. 不管成功与否,都往 modules vector 放入 entry 指针,保证模块 id 与数组下标一致。
  1. 关闭原 fd。
"/jit-cache" 只是提供给 linker 的伪路径,真实库来自 fd。
##### 行 51-53:启动确认
write_int(socket, 0) 告诉 magiskd:zygiskd 已经加载模块 companion 表,可以开始转发请求。
##### 行 54-73:zygiskd 主循环
poll 阻塞等待 magiskd 发来 client fd。每次请求:
  1. recv_fd(socket) 收到一个 client socket。
  1. 从 client 读 module_id
  1. 如果 id 合法且该模块有 companion entry,调用 exec_companion_entry(client, entry)
  1. 否则关闭 client。
这里的 companion entry 不是直接同步执行,而是进入 Rust 线程池,允许并发处理。
##### 行 76-84:zygisk\_main
这是 magisk applet 入口,只有内部执行 magisk zygisk companion <fd> 时使用。
参数匹配 argc == 3 && argv[1] == "companion" 时进入 zygiskd(parse_int(argv[2]))
##### 行 86-96:NativeBridge 注入口
导出符号 NativeBridgeItf ,让 Android native bridge loader 能找到。
isCompatibleWith lambda 是 Zygisk 第一次执行自己代码的关键点:
  1. 初始化 Zygisk 日志。
  1. 调用 hook_entry()
  1. 打印加载成功。
  1. 返回 false
返回 false 是有意的:Zygisk 要借 native bridge 入口执行注入,而不是最终承担 native bridge 功能。

#### 3.2.4 hook.cpp 逐行/逐段解释
##### 行 1-15:依赖
sys/mman.hsys/resource.hdlfcn.hunwind.h 支撑内存映射扫描、fd 上限、动态符号、栈回溯。
lsplt.hpp 是 PLT hook 库。Zygisk bootstrap 和模块 API 的 PLT hook 都依赖它。
module.hpp 提供 ZygiskContextjni_hooks.hpp 提供生成好的 JNI 包装函数列表。
##### 行 18-90:源码内 bootstrap 流程图
这段注释是官方写在源码里的关键说明。它表达的核心是:
  1. NativeBridgeItf 被 native bridge loader 调用。
  1. Zygisk 安装 PLT hook。
  1. dlclose hook 捕获 native bridge 加载结束。
  1. strdup("ZygoteInit") hook 捕获 JVM 即将进入 ZygoteInit 的时机。
  1. Zygisk 替换 Zygote JNI native 方法。
  1. specialize 后通过 pthread_attr_destroy hook 自卸载。
##### 行 91-99:常量与类型别名
kZygoteInit 是 Java 层入口类全名。
kZygote 是 JNI 查找类名格式: com/android/internal/os/Zygote
kForkAppkSpecializeAppkForkServer 是要替换的 native 方法名。
JNIMethodsstd::span<JNINativeMethod> ,方便对生成数组做统一处理。
JNIMethodsDyn 是动态分配的 JNI 方法表和数量。
##### 行 100-119:HookContext
HookContext 继承 JniHookDefinitions ,因此直接持有三组生成的 JNI hook 数组:
  1. app fork 方法数组。
  1. app specialize 方法数组。
  1. system\_server fork 方法数组。
字段含义:
| 字段 | 含义 | | --- | --- | | plt_backup | 记录已经成功注册的 PLT hook,用于后续恢复 | | runtime_callbacks | NativeBridgeRuntimeCallbacks,枚举 JNI native 方法时使用 | | self_handle | Zygisk 自身的 dlopen handle,用于最后 dlclose | | should_unmap | 是否允许自卸载 |
方法分两类:
  1. bootstrap/卸载: hook_plthook_unloaderrestore_plt_hookpost_native_bridge_load
  1. JNI 替换: hook_zygote_jnirestore_zygote_hookhook_jni_methods
##### 行 123-136:全局上下文
g_ctx 指向当前 specialize 流程中的 ZygiskContext 。它通常指向栈对象,只在包装 JNI 函数执行期间有效。
g_hook 指向长期存在的 HookContext ,从 native bridge 加载后一直存在到自卸载前。
get_defs() 返回 g_hook ,供 jni_hooks.hpp 生成的包装函数获取原始 native 函数指针。
##### 行 140-149:strdup hook
new_strdup(const char *str) 检查字符串是否等于 com.android.internal.os.ZygoteInit
如果匹配,调用 g_hook->hook_zygote_jni()
随后调用原始 strdup 。这个 hook 的意义是抓住 AndroidRuntime 即将启动 ZygoteInit 的窗口,此时 JVM 已创建,Zygote native 方法也已经可枚举和替换。
##### 行 151-154:fork hook
new_fork() 的逻辑:
  1. 如果 g_ctx 存在并且 g_ctx->pid >= 0 ,返回缓存 pid。
  1. 否则调用原始 fork()
Zygisk 在 fork_pre() 中会提前执行真正 fork。随后原始 Zygote native 函数内部再调用 fork() 时,会被这个 hook 拦截并返回同一个 pid,避免 fork 两次。
##### 行 156-167:unshare hook
Zygote specialize 过程中可能调用 unshare(CLONE_NEWNS) 创建私有 mount namespace。
如果当前在 Zygisk specialize 流程中,且创建 mount namespace 成功,并且 flags 中有 DO_REVERT_UNMOUNT ,则调用 revert_unmount() 。这用于 denylist 或模块强制隐藏时,从目标进程 mount namespace 中卸载 Magisk/模块痕迹。
##### 行 169-175:selinux\_android\_setcontext hook
secontext 改变后,进程权限和 SELinux 限制会发生变化。这里在切换前调用 zygisk_get_logd() ,预先拿到 logd fd,避免后续上下文下无法访问或日志管道异常。
##### 行 177-185:\_\_android\_log\_close hook
Zygote fork/specialize 时会关闭日志 fd。Zygisk 根据 SKIP_CLOSE_LOG_PIPE 控制是否关闭自己的 log 管道,然后调用原函数。
##### 行 187-194:dlclose hook
这是 native bridge bootstrap 的关键:
  1. native bridge loader 在加载结束附近会 dlclose loader handle。
  1. Zygisk 把 libnativebridge.sodlclose PLT 替换成 new_dlclose
  1. 第一次触发时 self_handle 还为空,调用 post_native_bridge_load(handle)
  1. 返回 0,不真正关闭。
post_native_bridge_load() 会用这个 handle 记录 Zygisk 自身,并通过栈回溯找到真实 LoadNativeBridge 和 runtime callbacks。
##### 行 196-223:pthread\_attr\_destroy hook
Zygisk 不能直接在自己的函数栈上调用 dlclose(self_handle) ,否则代码被 unmap 后返回地址可能落在已卸载内存里。
解决方式:
  1. 后期 hook libart.sopthread_attr_destroy
  1. JVM 创建 daemon 线程时会调用它。
  1. Zygisk 在主线程触发时恢复 PLT hook。
  1. 如果仍允许卸载,取出 self_handle ,删除 g_hook
  1. 使用 [[clang::musttail]] return dlclose(self_handle);,让 dlclose 直接返回给 pthread_attr_destroy 的调用者。
这样不会返回到 Zygisk 的已卸载代码。
##### 行 229-237:fd 上限与 ZygiskContext 构造
get_fd_max() 读取 RLIMIT_NOFILE ,默认准备 32768。
ZygiskContext 构造函数保存 JNIEnv 、specialize 参数、初始化 pid/flags/fd 位图和 mutex,并设置全局 g_ctx = this
##### 行 239-260:ZygiskContext 析构
析构时先把 g_ctx 清空,因为它指向栈对象。
如果不是子进程,直接返回。父 zygote 不能卸载 Zygisk,否则后续 fork 无法继续被 hook。
子进程中:
  1. 关闭 Zygisk logd。
  1. 重新初始化 Android logging。
  1. 清空每个模块的 API 表,防止 post 后继续使用。
  1. 设置 should_unmap = true
  1. 恢复 Zygote JNI hook。
  1. 安装 pthread_attr_destroy 卸载 hook。
##### 行 264-276:unwind\_get\_region\_start
封装 _Unwind_GetRegionStart 。ARM32 下如果 PC 处于 Thumb 模式,要把最低位置 1,保证函数地址匹配真实调用地址。
##### 行 278-344:find\_runtime\_callbacks
目标是从栈回溯上下文中找 NativeBridgeRuntimeCallbacks *
步骤:
  1. 扫描 /proc/self/maps ,找 libart.so 的可读写段。
  1. 这个 callbacks 对象位于 libart 的 writable 区域。
  1. 不同 ABI 参数传递方式不同:
  • arm64:查 r19-r28。
  • arm32:查 r4-r10。
  • x86:从 ebp 相对位置读第二参数。
  • x86\_64:查 rbx/r12-r15 等 callee-saved 寄存器。
  • riscv:查 callee-saved x8/x9/x18-x27。
  1. 找到落在 libart writable 范围内的值,即认为是 callbacks 指针。
这是 Zygisk 能枚举已注册 JNI 方法的基础。
##### 行 346-381:post\_native\_bridge\_load
dlclose hook 触发。
主要流程:
  1. 保存 self_handle
  1. 使用 _Unwind_Backtrace 回溯调用栈。
  1. 找到帧所属库是 libnativebridge.so 的函数地址,认为它是 android::LoadNativeBridge
  1. 在同一帧上下文中调用 find_runtime_callbacks()
  1. 若属性 ro.dalvik.vm.native.bridge 的值长于 libzygisk.so ,说明后面拼接了真实 native bridge 名。
  1. 调用真实 LoadNativeBridge(原 native bridge, callbacks) 补载它。
  1. 保存 callbacks 到 runtime_callbacks
##### 行 385-429:注册第一批 PLT hook
register_hook() 包装 lsplt::RegisterHook ,成功后把 (dev, inode, symbol, old_func) 存入 plt_backup
hook_plt() 扫描 maps 找:
  1. libandroid_runtime.so 的 dev/inode。
  1. libnativebridge.so 的 dev/inode。
然后注册:
最后 lsplt::CommitHook() 执行实际 patch,并删除没有成功备份 old\_func 的记录。
##### 行 431-446:注册自卸载 hook
hook_unloader() 扫描 maps 找 libart.so ,注册 pthread_attr_destroy hook。这一步发生在子进程 specialize 完成后,而不是最初 bootstrap 时。
##### 行 448-460:恢复 PLT hook
遍历 plt_backup ,把每个 symbol 替回 old\_func。若任何一步失败,设置 should_unmap = false ,避免在 hook 状态不确定时卸载自己。
##### 行 464-482:读取并注册 JNI native 方法
get_jni_methods() 通过 runtime callbacks:
  1. 查询 class 的 native 方法数量。
  1. 分配数组。
  1. 填充方法表。
register_jni_methods() 逐个调用 RegisterNatives ,允许失败。失败时清 exception,并把该方法的 fnPtr 置空。
##### 行 484-522:hook\_jni\_methods
这是替换 JNI native 方法的核心。
流程:
  1. 读取旧方法表。
  1. 对传入的候选 methods 逐个尝试 RegisterNatives
  1. 再读取新方法表。
  1. 对每个成功替换的方法,根据 name/signature 找到旧方法指针。
  1. 把旧方法指针写回 method.fnPtr
注意源码中特别强调:runtime callbacks 返回的 signature 不是标准格式,所以不能只靠字符串提前判断,而是要直接 RegisterNatives 试。
##### 行 525-532:按类名 hook JNI 方法
对模块 API 提供的入口。若 callbacks/env/class 无效,则把所有 fnPtr 清空。否则 FindClass 后调用上面的核心替换函数。
##### 行 534-609:hook\_zygote\_jni
这是“接管 Zygote specialize”的关键函数。
步骤:
  1. JNI_GetCreatedJavaVMs
  • dlsym(RTLD_DEFAULT, ...)
  • 找不到就扫描并 dlopen libnativehelper.so
  1. 获取 JavaVM
  1. JavaVM 获取当前 JNIEnv
  1. FindClass("com/android/internal/os/Zygote")
  1. 通过 callbacks 获取 Zygote 已注册 native 方法表。
  1. 遍历方法名:
  • 遇到 nativeForkAndSpecialize ,用 fork_app_methods 替换。
  • 遇到 nativeSpecializeAppProcess ,用 specialize_app_methods 替换。
  • 遇到 nativeForkSystemServer ,用 fork_server_methods 替换。
  1. 如果任何已存在目标方法替换失败,恢复已替换的方法,并清空对应数组,避免半 hook 状态。
##### 行 611-616:恢复 Zygote JNI 方法
fork_app_methodsspecialize_app_methodsfork_server_methods 中保存的旧函数指针重新注册回 Zygote 类。
##### 行 620-627:导出给 entry/API 的入口
hook_entry() 创建 g_hook 并安装早期 PLT hook。
hookJniNativeMethods() 是公开 API 的 C++ 实现,转给 g_hook->hook_jni_methods

#### 3.2.5 module.hpp 逐行/逐段解释
##### 行 1-7:基础声明
包含 regex、list 和公开 API 头 api.hpp 。模块 API 兼容层需要正则保存 PLT hook 规则。
##### 行 8-29:前置声明与 ABI 版本别名
Zygisk 支持多个 API/ABI 版本。这里用别名表达版本兼容关系:
  1. AppSpecializeArgs_v2 = AppSpecializeArgs_v1
  1. AppSpecializeArgs_v4 = AppSpecializeArgs_v3
  1. module_abi_v2/v3/v4/v5 都等同于 v1 布局。
  1. api_abi_v3 = api_abi_v2api_abi_v5 = api_abi_v4
这表示某些版本只扩展语义或参数对象,不改变模块 ABI 函数表布局。
##### 行 31-69:App specialize 参数 v3/v5
AppSpecializeArgs_v3 保存 app specialize 的参数引用。必须用引用,因为模块可以修改这些参数,让原始 Zygote specialize 使用修改后的值。
必选字段包括 uid/gid/gids/runtime\_flags/rlimits/mount\_external/se\_info/nice\_name/instruction\_set/app\_data\_dir。
可选字段用指针表示,因为不同 Android 版本和 OEM 签名不同:
AppSpecializeArgs_v5 在 v3 基础上增加 mount_sysprop_overrides
##### 行 71-97:App specialize 参数 v1 兼容层
旧 API v1/v2 没有 rlimits ,构造函数从 v5 转换到 v1 视图。模块如果声明旧 API,Zygisk 会创建这个兼容对象再调用模块回调。
##### 行 99-113:system\_server specialize 参数
ServerSpecializeArgs_v1 保存 system\_server 参数引用:
模块可在 pre 阶段修改这些值。
##### 行 115-122:模块 ABI 函数表
module_abi_v1 是模块注册给 Zygisk 的 ABI:
| 字段 | 含义 | | --- | --- | | api_version | 模块使用的 Zygisk API 版本 | | impl | 模块对象指针 | | preAppSpecialize | app specialize 前回调 | | postAppSpecialize | app specialize 后回调 | | preServerSpecialize | system\_server specialize 前回调 | | postServerSpecialize | system\_server specialize 后回调 |
##### 行 124-131:flags 掩码
static_assert 保证内部 flag 与公开 API flag 值一致。
UNMOUNT_MASK 表示“进程在 denylist 且 denylist enforced”。
PRIVATE_MASK 是不应该暴露给模块的内部 flag,包括 enforced 状态和 Magisk app 标记。
##### 行 133-168:Zygisk API ABI 表
api_abi_base 每个版本都有:
  1. impl 指向 ZygiskModule
  1. registerModule 用于模块注册自身 ABI。
v1 提供:
v2 增加:
v4 改造 PLT hook API,从 regex 匹配改为 dev/inode 精确定位,并增加:
ApiTable union 让同一块内存按不同 ABI 版本解释。
##### 行 170-209:ZygiskModule
ZygiskModule 表示一个已加载到目标进程的 Zygisk 模块。
重要字段:
| 字段 | 含义 | | --- | --- | | id | 模块在 magiskd 模块列表中的下标,用于 companion/getModuleDir | | unload | 模块是否要求 post 后 dlclose | | handle | dlopen handle | | entry | zygisk_module_entry 函数 | | api | Zygisk 注入给模块的 API 表 | | mod | 模块回填的 ABI 表 |
重要方法:
  1. onLoad() 调用模块入口。
  1. pre/post App/ServerSpecialize() 调模块回调。
  1. valid() 验证模块 ABI 是否完整。
  1. connectCompanion() 请求 root companion。
  1. getModuleDir() 从 magiskd 获取模块目录 fd。
  1. setOption() 处理模块选项。
  1. tryUnload() 根据选项 dlclose 模块。
##### 行 211-221:全局符号与内部 flags
g_ctx 在 hook.cpp 定义,供模块 API 访问当前 specialize 上下文。
old_fork 是 PLT hook 保存的原始 fork。
内部 flags:
| flag | 含义 | | --- | --- | | POST_SPECIALIZE | 已进入 post 阶段 | | APP_FORK_AND_SPECIALIZE | 当前是 fork app 流程 | | APP_SPECIALIZE | 当前是 specialize app 流程 | | SERVER_FORK_AND_SPECIALIZE | 当前是 system\_server fork 流程 | | DO_REVERT_UNMOUNT | 需要执行 denylist unmount | | SKIP_CLOSE_LOG_PIPE | 不关闭 Zygisk log pipe |
##### 行 227-284:ZygiskContext
ZygiskContext 是一次 specialize 流程的上下文,生命周期通常是 JNI 包装函数的栈帧。
字段:
| 字段 | 含义 | | --- | --- | | env | 当前 JNIEnv | | args | app 或 server specialize 参数 | | process | 进程名 | | modules | 当前子进程加载的 Zygisk 模块列表 | | pid | Zygisk 预 fork 的结果,子进程为 0,父进程为 child pid | | flags | 当前流程内部状态 | | info_flags | magiskd 返回的进程状态 | | allowed_fds | fork 后允许保留的 fd 位图 | | exempted_fds | 模块通过 API 申请豁免关闭的 fd | | register_info | 旧版 regex PLT hook 注册项 | | ignore_info | 旧版 regex PLT hook 排除项 |
方法按功能分组:
  1. run_modules_pre/post() :加载和执行模块。
  1. fork/app/server/nativeXXX_pre/post() :各类 JNI 包装流程。
  1. get_module_info() :向 magiskd 查询 flags 和模块 fd。
  1. sanitize_fds() :关闭不允许保留的 fd。
  1. exempt_fd() :实现模块 fd 豁免 API。
  1. plt_hook_*() :兼容旧版 regex PLT hook API。

#### 3.2.6 module.cpp 逐行/逐段解释
##### 行 14-19:zygisk\_request
连接 magiskd 的 RequestCode::ZYGISK ,写入具体 ZygiskRequest 子命令,然后返回 socket fd。
后续 GetInfoConnectCompanionGetModDir 都从这里发起。
##### 行 21-27:ZygiskModule 构造
保存 id、handle、entry。清零 API 表,设置:
模块入口拿到 API 表后,会调用 registerModule 把模块 ABI 回填给 Zygisk。
##### 行 29-69:RegisterModuleImpl
模块注册入口。
流程:
  1. 空指针检查。
  1. 读取模块声明的 api_version
  1. 如果版本大于当前 ZYGISK_API_VERSION ,拒绝。
  1. 保存模块 ABI 指针到 mod
  1. 按版本填充 API 函数指针。
v1 API:
  1. hookJniNativeMethods 指向全局实现。
  1. 旧版 pltHookRegister/Exclude/Commitg_ctx 的 regex 兼容层。
  1. connectCompanionsetOption 转到当前 ZygiskModule
v2 API:
  1. getModuleDir
  1. getFlags
v4 API:
  1. pltHookRegister(dev, inode, ...) 直接走 lsplt。
  1. exemptFd 转给 g_ctx->exempt_fd
##### 行 71-85:valid
模块必须满足:
  1. 已注册 ABI。
  1. API 版本在 1-5。
  1. impl 与四个 pre/post 回调指针都不为空。
否则模块会从 modules 列表删除。
##### 行 87-98:connectCompanion
向 magiskd 发 ConnectCompanion 请求。写入当前进程 ABI:
再写模块 id。返回的 fd 是连接到 companion handler 的 socket。
##### 行 100-106:getModuleDir
向 magiskd 发 GetModDir ,写模块 id,接收一个目录 fd。这个 fd 指向模块根目录。
##### 行 108-119:setOption
支持两个模块选项:
  1. FORCE_DENYLIST_UNMOUNT :设置 DO_REVERT_UNMOUNT
  1. DLCLOSE_MODULE_LIBRARY :post 后允许 dlclose 模块库。
##### 行 121-127:flags 与模块卸载
getFlags() 返回 info_flags 去掉内部私有位后的结果。
tryUnload() 如果模块设置 unload,就 dlclose(handle)
##### 行 131-160:调用不同 ABI 版本的模块回调
call_app 宏负责兼容旧 API:
  1. API v1/v2:构造 AppSpecializeArgs_v1 兼容对象。
  1. API v3/v4/v5:直接传 AppSpecializeArgs_v5 指针。
server 参数没有这种版本分流,直接调用。
##### 行 164-181:旧版 regex PLT hook 注册
v1/v2 模块用 regex 匹配库路径。Zygisk 保存:
  1. regex。
  1. symbol。
  1. callback。
  1. backup 指针。
exclude 规则也保存 regex 与 symbol。symbol 为空表示排除整个库。
##### 行 183-221:旧版 regex PLT hook 提交
plt_hook_process_regex() 扫描 maps:
  1. 只处理 offset 为 0、private、可读的映射。
  1. 对每条注册 regex 匹配路径。
  1. 再检查 exclude 规则。
  1. 未被排除就 lsplt::RegisterHook(map.dev, map.inode, symbol, callback, backup)
plt_hook_commit() 加锁、处理 regex、释放 regex 对象、清空列表,然后 lsplt::CommitHook()
##### 行 225-241:get\_module\_info
向 magiskd 发 GetInfo
  1. 写 uid。
  1. 写进程名。
  1. 写 ABI 位数。
  1. 读取 info_flags
  1. 如果 zygisk_should_load_module(info_flags) 为真,接收模块 fd 列表。
返回 fd 是因为 system\_server 流程后续还要把加载失败的模块 id 写回 magiskd。
##### 行 243-294:sanitize\_fds
先关闭 Zygisk logd fd。
父进程直接返回。子进程中:
  1. 如果支持 fds_to_ignore ,把模块豁免的 fd 合并到 Zygote 的忽略列表。
  1. 把已有 fds_to_ignore 中的 fd 标记为 allowed。
  1. 创建新 jintArray,把原 fd 与 exempted fd 合并。
  1. 扫描 /proc/self/fd
  1. 不在 allowed_fds 中且不是扫描目录 fd 的,全部关闭。
目的是避免从 Zygote 继承不该进入 app 的 fd,防止崩溃和泄漏。
##### 行 296-307:fd 豁免 API
exempt_fd(fd)
  1. 如果已经 post specialize,或者当前跳过关闭 log pipe,返回 true。
  1. 如果当前流程不能豁免 fd,返回 false。
  1. 否则加入 exempted_fds
can_exempt_fd() 要求当前是 APP_FORK_AND_SPECIALIZE ,且 app 参数有 fds_to_ignore
##### 行 309-346:fork\_pre/fork\_post
fork_pre() 是 Zygisk 避免第三方模块代码污染父 Zygote 的关键。
流程:
  1. block SIGCHLD
  1. 调用原始 old_fork()
  1. 父进程直接返回。
  1. 子进程扫描 /proc/self/fd ,记录当前允许保留的 fd。
  1. 目录 fd 标记为不允许。
  1. Zygisk logd fd 单独标记为不允许。
之后原始 Zygote 方法再调用 fork() 时,会被 new_fork() 返回缓存 pid,不会真的再 fork。
fork_post() unblock SIGCHLD
##### 行 348-386:run\_modules\_pre
加载并执行模块 pre 阶段。
第一轮遍历 fd:
  1. fd 必须是普通文件。
  1. 使用 android_dlopen_ext 从 fd 加载模块。
  1. 查找 zygisk_module_entry
  1. 成功则构造 ZygiskModule(i, h, e)
  1. system\_server 中 dlopen 失败会记录 warning,并把 fd 标成 -1,稍后上报失败模块。
第二轮:
  1. 调用 onLoad(env)
  1. 模块通过 API 表注册自身 ABI。
  1. valid() 通过则保留,否则删除。
第三轮:
  1. app 流程调用 preAppSpecialize(args.app)
  1. system\_server 流程调用 preServerSpecialize(args.server)
##### 行 388-398:run\_modules\_post
设置 POST_SPECIALIZE ,再对每个模块:
  1. app 调 postAppSpecialize
  1. system\_server 调 postServerSpecialize
  1. 根据模块选项 tryUnload()
##### 行 400-421:app specialize 共享 pre/post
app_specialize_pre()
  1. 设置 APP_SPECIALIZE
  1. 向 magiskd 查询进程 flags 和模块 fd。
  1. 如果进程在 enforced denylist,设置 DO_REVERT_UNMOUNT ,不加载模块。
  1. 否则加载并运行模块 pre。
app_specialize_post()
  1. 调模块 post。
  1. 如果当前进程是 Magisk app,设置环境变量 ZYGISK_ENABLED=1
  1. 释放 nice_name 的 UTF chars。
##### 行 423-445:system\_server pre/post
server_specialize_pre()
  1. 用 uid 1000 和进程名 system_server 查询 magiskd。
  1. 如果没有模块,写回 0。
  1. 如果有模块,加载并运行 pre。
  1. 收集加载失败的模块 id,写回 magiskd。magiskd 会在对应模块目录下创建 zygisk/unloaded 标记。
server_specialize_post()run_modules_post()
##### 行 449-460:nativeSpecializeAppProcess 包装流程
这是 Android Q 以后部分流程使用的“不 fork,只 specialize 当前进程”的路径。
pre:
  1. nice_name 获取进程名。
  1. 设置 SKIP_CLOSE_LOG_PIPE
  1. 调 app pre。
post:
  1. 调 app post。
##### 行 462-480:nativeForkSystemServer 包装流程
pre:
  1. 设置 SERVER_FORK_AND_SPECIALIZE
  1. 进程名固定为 system_server
  1. fork_pre()
  1. 子进程执行 server pre。
  1. 清理 fd。
post:
  1. 子进程执行 server post。
  1. 父子都执行 fork_post() 解锁 SIGCHLD。
##### 行 482-500:nativeForkAndSpecialize 包装流程
pre:
  1. nice_name 获取 app 进程名。
  1. 设置 APP_FORK_AND_SPECIALIZE
  1. 先真实 fork。
  1. 子进程执行 app pre。
  1. 清理 fd。
post:
  1. 子进程执行 app post。
  1. 父子都执行 fork_post()

#### 3.2.7 api.hpp 逐行/逐段解释
##### 行 1-21:版权、用途和警告
这是面向模块开发者的公开 API 头。源码提示不要直接修改,并建议模块开发使用发布版 sample 仓库中的头文件。
##### 行 24-27:JNI 与 API 版本
包含 jni.h ,定义当前 API 版本 ZYGISK_API_VERSION 5
##### 行 28-100:公开说明
注释解释了 Zygisk 的模型:
  1. Android app 进程从 Zygote fork。
  1. fork 后执行 specialize,把进程放入应用沙箱。
  1. Zygisk 允许模块在 app/system\_server specialize 前后运行代码。
  1. 模块代码运行在目标 app/system\_server 进程,不运行在长期的 Zygote daemon 中。
  1. 需要 root 能力时,使用 companion handler。
##### 行 102-141:zygisk::ModuleBase
模块作者继承此类,实现:
| 方法 | 调用时机 | | --- | --- | | onLoad(Api*, JNIEnv*) | 模块 so 被加载后立即调用 | | preAppSpecialize(AppSpecializeArgs*) | app specialize 前,仍接近 Zygote 权限 | | postAppSpecialize(const AppSpecializeArgs*) | app specialize 后,处于 app 沙箱 | | preServerSpecialize(ServerSpecializeArgs*) | system\_server specialize 前 | | postServerSpecialize(const ServerSpecializeArgs*) | system\_server specialize 后 |
##### 行 143-178:公开参数对象
AppSpecializeArgs 暴露 app specialize 参数。必选字段保证所有支持 Android 版本存在;可选字段要先判空。
ServerSpecializeArgs 暴露 system\_server 参数。
字段都是引用,模块可在 pre 阶段修改它们,影响后续原始 Zygote specialize 调用。
##### 行 185-200:模块选项
FORCE_DENYLIST_UNMOUNT :强制对当前进程执行 Magisk/模块文件 unmount。
DLCLOSE_MODULE_LIBRARY :post 后卸载模块库。注释明确警告:如果模块 hook 了进程函数,不应启用该选项,否则回调地址会变成悬空指针。
##### 行 202-209:公开状态 flags
PROCESS_GRANTED_ROOT 表示当前进程 uid 已被授权 root。
PROCESS_ON_DENYLIST 表示当前进程在 denylist 中。
内部 flag 如 denylist enforced、Magisk app 不直接暴露给模块。
##### 行 211-290:zygisk::Api
模块可用 API:
  1. connectCompanion() :连接 root companion 进程。
  1. getModuleDir() :获取模块根目录 fd。
  1. setOption() :设置模块选项。
  1. getFlags() :获取当前进程状态。
  1. exemptFd() :让指定 fd 不被 Zygote 自动关闭。
  1. hookJniNativeMethods() :替换某个 Java class 已注册 native 方法。
  1. pltHookRegister() :按 dev/inode/symbol 注册 PLT hook。
  1. pltHookCommit() :提交 PLT hook。
这些 API 在 post 后会失效,因为 Zygisk 会卸载并清空 API 表。
##### 行 292-310:注册宏
REGISTER_ZYGISK_MODULE(clazz) 导出 zygisk_module_entry ,Zygisk 加载模块后查找这个符号。
REGISTER_ZYGISK_COMPANION(func) 导出 zygisk_companion_entry ,zygiskd 加载模块 companion 后查找这个符号。
##### 行 317-360:内部 ABI 实现
internal::module_abi 是模块侧构造的 ABI 表,保存 API 版本、模块对象和四个回调 trampoline。
internal::api_table 是 Zygisk 传入模块的函数表,布局对应 module.hpp 中的 api_abi_v4
entry_impl<T>()
  1. 创建静态 Api
  1. 绑定传入 table。
  1. 创建静态模块对象 T module
  1. 创建静态 ABI。
  1. table->registerModule 注册。
  1. 成功后调用模块 onLoad(&api, env)
##### 行 364-387:Api inline 方法
这些方法只是薄封装:检查函数指针是否存在,再通过 tbl 调用 Zygisk 内部实现。
##### 行 391-398:默认导出声明
声明两个 C 符号:
模块可以只注册其中一个,也可以两个都注册。

#### 3.2.8 jni\_hooks.hpp 逐行/逐段解释
这是 gen_jni_hooks.py 生成的文件,不建议手写修改。它的职责是覆盖不同 Android 版本和厂商修改后的 Zygote native 方法签名。
##### 行 1-7:生成声明
声明 JniHookDefinitionsget_defs()get_defs()hook.cpp 中返回 g_hook ,因此生成代码可以访问保存旧函数指针的数组。
##### 行 9-230:fork\_app\_methods
数组包含 12 个 nativeForkAndSpecialize 签名:
  1. AOSP Android L/O/P/R/U/B 等版本变化。
  1. Samsung M/N/O/P 变体。
  1. Nubia U 变体。
每个元素结构相同:
  1. name 固定为 nativeForkAndSpecialize
  1. signature 是对应 Android/OEM 的 JNI 签名。
  1. fnPtr 是一个静态 lambda。
lambda 统一流程:
  1. 根据参数构造 AppSpecializeArgs_v5 args(...)
  1. 对当前签名存在的可选参数,把 args.xxx = &xxx
  1. 构造 ZygiskContext ctx(env, &args)
  1. ctx.nativeForkAndSpecialize_pre()
  1. 通过 get_defs()->fork_app_methods[index].fnPtr 调回原始 JNI native 函数。
  1. ctx.nativeForkAndSpecialize_post()
  1. 返回 ctx.pid
注意第 5 步中 fnPtr 在 hook 完成后已经被替换成原始函数指针,这是 HookContext::hook_jni_methods() 做的。
##### 行 232-361:specialize\_app\_methods
数组包含 7 个 nativeSpecializeAppProcess 签名。
这个路径返回 void ,因为它不 fork,只 specialize 当前进程。
lambda 流程与 fork app 类似,但调用:
##### 行 363-394:fork\_server\_methods
数组包含 2 个 nativeForkSystemServer 签名:
  1. AOSP 标准签名。
  1. Samsung Q 变体。
lambda 流程:
  1. 构造 ServerSpecializeArgs_v1
  1. 构造 ZygiskContext
  1. ctx.nativeForkSystemServer_pre()
  1. 调原始 nativeForkSystemServer
  1. ctx.nativeForkSystemServer_post()
  1. 返回 ctx.pid

#### 3.2.9 gen\_jni\_hooks.py 逐行/逐段解释
##### 行 3-19:JNI 类型建模
JType 保存 C++ 类型名和 JNI 签名字符。
JArray 根据元素类型生成数组类型:
  1. primitive 数组如 jintArray
  1. 非 primitive 数组统一为 jobjectArray
##### 行 21-38:参数建模
Argument 保存参数名、类型、是否要写入 AppSpecializeArgs 的可选字段。
Anon 表示 Zygisk 不关心但签名中必须保留的 OEM 参数,自动命名为 _0_1 等。
##### 行 40-69:JNI 方法建模
Return 保存返回值表达式和返回类型。
JNIMethod 能生成:
  1. C++ 参数列表。
  1. C++ 函数指针类型。
  1. lambda 签名。
  1. JNI signature 字符串。
##### 行 71-139:三类 hook 生成器
JNIHook 是抽象基类。
ForkApp 生成 nativeForkAndSpecialize 包装体:
  1. 初始化 args。
  1. 设置可选参数指针。
  1. 构造 ZygiskContext
  1. 调 pre。
  1. 调原始函数。
  1. 调 post。
  1. 返回 ctx.pid
SpecializeApp 改目标名为 nativeSpecializeAppProcess ,返回 void。
ForkServer 改目标名为 nativeForkSystemServer ,初始化 server args。
##### 行 140-181:公共参数定义
定义 AOSP 与 OEM 签名中会出现的参数对象,例如 uid/gid/gids/runtime\_flags、 fds_to_ignoreis_child_zygotemount_sysprop_overrides 、server capabilities 等。
set_arg=True 的参数会被写入 AppSpecializeArgs_v5 可选字段。
##### 行 183-612:具体签名列表
逐个定义不同 Android 版本/OEM 的方法签名:
  1. fas_* :fork app。
  1. spec_* :specialize app。
  1. server_* :fork system\_server。
Samsung、Nubia、XR 等变体通过 Anon 或额外参数保持 ABI 匹配,但 Zygisk 只提取自己关心的参数。
##### 行 615-631:生成数组代码
gen_jni_def(field, methods) 生成一个 std::array<JNINativeMethod, N>
每个元素包含:
  1. 方法名。
  1. JNI 签名。
  1. 包装 lambda。
##### 行 634-669:写出 jni\_hooks.hpp
脚本打开 jni_hooks.hpp 并写入:
  1. 文件头。
  1. 前置声明。
  1. JniHookDefinitions struct。
  1. 三组方法数组。

#### 3.2.10 daemon.rs 逐行/逐段解释
##### 行 1-17:依赖
引入模块根目录常量、MagiskD、FFI 枚举、resetprop、Unix socket 扩展、fd 工具、fork 工具、日志工具等。
##### 行 18-25:常量与加载判断
NBPROP 与 C++ 一致,是 native bridge 属性。
ZYGISKLDR 与 C++ 一致,是 libzygisk.so
UNMOUNT_MASK 表示 denylist enforced 且当前进程在 denylist。
zygisk_should_load_module(flags) 返回 true 的条件:
  1. 不是 enforced denylist 命中。
  1. 不是 Magisk app。
##### 行 27-59:exec\_zygiskd
启动 companion daemon。
关键点:
  1. 清除 remote socket 的 close-on-exec,让 fd 在 exec 后仍然有效。
  1. 64 位 magiskd 中,如果目标是 32 位 companion,执行 magisk32 ;否则执行 magisk
  1. 构造执行路径: get_magisk_tmp()/magiskmagisk32
  1. 执行参数:
这个参数最终进入 C++ zygisk_main()
##### 行 61-67:ZygiskState
字段:
| 字段 | 含义 | | --- | --- | | lib_name | 当前写入 native bridge 属性的值,用于恢复原属性 | | sockets | 32 位和 64 位 zygiskd 的连接 socket | | start_count | zygote crash 计数,用于回滚 |
##### 行 69-108:connect\_zygiskd
处理模块 connectCompanion() 请求。
流程:
  1. 从 client 读 ABI 位数。
  1. 选择 32 位或 64 位 zygiskd socket。
  1. 如果已有 socket,用 poll 检查是否仍有效。
  1. 有效则直接把 client fd 发给 zygiskd。
  1. 无效或不存在,则创建 socket pair。
  1. fork 子进程执行 exec_zygiskd
  1. 把当前 ABI 的所有模块 fd 发送给新 zygiskd。
  1. 等 zygiskd ack。
  1. 把当前 client fd 发给 zygiskd。
  1. 保存 local socket 供复用。
##### 行 110-127:reset
Zygisk 状态重置:
  1. restore 为 true 时重置 crash 计数并恢复属性。
  1. restore 为 false 时清空 companion sockets,增加 crash 计数。
  1. crash 超过 3 次,认为 zygote 崩溃过多,回滚 native bridge 属性。
##### 行 129-146:set\_prop
如果 lib_name 不为空,说明已设置过,直接返回。
否则读取原 native bridge 属性:
  1. 空或 "0"lib_name = "libzygisk.so"
  1. 非空: lib_name = "libzygisk.so" + 原值
写回 ro.dalvik.vm.native.bridge
如果 Huawei Maple 开启,则设置 ro.maple.enable=0 ,因为 Maple 特殊 Zygote 可能绕开 native bridge 创建 system\_server。
##### 行 148-155:restore\_prop
如果 lib_namelibzygisk.so 长,后半段就是原 native bridge;否则恢复为 "0"
写回属性并清空 lib_name
##### 行 158-176:MagiskD::zygisk\_handler
magiskd 收到 RequestCode::ZYGISK 后进入这里。
读取 ZygiskRequest ,分发:
  1. GetInfo :查询进程 flags 和模块 fd。
  1. ConnectCompanion :启动/复用 zygiskd,转发 client fd。
  1. GetModDir :发送模块目录 fd。
##### 行 178-189:get\_module\_fds
从模块列表取每个模块对应 ABI 的 zygisk so fd:
  1. 64 位取 m.z64
  1. 32 位取 m.z32
  1. 无效 fd 用 STDOUT_FILENO 占位,因为通过 socket 发送 fd 时必须是有效 fd;magiskd 中 stdout 始终是 /dev/null ,zygiskd/目标进程会把它视为无效模块。
##### 行 191-240:get\_process\_info
处理目标进程查询。
读取:
  1. uid。
  1. process 名。
  1. ABI 位数。
计算 flags:
  1. update_deny_flags 写入 denylist/enforced 信息。
  1. 如果 uid 是当前用户的 Magisk manager uid,设置 ProcessIsMagiskApp
  1. 如果 uid 已授权 root,设置 ProcessGrantedRoot
然后:
  1. 先写 flags 给客户端。
  1. 如果允许加载模块,发送模块 fd。
  1. 如果不是 system\_server,结束。
  1. 如果是 system\_server,继续读取 C++ 写回的 failed module ids。
  1. 对失败模块创建 zygisk/unloaded 标记文件。
##### 行 242-257:get\_mod\_dir
读取模块 id,查模块列表,打开模块根目录并把 fd 发送给客户端。
##### 行 260-265:zygisk\_enabled
FFI 辅助方法,返回 atomic zygisk_enabled 状态。

#### 3.2.11 mod.rs 逐行/逐段解释
##### 行 1-6:模块组织与导出
引入 daemon 子模块,并重新导出:
##### 行 8-28:exec\_companion\_entry
这是给 C++ entry.cpp 调用的 C ABI 函数。
参数:
  1. client :目标进程与 companion handler 通信的 socket fd。
  1. companion_handler :模块导出的 zygisk_companion_entry
执行流程:
  1. 把任务提交到 ThreadPool::exec_task
  1. 调用前用 fd_get_attr(client) 记录 fd 对应文件的 dev/inode。
  1. 执行模块 companion handler。
  1. handler 返回后再次检查 fd。
  1. 如果 fd 仍是同一个文件,关闭它。
这样做是为了避免模块 handler 自己关闭 fd 后,系统复用了相同 fd 数字,Zygisk 又误关了别的文件。

#### 3.2.12 关键时序图
##### 3.2.12.1 Zygote 启动注入
##### 3.2.12.2 app fork 流程
##### 3.2.12.3 system\_server fork 流程
##### 3.2.12.4 companion 流程

#### 3.2.13 几个容易误解的点
##### 3.2.13.1 模块不是加载在长期 Zygote daemon 中
公开 API 注释也强调:模块只在 fork 后的 app/system\_server 进程中加载。Zygisk 先 fork,再在子进程加载第三方模块,避免模块代码污染父 Zygote。
##### 3.2.13.2 isCompatibleWith() 返回 false 不代表注入失败
Zygisk 不是要成为真正 native bridge,它只是借这个入口完成 bootstrap。真正 native bridge 如存在,会在 post_native_bridge_load() 中补载。
##### 3.2.13.3 为什么要 hook fork
Zygisk 需要在第三方模块加载前先真实 fork,确保模块只进入子进程。但原始 Zygote native 函数内部仍会调用 fork。 new_fork() 返回缓存 pid,使原始逻辑继续按正常路径执行但不再创建新进程。
##### 3.2.13.4 为什么要枚举 JNI 方法再 RegisterNatives
不同 Android 版本和厂商修改了 Zygote native 方法签名。Zygisk 不能假设固定签名,所以生成大量候选签名,逐个 RegisterNatives 尝试,成功后再反查旧函数指针。
##### 3.2.13.5 为什么要自卸载
Zygisk 的职责是在 specialize 前后提供模块回调。完成后继续常驻 app 进程会增加检测面、内存占用和 hook 风险。因此子进程完成后恢复 hook 并 dlclose 自己。

#### 3.2.14 关键函数索引
| 函数/符号 | 文件 | 作用 | | --- | --- | --- | | NativeBridgeItf | entry.cpp | native bridge 注入入口 | | zygisk_main | entry.cpp | companion applet 入口 | | zygiskd | entry.cpp | root companion daemon 主循环 | | hook_entry | hook.cpp | 初始化 HookContext 并安装 bootstrap PLT hook | | HookContext::hook_plt | hook.cpp | hook dlclose/fork/unshare/strdup/... | | HookContext::post_native_bridge_load | hook.cpp | 找 callbacks 并补载真实 native bridge | | HookContext::hook_zygote_jni | hook.cpp | 替换 Zygote native specialize 方法 | | ZygiskContext::fork_pre | module.cpp | 提前 fork 并记录 fd | | ZygiskContext::run_modules_pre | module.cpp | dlopen 模块并调用 onLoad/pre | | ZygiskContext::run_modules_post | module.cpp | 调用 post 并按需卸载模块 | | ZygiskModule::RegisterModuleImpl | module.cpp | 模块 ABI 注册和 API 表填充 | | ZygiskState::set_prop | daemon.rs | 写入 native bridge 属性 | | MagiskD::get_process_info | daemon.rs | 返回进程 flags 和模块 fd | | ZygiskState::connect_zygiskd | daemon.rs | 启动/复用 companion daemon | | exec_companion_entry | mod.rs | 在线程池中执行 companion handler |

#### 3.2.15 用一句话概括
Zygisk 的核心是:magiskd 通过 native bridge 属性让 Zygote 加载 libzygisk.so ,Zygisk 在 native bridge 初始化窗口安装 PLT hook,再在 ZygoteInit 前替换 Zygote 的 JNI specialize 方法;真正创建 app/system\_server 时,它先 fork 到子进程,向 magiskd 获取模块 fd,加载模块执行 pre/post 回调,完成后恢复 hook 并从目标进程自卸载。
源码流程梳理清楚之后,就可以开始真正移植了。

四、开始移植:把最新版 Zygisk 迁到 r0zygisk

指令:
AI执行过程:
  1. 环境检测

4.1 Zygisk 迁移改动

#### 1\. 同步公开 API 到 v5
文件:
改动:
  • 使用 Magisk-master/native/src/core/zygisk/api.hpp 覆盖 r0zygisk 旧版公开 API 头文件。
  • ZYGISK_API_VERSION 从旧版本升级为:
意义:
  • 新编译的 Zygisk 模块会按 API v5 注册。
  • r0zygisk loader 需要能识别并加载 API v5 模块。
#### 2\. 内部参数结构升级为 AppSpecializeArgs\_v5
文件:
改动:
  • 增加 AppSpecializeArgs_v5
  • AppSpecializeArgs_v5 继承 AppSpecializeArgs_v3 ,并单独持有:
  • 增加 API v5 兼容别名:
  • call_app 增加 case 5 ,让 API v5 模块按新版参数传入。
意义:
  • 兼容 Magisk 最新 Zygisk API v5。
  • 保留对旧 API v1-v4 模块的兼容。
#### 3\. Hook 上下文改用 AppSpecializeArgs\_v5
文件:
改动:
  • ZygiskContext 中 app 参数指针从:
改为:
  • ZygiskModule::valid() 接受 API v5:
意义:
  • r0zygisk 的模块加载逻辑可以接受 v5 模块。
  • Zygote JNI wrapper 传入的是新版 specialize 参数对象。
#### 4\. 更新 JNI hook 生成器
文件:
改动:
  • wrapper 初始化参数从 AppSpecializeArgs_v3 改为 AppSpecializeArgs_v5
  • 增加 Magisk 最新 Zygisk 中已有的新参数:
  • 增加新的 Zygote JNI 签名候选:
  • 重新生成:
新增生成结果中包含:
意义:
  • nativeForkAndSpecialize_b 是 Android 16 / Baklava 相关适配的重要线索。
  • 新增的 OEM / XR 签名提高了对新系统和厂商 ROM 的兼容范围。

4.2 版本与输出文件名改动

#### 1\. 固定版本名和版本号
文件:
改动前:
改动后:
意义:
  • 项目从自己的第一版开始。
  • versionCode 固定为 1
#### 2\. 去掉 zip 文件名中的 commit hash
文件:
改动前:
改动后:
意义:
  • 不再因为目录不是 git 仓库而输出 unknown
  • 最终文件名符合要求。
#### 3\. 去掉 module.prop 版本显示中的 commit hash
文件:
改动前:
改动后:
生成结果:

4.3 构建验证

执行过的验证命令:
结果:
执行完整打包:
结果:
最终新产物:
项目已经开源,发布页在 GitHub Releases:

4.4 本次修改过的主要文件

4.5 当前阶段状态

当前源码已经完成:
  • Zygisk API v5 迁移。
  • Android 16 / Baklava 相关 JNI 签名候选加入。
  • r0zygisk 自有 loader / zygiskd / ptrace 架构保留。
  • 输出文件名改为 r0zygisk-v1.0.0-1-release.zip
  • 完整打包通过。
在 Android 14 的 SukiSU 环境刷入后,我又额外测试了 LSPosed 模块,确认加载正常。
到这里,移植主体已经基本完成,下一步开始补 WebUI,这部分同样结合 AI 一起推进。

五、WebUI 重构与联调记录

指令:
AI 给出的结论如下:
实机刷入后,确认 WebUI 虽然已经生成,但界面和信息组织都比较粗糙,这也是后面重做界面的直接原因。这里当时没有保留截图。 指令:

5.1 第一阶段:调研官方 ZygiskNext WebUI 功能

#### 目标
弄清楚官方 ZygiskNext WebUI 提供哪些功能,再决定哪些功能能在当前 r0zygisk fork 中真实接入。
#### 排查
本地先检查项目结构,确认 WebUI 在:
再查看控制脚本:
实际内容是通过:
调用 native 里的 ctl start|stop|exit
随后查看 loader/src/ptracer/main.cpp ,确认当前后端只支持:
换句话说,当前 fork 并没有暴露官方 ZygiskNext 中那些设置项对应的配置接口。
#### 官方 WebUI 功能参考
通过 GitHub 官方仓库的 i18n 分支文案确认,ZygiskNext WebUI 功能大致包括:
  • 状态页
  • 基本信息
  • 设置页
  • Root 实现显示
  • Zygote monitor 状态
  • Zygisk module 状态
  • ZN Module 状态
  • KernelSU / Magisk / APatch 排除列表策略说明
  • 日志写入 dmesg
  • 非 root 应用作为排除列表
  • 强制排除列表
  • 匿名内存加载
  • Zygisk Next linker
  • Zygote 注入状态说明
  • 模块问题提示
但是当前 r0zygisk 后端没有这些设置项的持久化配置或控制命令。因此不能做“看起来能点,实际没有接线”的假开关。
#### 第一版方案
把页面做成:
  • 状态仪表盘
  • 追踪器控制按钮
  • 已发现 Zygisk 模块列表
  • 每个按钮的说明
  • 原始诊断输出
  • ZygiskNext 参考功能地图,并标注当前 fork 是否接入

5.2 第二阶段:重做 WebUI

#### 修改文件
重写:
  • module/src/webroot/index.html
  • module/src/webroot/styles.css
  • module/src/webroot/app.js
#### 页面结构改造
新增的页面结构包括:
  • 顶部标题: Zygisk 控制台
  • 总状态评分: 0/33/3
  • 三个状态卡片:
  • Daemon
  • Zygote
  • Root
  • 三个操作按钮:
  • 启动追踪
  • 停止追踪
  • 退出监控
  • 已发现的 Zygisk 模块列表
  • 操作说明
  • 原始诊断信息
#### 前端状态读取逻辑
初版 WebUI 通过管理器 WebUI bridge 执行 shell:
然后从 module.propdescription= 中解析:
#### 验证
执行:
构建通过,产物为:

5.3 第三阶段:刷入后第一次反馈

#### 用户现象
刷入后界面看起来不错,但出现:
  • 当前状态显示追踪为运行
  • Daemon 为运行
  • 进程表未发现 zygisk
  • Zygote 未确认
  • Root 未识别
  • 无法读取 module.prop
  • 三个按钮点击没有任何反应
#### 初步判断
当时判断存在两个可能问题:
  1. 模块路径被硬编码为:
但实际刷入后模块目录可能在其他路径或更新目录中。
  1. WebUI 的 exec 调用方式可能不兼容当前管理器。
#### 模块路径修复
加入自动定位模块目录逻辑:
前端不再固定依赖 /data/adb/modules/r0zygisk ,而是在刷新和点击按钮时动态查找真实模块目录。
#### 第一版 exec 兼容修复
一开始用过:
并尝试根据返回值判断 Promise、同步结果或 callback。
重新打包后,问题还没有彻底解决。

5.4 第四阶段:删除参考块,并继续处理 exec 超时

#### 用户现象
实机反馈如下:
  • 不再显示 ZygiskNext 参考 这个块
  • 诊断信息显示:
  • 页面上显示:
  • 无法连接执行接口
  • Daemon、Zygote、Root 都等待刷新
  • 按钮仍然没有结果
#### 删除参考块
index.html 删除:
同时从 app.js 删除 officialFeaturesrenderFeatures() ,从 styles.css 删除对应的 .feature-grid.feature-item 样式。
#### exec 超时根因
继续查 KernelSU WebUI API 后确认:
KernelSU 的底层 bridge 不是:
而是:
关键差异是第三个参数传的是“全局回调函数名”,不是函数对象。
#### 修复方案
module/src/webroot/app.js 中实现:
同时保留降级:
最终执行顺序:
  1. bridge.exec(command, "{}", callbackName)
  1. bridge.exec(command, callbackName)
  1. bridge.exec(command)
#### 验证
执行:
并检查 zip:
确认 zip 中包含:

5.5 第五阶段:SukiSU 模块列表显示一长串 tracing 状态

#### 用户现象
实机反馈中,SukiSU 模块列表里出现了:
并询问这是什么意思。
#### 解释
这是 r0zygisk 原本把运行状态写进 module.propdescription= 中导致的。
loader/src/ptracer/monitor.cpp 原始逻辑中:
它会生成类似:
SukiSU 的模块列表直接显示 description ,所以用户会看到这串运行状态。
#### 含义说明
这些字段含义:
  • monitor: tracing :追踪器正在运行
  • zygote64: injected :64 位 zygote 已注入
  • daemon64: running :64 位 daemon 正在运行
  • Root: KernelSU :当前 root 实现识别为 KernelSU 系
  • module(3) :发现了 3 个 Zygisk 模块
这些状态本身说明模块运行正常,但并不适合直接显示在模块列表里。

5.6 第六阶段:迁移运行状态到 status.json

#### 调整目标
这里的目标已经明确:
即:
  • SukiSU 模块列表保持干净描述
  • 运行状态不要再塞进 module.prop description
  • 状态写入独立文件,如:
#### native 修改
修改文件:
主要变化:
  1. 删除 pre_sectionpost_section 的描述拼接逻辑。
  1. 不再生成临时 module.prop 并 bind mount 回模块目录。
  1. 新增:
  1. prepare_environment() 改为:
  1. updateStatus() 写 JSON:
  1. 状态文本里的表情也移除,改成纯文本:
#### WebUI 修改
修改:
刷新命令新增读取:
诊断信息增加:
前端逻辑改为:
  • 优先解析 status.json
  • 如果旧版本还在运行,没有 status.json ,则回退解析 module.prop description
#### 验证
执行:
并检查 native so:
确认包含:
并确认不再依赖 description= 状态拼接。
#### 用户刷入后反馈
再次刷入后,SukiSU 模块列表只显示:
解释为:
也就是 r0zygisk 不依赖 Magisk 自带 Zygisk,而是自己实现一套 Zygisk 注入和守护进程机制,让 KernelSU / SukiSU 环境也能加载 Zygisk 模块。
这说明把运行状态迁移到 status.json 的方案已经生效。

5.7 第七阶段:WebUI Zygote 红点,状态只有 2/3

#### 用户现象
后续实机反馈如下:
  • SukiSU 模块描述已经干净
  • 但 WebUI 中 ZYGOTE 红点
  • 显示“未确认”
  • 上方状态只有 2/3
  • 正常应该是 3/3
#### 根因
native 已经写出结构化 JSON:
但前端判断还主要依赖 raw 文本:
如果 raw 文本解析失败,或者和旧格式有细微差异,前端就会漏判 Zygote。
#### 修复
module/src/webroot/app.js 中把状态读取改成:
判断逻辑改成优先用 JSON 字段:
显示详情也改为优先显示 JSON:
#### 验证
执行:
并确认 zip 中包含:
输出包含:
最终 zip 时间:

六、最终状态

截至本记录生成时,主要文件状态如下。

WebUI

文件:
当前能力:
  • 显示总评分 0/33/3
  • 显示 monitor / daemon / zygote / root
  • status.json 读取结构化状态
  • 回退兼容旧 description 状态
  • 自动定位真实模块目录
  • 显示已安装 Zygisk 模块列表
  • 支持追踪器操作:
  • 启动追踪
  • 停止追踪
  • 退出监控
  • 诊断区显示:
  • module.dir
  • module.prop
  • status.json
  • processes
  • modules

Native Monitor

文件:
当前行为:
  • 不再把状态写入 module.prop description
  • 不再污染 SukiSU 模块列表
  • 状态写入:

模块描述

文件:
当前描述:
SukiSU 中显示这句话是正常现象,含义是:
补充过程中的一些截图和当前最终版本截图:
Android 15 测试:
notion image
Android 16 测试:
notion image
notion image
notion image
notion image
Android 14 SukiSU:
最后把模块加载修改为列表显示,不挤成一坨,当前最终版的 WebUI 样式:
notion image
测试后功能没有问题,但使用 Hunter 最新版 6.58 发现仍会被检测: !
以上为使用 ptrace 的方式注入。
这里尝试了多种修改方式,最终仍被检测到;期间还出现过一次卡开机,随后重刷并重建环境后继续分析。

七、第二版大修:通过 Hunter 的 Zygisk 检测

第一版基于 ptrace 的注入方案在功能上已经稳定,但用 Hunter 6.58 实测时仍会被检测,日志里出现:
这说明问题不在“是否能注入”,而在“注入行为特征是否被对方检测到”。所以第二版的核心工作不是继续堆规避技巧,而是先把 Hunter 的检测链路完整拆出来,再按检测点反推改造方向。

7.1 先定位检测触发点:检测发生在 Java 层还是 Native 层

使用 apktool 解包 com.zhenxi.hunter.apk ,全局搜索 Ptrace Checkfind zygisk detectedzygote root pid 等关键字。
结果很快明确:
  1. smali 层能看到 NativeEngine.checkZygisk() 的声明与调用。
  1. 关键日志模板不在业务 smali 里,而在 lib/arm64-v8a/libhunter.so
这一步很关键。因为如果误判成“Java 层静态字符串检测”,后续优化方向会完全跑偏。

7.2 从上层调用链定位触发入口

接下来把谁在触发 checkZygisk() 这件事补齐。沿着 smali 回溯后,能定位到两条主要路径:
  1. ZhenxiServerIpc 路径:最终进入 P1(6) ,再走到 NativeEngine.checkZygisk()
  1. ZhenxiServerTwinIpc 路径:最终进入 P1(8) ,同样会调用 NativeEngine.checkZygisk()
同时在 AndroidManifest.xml 中可以看到对应服务进程(包括 hunter_server_iso 与 twin 服务),和运行时日志中的进程标签可以对上。这意味着检测不是偶发调用,而是有明确的服务化触发链路。

7.3 检测核心:libhunter.so 的真实实现

因为目标 so 是 ELF aarch64 且有 strip,不能依赖常规符号名直接定位,所以采用了“字符串 + JNI 注册表 + 反汇编交叉”的方式:
  1. 在 so 里抽到关键字符串:
  • Ptrace Check Zygisk
  • find zygisk detected ,zygote root pid
  • /system/lib64/libzygisk_loader.so
  • /system/lib64/libzygisk.so
  • /sbin/.magisk/modules/zygisk_lsposed
  1. 定位 JNI_OnLoad 的动态注册逻辑,确认 checkZygisk 来自 RegisterNatives ,不是静态导出。
  1. 还原 JNINativeMethod 表后,锁定 checkZygisk 对应 native 函数入口: libhunter.so@0x2a93f8
到这一步,链路已经闭合:Java 触发点、JNI 绑定点、native 实现点三者一致。

7.4 检测逻辑拆解

checkZygisk 不是单一判断,而是“多分支命中后统一组装结果对象”的结构。结合反汇编可还原为:
  1. 先构造标题 Ptrace Check Zygisk
  1. 进行 pid/zygote 关系相关检查;命中后拼接 find zygisk detected ,zygote root pid ...
  1. 进行路径/特征串检查,涉及 libzygisk_loader.solibzygisk.sozygisk_lsposed 等标识。
  1. 把命中结果封装成返回对象( ListItemBean )上报到上层。
也就是说,Hunter 在这一块并非只看某个固定文件是否存在,而是把“进程关系 + 特征路径 + 运行态信息”做了组合判定。

八、结合 AI:使用新的注入方式完成第二版代码

这一阶段的目标很明确:不再继续打补丁式修修补补,而是参考 ZygiskNext 1.3.4 的成熟路线,重做第二版运行链路。整体工作分成两部分:先改注入方式,再完成工程与模块命名收敛。

8.1 第一部分:修改注入方式(从 ptrace 到 native bridge)

第一版是典型的 ptrace -> 远程 dlopen 路线:
  • 通过 r0z-trace32/64 常驻进程监控 zygote/app_process
  • 命中后注入 libr0z.so
  • 运行过程中会留下较明显的 ptrace 与进程关系特征
在第七部分中确认 Hunter 检测是“多维组合判定”后,这条路线风险较高,所以第二版切换为 native bridge loader -> payload -> zygisk core -> daemon
关键改动如下:
  1. 新增并接入: libzn_loader.solibpayload.solibzygisk.so
这一步不是简单多加几个库,而是把“入口加载”“功能实现”“Zygisk 主体”拆成三层。好处是桥接库只负责承接系统加载,payload 只负责承载运行逻辑,真正的 Zygisk 核心则保留自己的职责边界,后续替换和排查都更清楚。
  1. entry.cpp 增加 zygisk_entry ,构建产物从 libr0z.so + libr0z_ptrace.so 切换为三段式库输出。
原来的 ptrace 路线依赖主动盯进程,特征非常明显;新的 native bridge 路线则是让系统按属性把桥接库拉起来,再由入口去串起后续流程。 zygisk_entry 就是为了把这个入口补齐。
  1. 模块脚本不再依赖 r0z-trace ,改为 system.prop 设置 ro.dalvik.vm.native.bridge=libzn_loader.so ,并在 post-fs-data.sh/service.sh 启动 daemon。
这里的变化核心是把“前台监控”改成“系统加载”。 r0z-trace 这类常驻监控进程最容易留下 ptrace 痕迹,所以第二版直接放弃它,把启动触发点收敛到属性注入和系统阶段脚本上。
  1. 库安装路径统一为 $MODPATH/system/lib$MODPATH/system/lib64
这样做是为了按 32/64 位 ABI 分目录落盘,确保 native bridge 和后续 payload 在系统预期的库路径里可直接被加载,避免额外的路径适配和查找失败。
  1. daemon 状态链路改造为本地 status.json ,移除对旧 init_monitor socket 的硬依赖。
旧的 socket 更像临时联调通道,不适合作为 WebUI 的稳定数据源。改成 status.json 后,状态就变成了可直接读取的结构化文件,WebUI、模块列表和调试输出可以各自独立展示,不再互相绑死。
这次改造是架构级切换,不是启动命令替换,实际同时调整了注入入口、动态库产物、安装路径、启动时序与状态通道。

8.2 第二部分:统一名称为 r0z

完成注入路线切换后,第二版继续做命名统一,避免构建、日志、脚本、WebUI 之间出现旧名残留。
指令:
执行过程:
执行结果:项目完成改名后刷入测试,仍被检测;原因是检测侧仍能发现 libzygisk.so
接下来继续让 AI 进行修改:
指令:
执行结果: !
指令:
执行结果: !
notion image
notion image
刷入手机结果: !
WebUI: !
安装和打开lsposed模块也正常: !
最新版 Hunter 检测结果全绿: !
ruru结果: !
至此,第二版大修结束,已通过新版 Hunter 的 Zygisk 检测。

九、总结

这次做下来,最大的收获其实不只是把模块跑起来了,而是把一条完整的开发链路真正走通了:先确认最新版 MagiskZygisk 已经适配 Android 16 ,再借助 code-panorama 和 AI 快速梳理源码结构,后面一步步补齐 API、JNI 签名、打包链路、 WebUI 、守护进程状态和注入方式,最后再去处理真实设备上的兼容问题和检测问题。一路做下来,才把现在这个带 WebUI 、功能上等效于 Zygiskr0z 模块完整落地。
完整源码和发布包已经开源在 GitHub Releases:
这篇文章记录的是一个完整模块从源码阅读、迁移实现、联调修复,到最后对抗检测的过程。源码规模够大、调用链够长的时候,AI 能帮人省掉很多纯体力活,但真正决定结果的,还是自己去拆执行流程、判断注入链路、定位检测点、解决刷机和兼容性问题。至少对这次来说,目标已经完成了:模块做出来了, WebUI 跑起来了, Android 16 适配了,第二版也通过了新版 Hunter 的检测。
不过目前已经有朋友反馈,还是存在被检测到的情况。后续还会继续修改和完善,项目也会持续更新,新的进展我会继续补到后面的文章里。谢谢大家关注。
上一篇
爬虫数据采集加密算法全景指南
下一篇
【猿人学转载】Akamai对抗的隐秘战线——TLS指纹

Comments
Loading...