首页
社区
课程
招聘
Vibe Coding从源码阅读到模块开发:基于最新版 Magisk Zygisk,移植实现一个带 WebUI、支持 Android 16 的 Zygisk 等效模块r0zygisk(一)
发表于: 2026-5-12 21:57 14999

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

2026-5-12 21:57
14999

这篇文章主要记录了基于最新版 MagiskZygisk,整理并移植到 r0zygisk 的完整过程,同时把 KernelSU / SukiSU 环境下的 WebUI 一并补齐。

完整源码和发布包已经开源在 GitHub Releases:

r0zygisk Releases

整个过程可以归纳成八条主线:

关于 MagiskZygiskNext 老版本源码的阅读,这次我用到了一个开源库:

093K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6^5N6h3q4F1P5i4g2S2L8Y4A6Z5K9h3k6W2L8X3N6Q4x3V1k6U0L8$3c8W2i4K6u0V1M7r3q4F1L8%4u0S2L8h3p5`.

这个工具对源码阅读和结构定位帮助很大。

文章整体也使用了 AI 辅助完成,具体环境是:codex cli + gpt5.4 模型。

结果测试覆盖了 Android 14 到 Android 16 机型。

项目初始目录结构如下:

r0zygisk 是参考 ZygiskNext 的内容继续做的,使用 Android Studio 编译,开发环境如下。

Android Gradle Plugin 和 Gradle Wrapper 版本:

JDK 配置在 gradle.properties 中:

项目打包过程中,前期遇到的问题主要有这些:

开发环境准备好之后,就开始进入正式工作:基于最新版 MagiskZygisk,做一个支持 Android 16 的 r0zygisk

动手移植之前,先确认当前 Magisk-master 这版到底有没有完成 Android 16 适配。

我先让 AI 帮我判断这版 Magisk-master 的适配情况,提问如下:

得到的分析结果如下:

通过对 Magisk 仓库的查看和 AI 分析结果,可以确认这个版本已经适配 Android 16,那么后续就可以直接基于这版 Zygisk 来做迁移。

在开始改代码之前,先把最新版 Magisk Zygisk 的执行流程和关键入口梳理清楚。

阅读 Magisk 源码时,我先把前面提到的开源库本地安装并运行起来:

点一下生成全景图,就开始分析了:

生成结果如下:

借助这个视图,可以快速定位主要入口。

由于我主要关注的是 zygisk,因此后续单独针对这部分代码继续下钻:

使用这个库后,项目入口和主要结构会清晰很多。具体使用过程这里就不展开了。

在这个基础上,我再结合 codex 继续分析代码。

/Magisk-master/native/src/core/zygisk 目录下,打开 codex,给它下指令:

等待 AI 执行:

结果:

本文基于当前目录 /native/src/core/zygisk 中的源码编写,目标是把 Zygisk 的“如何被加载、如何接管 Zygote、如何加载模块、如何与 magiskd/zygiskd 协作、最后如何清理卸载”串成一条完整链路,并按文件解释代码。

目录内文件职责:

Zygisk 不是通过修改 Zygote 可执行文件直接进入的,而是借用了 Android native bridge 加载机制。

Rust 侧 ZygiskState::set_prop() 修改属性:

写入策略:

因此 Zygote 启动 native bridge 时会优先加载 libzygisk.so。如果系统原本就配置了 native bridge,Zygisk 后续会从属性字符串里剥出原值并重新加载真实 native bridge,避免破坏原系统行为。

Android 的 LoadNativeBridge 会查找动态库中的导出符号 NativeBridgeItf。Zygisk 在 entry.cpp 中导出同名符号:

isCompatibleWith() 被调用时,Zygisk 已经进入 Zygote 进程。这里执行:

hook_entry() 创建全局 HookContext,调用 HookContext::hook_plt()

第一批 PLT hook 挂在两个库上:

AndroidRuntime 启动 Java 层 ZygoteInit 前会处理类名字符串。strdup("com.android.internal.os.ZygoteInit") 触发 Zygisk 的 new_strdup(),调用 hook_zygote_jni()

hook_zygote_jni() 做三件事:

替换后的函数来自 jni_hooks.hpp。它们都是包装器:先构造 ZygiskContext,执行 pre 回调,再调用原始 JNI 函数,最后执行 post 回调。

当 Zygote 要创建 app 或 system_server 时,流程进入 Zygisk 包装器:

模块如果需要 root 权限,可以调用 Api::connectCompanion()。这不会让 app 进程变成 root,而是:

子进程 specialize 完成后,Zygisk 不希望一直留在 app/system_server 进程中。析构 ZygiskContext 时:

后续 JVM 创建线程时会调用 pthread_attr_destroy。Zygisk 在这个安全时机:

#pragma once 防止头文件重复包含。jni.h 提供 JNI 类型,core.hpp 提供 Magisk core 内部工具、日志、IPC 等声明。

ZYGISKLDR 是 Zygisk loader 库名:libzygisk.so

NBPROP 是 native bridge 属性名:ro.dalvik.vm.native.bridge。Rust 与 C++ 两边都使用同一语义。

64 位编译时日志前缀为 zygisk64,32 位为 zygisk32。这样同一设备有双 Zygote 时,logcat 中可以区分是哪一套代码在运行。

ZLOGV 默认被定义成空操作。取消注释可把 verbose 日志转成 debug 日志,但正常构建不启用,避免刷屏和性能影响。

hook_entry() 是 native bridge 回调进入 bootstrap 的入口。

hookJniNativeMethods() 是 Zygisk API 暴露给模块的 JNI hook 实现入口,内部会转给当前全局 HookContext

这两个结构是 Android native bridge 头文件中的 ABI 子集:

NativeBridgeRuntimeCallbacks 提供三类能力:

Zygisk 依赖它枚举并替换 Zygote native 方法。

NativeBridgeCallbacks 是 native bridge 动态库导出的 NativeBridgeItf 结构。Zygisk 只填了版本、padding、isCompatibleWith

android/dlext.h 用于从 fd 加载 so。dlfcn.h 用于 dlsympoll.h 用于 companion daemon 等待请求。

comp_entry 是模块 companion 函数类型:void (*)(int)

exec_companion_entry 是 Rust 侧 mod.rs 导出的 C ABI 函数。C++ 不直接创建线程,而是把 companion 请求交给 Rust 线程池执行。

zygiskd(int socket) 是 root companion daemon 主循环。开头要求:

不满足直接退出。

64 位进程命名为 zygiskd64,32 位为 zygiskd32。这和 magiskd 中按 ABI 维护两个 socket 对应。

从 magiskd 传来的 socket 中接收一组模块 fd。每个 fd:

"/jit-cache" 只是提供给 linker 的伪路径,真实库来自 fd。

write_int(socket, 0) 告诉 magiskd:zygiskd 已经加载模块 companion 表,可以开始转发请求。

poll 阻塞等待 magiskd 发来 client fd。每次请求:

这里的 companion entry 不是直接同步执行,而是进入 Rust 线程池,允许并发处理。

这是 magisk applet 入口,只有内部执行 magisk zygisk companion <fd> 时使用。

参数匹配 argc == 3 && argv[1] == "companion" 时进入 zygiskd(parse_int(argv[2]))

导出符号 NativeBridgeItf,让 Android native bridge loader 能找到。

isCompatibleWith lambda 是 Zygisk 第一次执行自己代码的关键点:

返回 false 是有意的:Zygisk 要借 native bridge 入口执行注入,而不是最终承担 native bridge 功能。

sys/mman.hsys/resource.hdlfcn.hunwind.h 支撑内存映射扫描、fd 上限、动态符号、栈回溯。

lsplt.hpp 是 PLT hook 库。Zygisk bootstrap 和模块 API 的 PLT hook 都依赖它。

module.hpp 提供 ZygiskContextjni_hooks.hpp 提供生成好的 JNI 包装函数列表。

这段注释是官方写在源码里的关键说明。它表达的核心是:

kZygoteInit 是 Java 层入口类全名。

kZygote 是 JNI 查找类名格式:com/android/internal/os/Zygote

kForkAppkSpecializeAppkForkServer 是要替换的 native 方法名。

JNIMethodsstd::span<JNINativeMethod>,方便对生成数组做统一处理。

JNIMethodsDyn 是动态分配的 JNI 方法表和数量。

HookContext 继承 JniHookDefinitions,因此直接持有三组生成的 JNI hook 数组:

字段含义:

方法分两类:

g_ctx 指向当前 specialize 流程中的 ZygiskContext。它通常指向栈对象,只在包装 JNI 函数执行期间有效。

g_hook 指向长期存在的 HookContext,从 native bridge 加载后一直存在到自卸载前。

get_defs() 返回 g_hook,供 jni_hooks.hpp 生成的包装函数获取原始 native 函数指针。

new_strdup(const char *str) 检查字符串是否等于 com.android.internal.os.ZygoteInit

如果匹配,调用 g_hook->hook_zygote_jni()

随后调用原始 strdup。这个 hook 的意义是抓住 AndroidRuntime 即将启动 ZygoteInit 的窗口,此时 JVM 已创建,Zygote native 方法也已经可枚举和替换。

new_fork() 的逻辑:

Zygisk 在 fork_pre() 中会提前执行真正 fork。随后原始 Zygote native 函数内部再调用 fork() 时,会被这个 hook 拦截并返回同一个 pid,避免 fork 两次。

Zygote specialize 过程中可能调用 unshare(CLONE_NEWNS) 创建私有 mount namespace。

如果当前在 Zygisk specialize 流程中,且创建 mount namespace 成功,并且 flags 中有 DO_REVERT_UNMOUNT,则调用 revert_unmount()。这用于 denylist 或模块强制隐藏时,从目标进程 mount namespace 中卸载 Magisk/模块痕迹。

secontext 改变后,进程权限和 SELinux 限制会发生变化。这里在切换前调用 zygisk_get_logd(),预先拿到 logd fd,避免后续上下文下无法访问或日志管道异常。

Zygote fork/specialize 时会关闭日志 fd。Zygisk 根据 SKIP_CLOSE_LOG_PIPE 控制是否关闭自己的 log 管道,然后调用原函数。

这是 native bridge bootstrap 的关键:

post_native_bridge_load() 会用这个 handle 记录 Zygisk 自身,并通过栈回溯找到真实 LoadNativeBridge 和 runtime callbacks。

Zygisk 不能直接在自己的函数栈上调用 dlclose(self_handle),否则代码被 unmap 后返回地址可能落在已卸载内存里。

解决方式:

这样不会返回到 Zygisk 的已卸载代码。

get_fd_max() 读取 RLIMIT_NOFILE,默认准备 32768。

ZygiskContext 构造函数保存 JNIEnv、specialize 参数、初始化 pid/flags/fd 位图和 mutex,并设置全局 g_ctx = this

析构时先把 g_ctx 清空,因为它指向栈对象。

如果不是子进程,直接返回。父 zygote 不能卸载 Zygisk,否则后续 fork 无法继续被 hook。

子进程中:

封装 _Unwind_GetRegionStart。ARM32 下如果 PC 处于 Thumb 模式,要把最低位置 1,保证函数地址匹配真实调用地址。

目标是从栈回溯上下文中找 NativeBridgeRuntimeCallbacks *

步骤:

这是 Zygisk 能枚举已注册 JNI 方法的基础。

dlclose hook 触发。

主要流程:

register_hook() 包装 lsplt::RegisterHook,成功后把 (dev, inode, symbol, old_func) 存入 plt_backup

hook_plt() 扫描 maps 找:

然后注册:

最后 lsplt::CommitHook() 执行实际 patch,并删除没有成功备份 old_func 的记录。

hook_unloader() 扫描 maps 找 libart.so,注册 pthread_attr_destroy hook。这一步发生在子进程 specialize 完成后,而不是最初 bootstrap 时。

遍历 plt_backup,把每个 symbol 替回 old_func。若任何一步失败,设置 should_unmap = false,避免在 hook 状态不确定时卸载自己。

get_jni_methods() 通过 runtime callbacks:

register_jni_methods() 逐个调用 RegisterNatives,允许失败。失败时清 exception,并把该方法的 fnPtr 置空。

这是替换 JNI native 方法的核心。

流程:

注意源码中特别强调:runtime callbacks 返回的 signature 不是标准格式,所以不能只靠字符串提前判断,而是要直接 RegisterNatives 试。

对模块 API 提供的入口。若 callbacks/env/class 无效,则把所有 fnPtr 清空。否则 FindClass 后调用上面的核心替换函数。

这是“接管 Zygote specialize”的关键函数。

步骤:

fork_app_methodsspecialize_app_methodsfork_server_methods 中保存的旧函数指针重新注册回 Zygote 类。

hook_entry() 创建 g_hook 并安装早期 PLT hook。

hookJniNativeMethods() 是公开 API 的 C++ 实现,转给 g_hook->hook_jni_methods

包含 regex、list 和公开 API 头 api.hpp。模块 API 兼容层需要正则保存 PLT hook 规则。

Zygisk 支持多个 API/ABI 版本。这里用别名表达版本兼容关系:

这表示某些版本只扩展语义或参数对象,不改变模块 ABI 函数表布局。

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

旧 API v1/v2 没有 rlimits,构造函数从 v5 转换到 v1 视图。模块如果声明旧 API,Zygisk 会创建这个兼容对象再调用模块回调。

ServerSpecializeArgs_v1 保存 system_server 参数引用:

模块可在 pre 阶段修改这些值。

module_abi_v1 是模块注册给 Zygisk 的 ABI:

static_assert 保证内部 flag 与公开 API flag 值一致。

UNMOUNT_MASK 表示“进程在 denylist 且 denylist enforced”。

PRIVATE_MASK 是不应该暴露给模块的内部 flag,包括 enforced 状态和 Magisk app 标记。

api_abi_base 每个版本都有:

v1 提供:

v2 增加:

v4 改造 PLT hook API,从 regex 匹配改为 dev/inode 精确定位,并增加:

ApiTable union 让同一块内存按不同 ABI 版本解释。

ZygiskModule 表示一个已加载到目标进程的 Zygisk 模块。

重要字段:

重要方法:

g_ctx 在 hook.cpp 定义,供模块 API 访问当前 specialize 上下文。

old_fork 是 PLT hook 保存的原始 fork。

内部 flags:

ZygiskContext 是一次 specialize 流程的上下文,生命周期通常是 JNI 包装函数的栈帧。

字段:

方法按功能分组:

连接 magiskd 的 RequestCode::ZYGISK,写入具体 ZygiskRequest 子命令,然后返回 socket fd。

后续 GetInfoConnectCompanionGetModDir 都从这里发起。

保存 id、handle、entry。清零 API 表,设置:

模块入口拿到 API 表后,会调用 registerModule 把模块 ABI 回填给 Zygisk。

模块注册入口。

流程:

v1 API:

v2 API:

v4 API:

模块必须满足:

否则模块会从 modules 列表删除。

向 magiskd 发 ConnectCompanion 请求。写入当前进程 ABI:

再写模块 id。返回的 fd 是连接到 companion handler 的 socket。

向 magiskd 发 GetModDir,写模块 id,接收一个目录 fd。这个 fd 指向模块根目录。

支持两个模块选项:

getFlags() 返回 info_flags 去掉内部私有位后的结果。

tryUnload() 如果模块设置 unload,就 dlclose(handle)

call_app 宏负责兼容旧 API:

server 参数没有这种版本分流,直接调用。

v1/v2 模块用 regex 匹配库路径。Zygisk 保存:

exclude 规则也保存 regex 与 symbol。symbol 为空表示排除整个库。

plt_hook_process_regex() 扫描 maps:

plt_hook_commit() 加锁、处理 regex、释放 regex 对象、清空列表,然后 lsplt::CommitHook()

向 magiskd 发 GetInfo

返回 fd 是因为 system_server 流程后续还要把加载失败的模块 id 写回 magiskd。

先关闭 Zygisk logd fd。

父进程直接返回。子进程中:

目的是避免从 Zygote 继承不该进入 app 的 fd,防止崩溃和泄漏。

exempt_fd(fd)

can_exempt_fd() 要求当前是 APP_FORK_AND_SPECIALIZE,且 app 参数有 fds_to_ignore

fork_pre() 是 Zygisk 避免第三方模块代码污染父 Zygote 的关键。

流程:

之后原始 Zygote 方法再调用 fork() 时,会被 new_fork() 返回缓存 pid,不会真的再 fork。

fork_post() unblock SIGCHLD

加载并执行模块 pre 阶段。

第一轮遍历 fd:

第二轮:

第三轮:

设置 POST_SPECIALIZE,再对每个模块:

app_specialize_pre()

app_specialize_post()

server_specialize_pre()

server_specialize_post()run_modules_post()

这是 Android Q 以后部分流程使用的“不 fork,只 specialize 当前进程”的路径。

pre:

post:

pre:

post:

pre:

post:

这是面向模块开发者的公开 API 头。源码提示不要直接修改,并建议模块开发使用发布版 sample 仓库中的头文件。

包含 jni.h,定义当前 API 版本 ZYGISK_API_VERSION 5

注释解释了 Zygisk 的模型:

模块作者继承此类,实现:

AppSpecializeArgs 暴露 app specialize 参数。必选字段保证所有支持 Android 版本存在;可选字段要先判空。

ServerSpecializeArgs 暴露 system_server 参数。

字段都是引用,模块可在 pre 阶段修改它们,影响后续原始 Zygote specialize 调用。

FORCE_DENYLIST_UNMOUNT:强制对当前进程执行 Magisk/模块文件 unmount。

DLCLOSE_MODULE_LIBRARY:post 后卸载模块库。注释明确警告:如果模块 hook 了进程函数,不应启用该选项,否则回调地址会变成悬空指针。

PROCESS_GRANTED_ROOT 表示当前进程 uid 已被授权 root。

PROCESS_ON_DENYLIST 表示当前进程在 denylist 中。

内部 flag 如 denylist enforced、Magisk app 不直接暴露给模块。

模块可用 API:

这些 API 在 post 后会失效,因为 Zygisk 会卸载并清空 API 表。

REGISTER_ZYGISK_MODULE(clazz) 导出 zygisk_module_entry,Zygisk 加载模块后查找这个符号。

REGISTER_ZYGISK_COMPANION(func) 导出 zygisk_companion_entry,zygiskd 加载模块 companion 后查找这个符号。

internal::module_abi 是模块侧构造的 ABI 表,保存 API 版本、模块对象和四个回调 trampoline。

internal::api_table 是 Zygisk 传入模块的函数表,布局对应 module.hpp 中的 api_abi_v4

entry_impl<T>()

这些方法只是薄封装:检查函数指针是否存在,再通过 tbl 调用 Zygisk 内部实现。

声明两个 C 符号:

模块可以只注册其中一个,也可以两个都注册。

这是 gen_jni_hooks.py 生成的文件,不建议手写修改。它的职责是覆盖不同 Android 版本和厂商修改后的 Zygote native 方法签名。

声明 JniHookDefinitionsget_defs()get_defs()hook.cpp 中返回 g_hook,因此生成代码可以访问保存旧函数指针的数组。

数组包含 12 个 nativeForkAndSpecialize 签名:

每个元素结构相同:

lambda 统一流程:

注意第 5 步中 fnPtr 在 hook 完成后已经被替换成原始函数指针,这是 HookContext::hook_jni_methods() 做的。

数组包含 7 个 nativeSpecializeAppProcess 签名。

这个路径返回 void,因为它不 fork,只 specialize 当前进程。

lambda 流程与 fork app 类似,但调用:

数组包含 2 个 nativeForkSystemServer 签名:

lambda 流程:

JType 保存 C++ 类型名和 JNI 签名字符。

JArray 根据元素类型生成数组类型:

Argument 保存参数名、类型、是否要写入 AppSpecializeArgs 的可选字段。

Anon 表示 Zygisk 不关心但签名中必须保留的 OEM 参数,自动命名为 _0_1 等。

Return 保存返回值表达式和返回类型。

JNIMethod 能生成:

JNIHook 是抽象基类。

ForkApp 生成 nativeForkAndSpecialize 包装体:

SpecializeApp 改目标名为 nativeSpecializeAppProcess,返回 void。

ForkServer 改目标名为 nativeForkSystemServer,初始化 server args。

定义 AOSP 与 OEM 签名中会出现的参数对象,例如 uid/gid/gids/runtime_flags、fds_to_ignoreis_child_zygotemount_sysprop_overrides、server capabilities 等。

set_arg=True 的参数会被写入 AppSpecializeArgs_v5 可选字段。

逐个定义不同 Android 版本/OEM 的方法签名:

Samsung、Nubia、XR 等变体通过 Anon 或额外参数保持 ABI 匹配,但 Zygisk 只提取自己关心的参数。

gen_jni_def(field, methods) 生成一个 std::array<JNINativeMethod, N>

每个元素包含:

脚本打开 jni_hooks.hpp 并写入:

引入模块根目录常量、MagiskD、FFI 枚举、resetprop、Unix socket 扩展、fd 工具、fork 工具、日志工具等。

NBPROP 与 C++ 一致,是 native bridge 属性。

ZYGISKLDR 与 C++ 一致,是 libzygisk.so

UNMOUNT_MASK 表示 denylist enforced 且当前进程在 denylist。

zygisk_should_load_module(flags) 返回 true 的条件:

启动 companion daemon。

关键点:

这个参数最终进入 C++ zygisk_main()

字段:

处理模块 connectCompanion() 请求。

流程:

Zygisk 状态重置:

如果 lib_name 不为空,说明已设置过,直接返回。

否则读取原 native bridge 属性:

写回 ro.dalvik.vm.native.bridge

如果 Huawei Maple 开启,则设置 ro.maple.enable=0,因为 Maple 特殊 Zygote 可能绕开 native bridge 创建 system_server。

如果 lib_namelibzygisk.so 长,后半段就是原 native bridge;否则恢复为 "0"

写回属性并清空 lib_name

magiskd 收到 RequestCode::ZYGISK 后进入这里。

读取 ZygiskRequest,分发:

从模块列表取每个模块对应 ABI 的 zygisk so fd:

处理目标进程查询。

读取:

计算 flags:

然后:

读取模块 id,查模块列表,打开模块根目录并把 fd 发送给客户端。

FFI 辅助方法,返回 atomic zygisk_enabled 状态。

引入 daemon 子模块,并重新导出:

这是给 C++ entry.cpp 调用的 C ABI 函数。

参数:

执行流程:

这样做是为了避免模块 handler 自己关闭 fd 后,系统复用了相同 fd 数字,Zygisk 又误关了别的文件。

公开 API 注释也强调:模块只在 fork 后的 app/system_server 进程中加载。Zygisk 先 fork,再在子进程加载第三方模块,避免模块代码污染父 Zygote。

Zygisk 不是要成为真正 native bridge,它只是借这个入口完成 bootstrap。真正 native bridge 如存在,会在 post_native_bridge_load() 中补载。

Zygisk 需要在第三方模块加载前先真实 fork,确保模块只进入子进程。但原始 Zygote native 函数内部仍会调用 fork。new_fork() 返回缓存 pid,使原始逻辑继续按正常路径执行但不再创建新进程。

不同 Android 版本和厂商修改了 Zygote native 方法签名。Zygisk 不能假设固定签名,所以生成大量候选签名,逐个 RegisterNatives 尝试,成功后再反查旧函数指针。

Zygisk 的职责是在 specialize 前后提供模块回调。完成后继续常驻 app 进程会增加检测面、内存占用和 hook 风险。因此子进程完成后恢复 hook 并 dlclose 自己。

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 并从目标进程自卸载。

源码流程梳理清楚之后,就可以开始真正移植了。

指令:

AI执行过程:

文件:

改动:

意义:

文件:

改动:

意义:

文件:

改动:

改为:

意义:

文件:

改动:

新增生成结果中包含:

意义:

文件:

改动前:

改动后:

意义:

文件:

改动前:

改动后:

意义:

文件:

改动前:

改动后:

生成结果:

执行过的验证命令:

结果:

执行完整打包:

结果:

最终新产物:

项目已经开源,发布页在 GitHub Releases:

r0zygisk Releases

当前源码已经完成:

在 Android 14 的 SukiSU 环境刷入后,我又额外测试了 LSPosed 模块,确认加载正常。

到这里,移植主体已经基本完成,下一步开始补 WebUI,这部分同样结合 AI 一起推进。

指令:

AI 给出的结论如下:

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

弄清楚官方 ZygiskNext WebUI 提供哪些功能,再决定哪些功能能在当前 r0zygisk fork 中真实接入。

本地先检查项目结构,确认 WebUI 在:

再查看控制脚本:

实际内容是通过:

调用 native 里的 ctl start|stop|exit

随后查看 loader/src/ptracer/main.cpp,确认当前后端只支持:

换句话说,当前 fork 并没有暴露官方 ZygiskNext 中那些设置项对应的配置接口。

通过 GitHub 官方仓库的 i18n 分支文案确认,ZygiskNext WebUI 功能大致包括:

但是当前 r0zygisk 后端没有这些设置项的持久化配置或控制命令。因此不能做“看起来能点,实际没有接线”的假开关。

把页面做成:

重写:

新增的页面结构包括:

初版 WebUI 通过管理器 WebUI bridge 执行 shell:

然后从 module.propdescription= 中解析:

执行:

构建通过,产物为:

刷入后界面看起来不错,但出现:

当时判断存在两个可能问题:

但实际刷入后模块目录可能在其他路径或更新目录中。

加入自动定位模块目录逻辑:

前端不再固定依赖 /data/adb/modules/r0zygisk,而是在刷新和点击按钮时动态查找真实模块目录。

一开始用过:

并尝试根据返回值判断 Promise、同步结果或 callback。

重新打包后,问题还没有彻底解决。

实机反馈如下:

index.html 删除:

同时从 app.js 删除 officialFeaturesrenderFeatures(),从 styles.css 删除对应的 .feature-grid.feature-item 样式。

继续查 KernelSU WebUI API 后确认:

KernelSU 的底层 bridge 不是:

而是:

关键差异是第三个参数传的是“全局回调函数名”,不是函数对象。

module/src/webroot/app.js 中实现:

同时保留降级:

最终执行顺序:

执行:

并检查 zip:

确认 zip 中包含:

实机反馈中,SukiSU 模块列表里出现了:

并询问这是什么意思。

这是 r0zygisk 原本把运行状态写进 module.propdescription= 中导致的。

loader/src/ptracer/monitor.cpp 原始逻辑中:

它会生成类似:

SukiSU 的模块列表直接显示 description,所以用户会看到这串运行状态。

这些字段含义:

这些状态本身说明模块运行正常,但并不适合直接显示在模块列表里。

这里的目标已经明确:

即:

修改文件:

主要变化:

修改:

刷新命令新增读取:

诊断信息增加:

前端逻辑改为:

执行:

并检查 native so:

确认包含:

并确认不再依赖 description= 状态拼接。

再次刷入后,SukiSU 模块列表只显示:

解释为:

也就是 r0zygisk 不依赖 Magisk 自带 Zygisk,而是自己实现一套 Zygisk 注入和守护进程机制,让 KernelSU / SukiSU 环境也能加载 Zygisk 模块。

这说明把运行状态迁移到 status.json 的方案已经生效。

后续实机反馈如下:

native 已经写出结构化 JSON:

但前端判断还主要依赖 raw 文本:

如果 raw 文本解析失败,或者和旧格式有细微差异,前端就会漏判 Zygote。

module/src/webroot/app.js 中把状态读取改成:

判断逻辑改成优先用 JSON 字段:

显示详情也改为优先显示 JSON:

执行:

并确认 zip 中包含:

输出包含:

最终 zip 时间:

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

文件:

当前能力:

文件:

当前行为:

文件:

当前描述:

SukiSU 中显示这句话是正常现象,含义是:

补充过程中的一些截图和当前最终版本截图:

Android 15 测试:

Android 16 测试:




Android 14 SukiSU:

最后把模块加载修改为列表显示,不挤成一坨,当前最终版的 WebUI 样式:

测试后功能没有问题,但使用 Hunter 最新版 6.58 发现仍会被检测:

以上为使用 ptrace 的方式注入。

这里尝试了多种修改方式,最终仍被检测到;期间还出现过一次卡开机,随后重刷并重建环境后继续分析。

第一版基于 ptrace 的注入方案在功能上已经稳定,但用 Hunter 6.58 实测时仍会被检测,日志里出现:

这说明问题不在“是否能注入”,而在“注入行为特征是否被对方检测到”。所以第二版的核心工作不是继续堆规避技巧,而是先把 Hunter 的检测链路完整拆出来,再按检测点反推改造方向。

使用 apktool 解包 com.zhenxi.hunter.apk,全局搜索 Ptrace Checkfind zygisk detectedzygote root pid 等关键字。

结果很快明确:

这一步很关键。因为如果误判成“Java 层静态字符串检测”,后续优化方向会完全跑偏。

接下来把谁在触发 checkZygisk() 这件事补齐。沿着 smali 回溯后,能定位到两条主要路径:

同时在 AndroidManifest.xml 中可以看到对应服务进程(包括 hunter_server_iso 与 twin 服务),和运行时日志中的进程标签可以对上。这意味着检测不是偶发调用,而是有明确的服务化触发链路。

因为目标 so 是 ELF aarch64 且有 strip,不能依赖常规符号名直接定位,所以采用了“字符串 + JNI 注册表 + 反汇编交叉”的方式:

到这一步,链路已经闭合:Java 触发点、JNI 绑定点、native 实现点三者一致。

checkZygisk 不是单一判断,而是“多分支命中后统一组装结果对象”的结构。结合反汇编可还原为:

也就是说,Hunter 在这一块并非只看某个固定文件是否存在,而是把“进程关系 + 特征路径 + 运行态信息”做了组合判定。

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

第一版是典型的 ptrace -> 远程 dlopen 路线:

在第七部分中确认 Hunter 检测是“多维组合判定”后,这条路线风险较高,所以第二版切换为 native bridge loader -> payload -> zygisk core -> daemon

关键改动如下:

这次改造是架构级切换,不是启动命令替换,实际同时调整了注入入口、动态库产物、安装路径、启动时序与状态通道。

完成注入路线切换后,第二版继续做命名统一,避免构建、日志、脚本、WebUI 之间出现旧名残留。

指令:

执行过程:

执行结果:项目完成改名后刷入测试,仍被检测;原因是检测侧仍能发现 libzygisk.so

接下来继续让 AI 进行修改:

指令:

执行结果:

指令:

执行结果:


刷入手机结果:

WebUI:

安装和打开lsposed模块也正常:

最新版 Hunter 检测结果全绿:

ruru结果:

至此,第二版大修结束,已通过新版 Hunter 的 Zygisk 检测。

这次做下来,最大的收获其实不只是把模块跑起来了,而是把一条完整的开发链路真正走通了:先确认最新版 MagiskZygisk 已经适配 Android 16,再借助 code-panorama 和 AI 快速梳理源码结构,后面一步步补齐 API、JNI 签名、打包链路、WebUI、守护进程状态和注入方式,最后再去处理真实设备上的兼容问题和检测问题。一路做下来,才把现在这个带 WebUI、功能上等效于 Zygiskr0z 模块完整落地。

完整源码和发布包已经开源在 GitHub Releases:

r0zygisk Releases

这篇文章记录的是一个完整模块从源码阅读、迁移实现、联调修复,到最后对抗检测的过程。源码规模够大、调用链够长的时候,AI 能帮人省掉很多纯体力活,但真正决定结果的,还是自己去拆执行流程、判断注入链路、定位检测点、解决刷机和兼容性问题。至少对这次来说,目标已经完成了:模块做出来了,WebUI 跑起来了,Android 16 适配了,第二版也通过了新版 Hunter 的检测。

不过目前已经有朋友反馈,还是存在被检测到的情况。后续还会继续修改和完善,项目也会持续更新,新的进展我会继续补到后面的文章里。谢谢大家关注。

文件 作用
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
目标库 被 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 关闭
字段 含义
plt_backup 记录已经成功注册的 PLT hook,用于后续恢复
runtime_callbacks NativeBridgeRuntimeCallbacks,枚举 JNI native 方法时使用
self_handle Zygisk 自身的 dlopen handle,用于最后 dlclose
should_unmap 是否允许自卸载
字段 含义
api_version 模块使用的 Zygisk API 版本
impl 模块对象指针
preAppSpecialize app specialize 前回调
postAppSpecialize app specialize 后回调
preServerSpecialize system_server specialize 前回调
postServerSpecialize system_server specialize 后回调
字段 含义
id 模块在 magiskd 模块列表中的下标,用于 companion/getModuleDir
unload 模块是否要求 post 后 dlclose
handle dlopen handle
entry zygisk_module_entry 函数
api Zygisk 注入给模块的 API 表
mod 模块回填的 ABI 表
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
字段 含义
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 排除项
方法 调用时机
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 后
字段 含义
lib_name 当前写入 native bridge 属性的值,用于恢复原属性
sockets 32 位和 64 位 zygiskd 的连接 socket
start_count zygote crash 计数,用于回滚
函数/符号 文件 作用
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
目录中的 Magisk-master 是最新版 Magisk,分析这个版本和之前版本的差异,是否适配了 Android 16。

• 结论:Magisk-master 这个版本已经适配 Android 16,并且适配点明确覆盖 Android 16 QPR2。更准确地说,它不是只把 App 的 targetSdk 改到 36,而是同时处理了 MagiskInit、MagiskPolicy、
  Zygisk 三个对 root/注入影响最大的底层点。

  本地版本判断
  本地 Magisk-master/docs/changes.md:3 显示当前是 v30.7 (2026.2.23),重点变更是:

  - Magisk-master/docs/changes.md:5:MagiskInit 支持 Android 16 QPR2 sepolicy format
  - Magisk-master/docs/changes.md:7:Zygisk 支持 Android 16 QPR2 and higher
  - Magisk-master/docs/changes.md:29:v30.3 已经加入 Android 16 QPR2 新 sepolicy binary format 支持

  App 构建配置也对上了 Android 16/QPR2:

  - Magisk-master/app/build-logic/src/main/java/Setup.kt:46:compileSdk 使用 release(36)
  - Magisk-master/app/build-logic/src/main/java/Setup.kt:48:minorApiLevel = 1,对应 Android 16 QPR2 SDK 36.1
  - Magisk-master/app/build-logic/src/main/java/Setup.kt:229:targetSdk = 36

  这些配置和 Google 官方 Android 16/QPR2 SDK 文档一致:Android 16 是 API 36,QPR2 使用 release(36) { minorApiLevel = 1 }。参考 Android 官方 API level 表、Android 16 SDK 设置、
  Android 16 QPR2 SDK 设置:
  https://developer.android.com/guide/appendix/api-levels
  https://developer.android.com/about/versions/16/setup-sdk
  https://developer.android.com/about/versions/16/qpr2/setup-sdk?hl=en

  和之前版本的主要差异
  如果按 changelog 看,Android 16 相关差异主要集中在 v30.3 到 v30.7:

  - v30.3:开始支持 Android 16 QPR2 引入的新 sepolicy binary format,并支持安装到 vendor_boot。
  - v30.5:改进命令行参数解析,resetprop 支持带 property overrides 的 Android 版本。
  - v30.6:回滚一个可能导致 bootloop 的改动。
  - v30.7:补齐 Android 16 QPR2 的关键适配,尤其是 MagiskInit 和 Zygisk。

  本地还有一个 文章发布版本/Magisk-master,我对比了文件清单和关键版本信息,它同样是 v30.7 / versionCode 30700,不是一个明显的旧版目录。因此这里的“之前版本”更适合按 changelog 里的
  v30.6/v30.5/v30.3 来分析,而不是把旁边那个目录当作旧版。

  底层适配证据
  Zygisk 适配不是简单判断 SDK_INT,而是维护多组 Zygote JNI 签名。Android 16/QPR2 相关新增参数体现在:

  - Magisk-master/native/src/core/zygisk/gen_jni_hooks.py:282:fas_u
  - Magisk-master/native/src/core/zygisk/gen_jni_hooks.py:303:包含 mount_sysprop_overrides
  - Magisk-master/native/src/core/zygisk/gen_jni_hooks.py:497:spec_u
  - Magisk-master/native/src/core/zygisk/gen_jni_hooks.py:516:同样处理 mount_sysprop_overrides

  sepolicy 编译/加载路径也按 split policy、mapping、compat CIL、vendor/product/system_ext/odm 等路径处理:

  - Magisk-master/native/src/sepolicy/policydb.cpp:153:读取并设置 policy version
  - Magisk-master/native/src/sepolicy/policydb.cpp:163:加载 platform mapping CIL
  - Magisk-master/native/src/sepolicy/policydb.cpp:166:加载 compat CIL
  - Magisk-master/native/src/sepolicy/policydb.cpp:192:继续加载 vendor 侧策略

  官方上游 changelog 也和本地内容一致,v30.7 明确写了 Android 16 QPR2 sepolicy 和 Zygisk 支持:
  https://github.com/topjohnwu/Magisk/blob/master/docs/changes.md

  判断
  可以认为这个 Magisk-master 已经适配 Android 16,且适配范围包含 Android 16 QPR2:

  - App 层:compileSdk 36.1、targetSdk 36
  - MagiskInit:支持 Android 16 QPR2 sepolicy format
  - MagiskPolicy:从 v30.3 开始支持 Android 16 QPR2 新 sepolicy binary format
  - Zygisk:明确支持 Android 16 QPR2 and higher,并包含对应 JNI 签名变体

  剩下的风险不在“代码是否声明适配”,而在具体机型:Android 16/QPR2 的不同 ROM、内核、vendor_boot 布局、SELinux policy、OEM 改 Zygote 签名,都可能让某些设备还需要实机验证。代码层面
  看,这版已经做了 Android 16 适配。
我现在要详细了解Magisk中zygisk的执行流程和加载过程,逐行详细解释目录中的代码,并生成到MAGISK-ZYGISK-SOURCE.md
ro.dalvik.vm.native.bridge
extern "C" NativeBridgeCallbacks NativeBridgeItf { ... };
libnativebridge.so: dlclose
libandroid_runtime.so: fork
libandroid_runtime.so: unshare
libandroid_runtime.so: selinux_android_setcontext
libandroid_runtime.so: strdup
libandroid_runtime.so: __android_log_close
fds_to_ignore
is_child_zygote
is_top_app
pkg_data_info_list
whitelisted_data_info_list
mount_data_dirs
mount_storage_dirs
uid, gid, gids, runtime_flags, permitted_capabilities, effective_capabilities
hookJniNativeMethods
pltHookRegister(regex 版本)
pltHookExclude
pltHookCommit
connectCompanion
setOption
getModuleDir
getFlags
exemptFd
api.base.impl = this
api.base.registerModule = RegisterModuleImpl
64 位写 true
32 位写 false
zygisk_module_entry
zygisk_companion_entry
ctx.nativeSpecializeAppProcess_pre()
原始 nativeSpecializeAppProcess
ctx.nativeSpecializeAppProcess_post()
magisk zygisk companion <fd>
ZygiskState
zygisk_should_load_module
magiskd set_prop(ro.dalvik.vm.native.bridge)
    ↓
zygote LoadNativeBridge("libzygisk.so...")
    ↓
NativeBridgeItf.isCompatibleWith()
    ↓
hook_entry()
    ↓
HookContext::hook_plt()
    ↓
dlclose hook -> post_native_bridge_load()
    ↓
保存 callbacks,必要时补载真实 native bridge
    ↓
strdup("com.android.internal.os.ZygoteInit")
    ↓
hook_zygote_jni()
    ↓
替换 Zygote native specialize 方法
Zygote.nativeForkAndSpecialize(...)
    ↓
Zygisk generated lambda
    ↓
ZygiskContext ctx
    ↓
ctx.nativeForkAndSpecialize_pre()
    ↓
old_fork() 先 fork
    ↓
子进程 get_module_info(uid, process)
    ↓
magiskd 返回 flags + module fds
    ↓
子进程 dlopen 模块
    ↓
模块 onLoad + preAppSpecialize
    ↓
调用原始 nativeForkAndSpecialize
    ↓
fork hook 返回缓存 pid,避免二次 fork
    ↓
原始 specialize 完成
    ↓
ctx.nativeForkAndSpecialize_post()
    ↓
模块 postAppSpecialize
    ↓
ctx 析构,恢复 JNI hook,准备自卸载
Zygote.nativeForkSystemServer(...)
    ↓
Zygisk generated lambda
    ↓
ctx.nativeForkSystemServer_pre()
    ↓
old_fork()
    ↓
子进程 get_module_info(1000, "system_server")
    ↓
加载模块并执行 preServerSpecialize
    ↓
把加载失败模块 id 写回 magiskd
    ↓
调用原始 nativeForkSystemServer
    ↓
ctx.nativeForkSystemServer_post()
    ↓
模块 postServerSpecialize
模块 Api::connectCompanion()
    ↓
ZygiskModule::connectCompanion()
    ↓
magiskd zygisk_handler ConnectCompanion
    ↓
connect_zygiskd()
    ↓
启动或复用 zygiskd32/zygiskd64
    ↓
zygiskd 收 module fds 并查 zygisk_companion_entry
    ↓
magiskd 把 client fd 发送给 zygiskd
    ↓
zygiskd 读 module_id
    ↓
exec_companion_entry(client, module_entry)
    ↓
Rust ThreadPool 调模块 companion_handler(client)
当前目录中有Magisk-master和r0zygisk两个项目。Magisk-master中已存在适配 Android 16的zygisk,我现在需要移植这个版本到r0zygisk,用作 KernelSU / Magisk 环境下的 Zygisk 替代实现。最终要形成打包文件:r0zygisk-v1.0.0-1-release.zip
    1. 先检查 `Magisk-master` 是否体现 Android 16 支持。
       - `app/build-logic/src/main/java/Setup.kt` 中 `compileSdk` / `targetSdk` 使用 API 36。
       - `scripts/avd.sh` 中 `Baklava) api=36 ;;`,说明脚本知道 Android 16 代号和 API 级别。
       - `native/src/core/zygisk/gen_jni_hooks.py` 和生成的 `jni_hooks.hpp` 中有新的 Zygote JNI 签名候选,例如 `nativeForkAndSpecialize_b`。
    
    2. 再检查 `r0zygisk` 的结构。
       - `r0zygisk` 不是普通 Zygisk 模块,而是独立 Zygisk 实现,包含自己的 `loader`、`zygiskd`、ptrace 注入逻辑和模块分发逻辑。
       - 因此不能直接把 Magisk 的 `hook.cpp/module.cpp` 整套替换进去,否则会破坏 r0zygisk 自己的架构。
    
    3. 采用保守迁移策略。
       - 同步公开 Zygisk API 到 Magisk 最新 API v5。
       - 保留 r0zygisk 自己的 daemon / ptrace / 模块加载流程。
       - 只把必要的 Zygote JNI 签名和参数结构迁移进 r0zygisk 的现有 hook 生成器。
    
    4. 修改版本命名。
       - 按要求改为固定输出 `r0zygisk-v1.0.0-1-release.zip`。
loader/src/include/api.hpp
#define ZYGISK_API_VERSION 5
loader/src/injector/module.hpp
jboolean *mount_sysprop_overrides = nullptr;
using module_abi_v5 = module_abi_v1;
using api_abi_v5 = api_abi_v4;
loader/src/injector/hook.cpp
AppSpecializeArgs_v3 *app;
AppSpecializeArgs_v5 *app;
case 5:
loader/src/injector/gen_jni_hooks.py
is_perception_app
use_fifo_ui
fas_b
fas_nubia_u
spec_xr_u
spec_nubia_u
loader/src/injector/jni_hooks.hpp
nativeForkAndSpecialize_b
nativeForkAndSpecialize_nubia_u
nativeSpecializeAppProcess_xr_u
nativeSpecializeAppProcess_nubia_u
build.gradle.kts
val verName by extra("v4-0.9.1.1")
val verCode by extra(gitCommitCount)
val verName by extra("v1.0.0")
val verCode by extra(1)
module/build.gradle.kts
val zipFileName = "$moduleName-$verName-$verCode-$commitHash-$buildTypeLowered.zip".replace(' ', '-')
val zipFileName = "$moduleName-$verName-$verCode-$buildTypeLowered.zip".replace(' ', '-')
module/build.gradle.kts
"versionName" to "$verName ($verCode-$commitHash-$variantLowered)",
"versionName" to "$verName ($verCode-$variantLowered)",
version=v1.0.0 (1-release)
versionCode=1
./gradlew :loader:assembleRelease --no-daemon --console=plain
BUILD SUCCESSFUL
./gradlew zipRelease --no-daemon --console=plain
BUILD SUCCESSFUL
module/build/outputs/release/r0zygisk-v1.0.0-1-release.zip
build.gradle.kts
module/build.gradle.kts
loader/src/include/api.hpp
loader/src/injector/module.hpp
loader/src/injector/hook.cpp
loader/src/injector/gen_jni_hooks.py
loader/src/injector/jni_hooks.hpp
r0zygisk-v1.0.0-1-release.zip 刷入并测试后,功能正常。但是当前版本中,我没有看到 WebUI,我希望增加 WebUI 的功能。
    项目中已经存在一个最小 WebUI,位于:
    
    - `module/src/webroot/index.html`
    - `module/src/webroot/styles.css`
    - `module/src/webroot/app.js`
    
    初始页面很简陋,只显示:
    
    - 当前状态原始输出
    - `start`
    - `stop`
    - `exit`
`r0zygisk-v1.0.0-1-release.zip` 刷入并测试后,功能本身是正常的,也已经生成了 WebUI;但页面既不够直观,也缺少功能说明。因此我进一步参考了 GitHub 上 `KernelSU` 和 `ZygiskNext` 的 WebUI,想先弄清它们分别提供了什么功能,再决定当前项目里应该接哪些能力。
module/src/webroot/index.html
module/src/webroot/styles.css
module/src/webroot/app.js
module/src/zygisk-ctl.sh
exec $MODDIR/bin/zygisk-ptrace64 ctl $*
ctl start
ctl stop
ctl exit
version
cat /data/adb/modules/r0zygisk/module.prop
ps -A | grep -E "zygiskd|zygisk-ptrace|app_process"
for d in /data/adb/modules/*; do ...
[monitor:tracing, zygote64:injected, daemon64:running(...)]
node --check module/src/webroot/app.js
./gradlew :module:prepareModuleFilesRelease --no-daemon --console=plain
./gradlew :module:zipRelease --no-daemon --console=plain
module/build/outputs/release/r0zygisk-v1.0.0-1-release.zip
/data/adb/modules/r0zygisk
for base in /data/adb/modules /data/adb/modules_update /data/adb/ksu/modules /data/adb/ap/modules; do
  ...
  if grep -q '^id=r0zygisk$' "$prop" || grep -q '^name=r0zygisk$' "$prop"; then
    MODDIR=${prop%/module.prop}
  fi
done
bridge.exec(command, callback)
无法直接执行 WebUI 命令
exec 超时,管理器没有返回命令结果
<section class="panel">
  ...
  <h2>官方 WebUI 功能地图</h2>
  ...
</section>
exec(command, callbackFunction)
exec(command, JSON.stringify(options), callbackFunctionName)
const callbackName = `r0zygisk_exec_${Date.now()}_${callbackCounter++}`;

window[callbackName] = (errno, stdout, stderr) => {
  finish({ errno, stdout, stderr });
};

bridge.exec(command, "{}", callbackName);
bridge.exec(command, callbackName)
bridge.exec(command)
node --check module/src/webroot/app.js
./gradlew :module:zipRelease --no-daemon --console=plain
unzip -p module/build/outputs/release/r0zygisk-v1.0.0-1-release.zip webroot/app.js | rg "callbackName|exec\\(command"
bridge.exec(command, "{}", callbackName)
tracing, zygote64: injected, daemon64: running(Root: KernelSU, module(3): ...)
fprintf(prop.get(), "%s[%s] %s", pre_section.c_str(), status_text.c_str(), post_section.c_str());
description=[monitor:tracing, zygote64:injected, daemon64:running(...)] Standalone implementation of Zygisk.
/data/adb/modules/r0zygisk/status.json
loader/src/ptracer/monitor.cpp
static std::string status_path;
status_path = "./status.json";
close(open(status_path.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0644));
updateStatus();
{
  "monitor": "tracing",
  "stop_reason": "",
  "zygote64": "injected",
  "daemon64": "running",
  "daemon64_info": "Root: KernelSU,module(3): ...",
  "zygote32": "unsupported",
  "daemon32": "unsupported",
  "daemon32_info": "",
  "raw": "monitor:tracing, zygote64:injected, daemon64:running(...)"
}
tracing
stopped
exited
injected
not injected
running
crashed
unknown
module/src/webroot/app.js
cat "$MODDIR/status.json" 2>/dev/null || true
--- status.json ---
...
node --check module/src/webroot/app.js
./gradlew :module:zipRelease --no-daemon --console=plain
strings module/build/outputs/module/release/lib/arm64-v8a/libzygisk_ptrace.so | rg "status\\.json|description=|monitor:"
./status.json
Standalone implementation of Zygisk.
Zygisk 的独立实现。
"zygote64": "injected"
const status = parseStatus(readStatusRaw(statusText, prop));
const zygoteOk = status.zygotes.some((item) => /:injected\b/.test(item));
function readStatus(statusText, prop) {
  const text = statusText.trim();
  let json = null;
  let raw = prop.description || "";

  if (text) {
    try {
      json = JSON.parse(text);
      if (json && json.raw) {
        raw = String(json.raw);
      }
    } catch (_) {
      raw = text;
    }
  }

  const parsed = parseStatus(raw);
  parsed.json = json;
  return parsed;
}
const json = status.json || {};

const monitorOk =
  json.monitor === "tracing" ||
  status.monitor.includes("tracing") ||
  /zygisk-ptrace/.test(processes);

const daemonOk =
  json.daemon64 === "running" ||
  json.daemon32 === "running" ||
  status.daemons.some((item) => item.includes("running")) ||
  /zygiskd/.test(processes);

const zygoteOk =
  json.zygote64 === "injected" ||
  json.zygote32 === "injected" ||
  status.zygotes.some((item) => /:injected\b/.test(item));
zygote64:injected
daemon64:running
node --check module/src/webroot/app.js
./gradlew :module:zipRelease --no-daemon --console=plain
unzip -p module/build/outputs/release/r0zygisk-v1.0.0-1-release.zip webroot/app.js | rg "json\\.zygote64"
json.zygote64 === "injected"
2026-04-17 14:54
module/src/webroot/index.html
module/src/webroot/styles.css
module/src/webroot/app.js
loader/src/ptracer/monitor.cpp
/data/adb/modules/r0zygisk/status.json
module/src/module.prop
description=Standalone implementation of Zygisk.
Zygisk 的独立实现。
Ptrace Check Zygisk
find zygisk detected, zygote root pid ...
修改目录中 zygisk 字符串为 r0z,不要影响核心功能。
统一规则如下:

- 模块标识:`r0zygisk` -> `r0z`
- daemon:`zygiskd` -> `r0zd`
- 控制脚本:`zygisk-ctl.sh` -> `r0z-ctl.sh`
- 注入核心库发布命名:`libzygisk.so` -> `libr0zgk.so`
- 版本:`v1.0.1``versionCode=2`

同步范围覆盖构建、打包、脚本、WebUI 与文档:

1. Gradle/Cargo 任务与逻辑名映射。
2. `module.prop`、安装脚本、控制脚本文案统一 `r0z`3. 打包签名与文件清单映射更新。
4. WebUI 标题与状态描述统一。
5. 发布包命名统一为 `r0z-v1.0.1-2-<buildType>.zip`。

这一部分的目的不是“改展示文字”,而是把第二版运行链路与工程标识统一成同一套语义,保证后续排障和迭代基线稳定。
为什么生成产物中仍存在 `libzygisk.so`?它的功能是什么,是否可以改名为 `libr0zgk`?
改名并保持可运行
文件 作用
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
目标库 被 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 关闭
字段 含义
plt_backup 记录已经成功注册的 PLT hook,用于后续恢复
runtime_callbacks NativeBridgeRuntimeCallbacks,枚举 JNI native 方法时使用
self_handle Zygisk 自身的 dlopen handle,用于最后 dlclose
should_unmap 是否允许自卸载
字段 含义
api_version 模块使用的 Zygisk API 版本
impl 模块对象指针
preAppSpecialize app specialize 前回调
postAppSpecialize app specialize 后回调
preServerSpecialize system_server specialize 前回调
postServerSpecialize system_server specialize 后回调
字段 含义
id 模块在 magiskd 模块列表中的下标,用于 companion/getModuleDir
unload 模块是否要求 post 后 dlclose
handle dlopen handle
entry zygisk_module_entry 函数
api Zygisk 注入给模块的 API 表
mod 模块回填的 ABI 表
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
字段 含义
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 排除项
方法 调用时机
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 后
字段 含义
lib_name 当前写入 native bridge 属性的值,用于恢复原属性
sockets 32 位和 64 位 zygiskd 的连接 socket
start_count zygote crash 计数,用于回滚
函数/符号 文件 作用
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
  • Android Gradle Plugin:8.9.1
  • Gradle Wrapper:8.11.1
  • org.gradle.java.home=/Library/Java/JavaVirtualMachines/jdk-17.jdk/Contents/Home
  • nativeForkAndSpecialize
  • nativeSpecializeAppProcess
  • nativeForkSystemServer
  • 对 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
  • 调用模块 postAppSpecializepostServerSpecialize
  • 根据模块选项可能 dlclose 模块。
  • 恢复 Zygote JNI hook。
  • 安装自卸载 hook。
  • arm64:查 r19-r28。
  • arm32:查 r4-r10。
  • x86:从 ebp 相对位置读第二参数。
  • x86_64:查 rbx/r12-r15 等 callee-saved 寄存器。
  • riscv:查 callee-saved x8/x9/x18-x27。
  • dlsym(RTLD_DEFAULT, ...)
  • 找不到就扫描并 dlopen libnativehelper.so
  • 遇到 nativeForkAndSpecialize,用 fork_app_methods 替换。
  • 遇到 nativeSpecializeAppProcess,用 specialize_app_methods 替换。
  • 遇到 nativeForkSystemServer,用 fork_server_methods 替换。
  • 使用 Magisk-master/native/src/core/zygisk/api.hpp 覆盖 r0zygisk 旧版公开 API 头文件。
  • ZYGISK_API_VERSION 从旧版本升级为:
  • 新编译的 Zygisk 模块会按 API v5 注册。
  • r0zygisk loader 需要能识别并加载 API v5 模块。
  • 增加 AppSpecializeArgs_v5
  • AppSpecializeArgs_v5 继承 AppSpecializeArgs_v3,并单独持有:
  • 增加 API v5 兼容别名:
  • call_app 增加 case 5,让 API v5 模块按新版参数传入。
  • 兼容 Magisk 最新 Zygisk API v5。
  • 保留对旧 API v1-v4 模块的兼容。
  • ZygiskContext 中 app 参数指针从:
  • ZygiskModule::valid() 接受 API v5:
  • r0zygisk 的模块加载逻辑可以接受 v5 模块。
  • Zygote JNI wrapper 传入的是新版 specialize 参数对象。
  • wrapper 初始化参数从 AppSpecializeArgs_v3 改为 AppSpecializeArgs_v5
  • 增加 Magisk 最新 Zygisk 中已有的新参数:
  • 增加新的 Zygote JNI 签名候选:
  • 重新生成:
  • nativeForkAndSpecialize_b 是 Android 16 / Baklava 相关适配的重要线索。
  • 新增的 OEM / XR 签名提高了对新系统和厂商 ROM 的兼容范围。
  • 项目从自己的第一版开始。
  • versionCode 固定为 1
  • 不再因为目录不是 git 仓库而输出 unknown
  • 最终文件名符合要求。
  • Zygisk API v5 迁移。
  • Android 16 / Baklava 相关 JNI 签名候选加入。
  • r0zygisk 自有 loader / zygiskd / ptrace 架构保留。
  • 输出文件名改为 r0zygisk-v1.0.0-1-release.zip
  • 完整打包通过。
  • 状态页
  • 基本信息
  • 设置页
  • Root 实现显示
  • Zygote monitor 状态
  • Zygisk module 状态
  • ZN Module 状态
  • KernelSU / Magisk / APatch 排除列表策略说明
  • 日志写入 dmesg
  • 非 root 应用作为排除列表
  • 强制排除列表
  • 匿名内存加载
  • Zygisk Next linker
  • Zygote 注入状态说明
  • 模块问题提示
  • 状态仪表盘
  • 追踪器控制按钮
  • 已发现 Zygisk 模块列表
  • 每个按钮的说明
  • 原始诊断输出
  • ZygiskNext 参考功能地图,并标注当前 fork 是否接入
  • module/src/webroot/index.html
  • module/src/webroot/styles.css
  • module/src/webroot/app.js

传播安全知识、拓宽行业人脉——看雪讲师团队等你加入!

最后于 2026-5-12 22:01 被fyrlove编辑 ,原因: 补漏
上传的附件:
收藏
免费 22
打赏
分享
打赏 + 1.00雪花
打赏次数 1 雪花 + 1.00
 
赞赏  kanxue   +1.00 2026/05/29 感谢分享~
最新回复 (11)
雪    币: 299
活跃值: (1460)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
严肃学习
2026-5-12 22:44
0
雪    币: 14
活跃值: (4659)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
3

大制作啊,好好学习一下

最后于 2026-5-12 22:52 被fyrlove编辑 ,原因: 哈哈哈,有AI加持,效率翻倍
2026-5-12 22:50
0
雪    币: 4206
活跃值: (6942)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
4
感谢分享
2026-5-12 22:50
0
雪    币: 104
活跃值: (8682)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
5
tql
2026-5-13 09:23
0
雪    币: 100
活跃值: (736)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
6
native bridge死路一条
2026-5-13 21:47
0
雪    币: 6015
活跃值: (2440)
能力值: (RANK:140 )
在线值:
发帖
回帖
粉丝
7
mb_bvvcoitr native bridge死路一条
为什么呢。用什么会比较好,有相关文章或者资料推荐一下吗
2026-5-13 22:47
0
雪    币: 815
活跃值: (1020)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
8
666
2026-5-14 11:16
0
雪    币: 2212
活跃值: (1263)
能力值: ( LV6,RANK:95 )
在线值:
发帖
回帖
粉丝
9
感谢大佬的分享
2026-5-15 20:58
0
雪    币: 434
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
10
感谢大佬的教程分享,太棒了
2026-5-26 21:48
0
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
11

学习了 感谢大佬无私分享,这个lsposed 还会检测到吗?
还得冒昧的问问 这个 zygisk frida Gadget 和frida server 对比是更好用吗?最近感觉 frida server 总是被各种检测,学艺不精,还望海涵。

最后于 2026-5-26 22:54 被git_86585flowclouds编辑 ,原因:
2026-5-26 22:52
0
雪    币: 6015
活跃值: (2440)
能力值: (RANK:140 )
在线值:
发帖
回帖
粉丝
12
git_86585flowclouds 学习了&nbsp;感谢大佬无私分享,这个lsposed&nbsp;还会检测到吗?还得冒昧的问问&nbsp;这个&nbsp;zygisk&nbsp;frida&a ...
这个只是一个独立的Zygisk模块,安装需要依赖zygisk的其他模块的,比如lsp等。frida gaget可以修改,隐藏效果更好。frida server被检测太正常了。一步一步学吧,论坛上有很多检测的文章。
2026-5-27 00:23
0
游客
登录 | 注册 方可回帖
返回