Zygisk 注入与通用悬浮窗
在 Android Mod 领域,我们的目标是构建一个强大的注入后端(.so)和一个灵活的 UI 前端(悬浮窗)。一个平庸的设计是将二者紧密耦合,而一个出色的设计,则是我们今天要探讨的:将 .so 作为“大脑”和服务端,悬浮窗 App 只是一个“空壳”和客户端。
这一架构的完整原理依赖三个核心组件:
- 注入层 (Injector): Magisk Zygisk 模块,负责将
.so 动态库注入目标应用进程。
- 服务端 (Backend): 注入的
.so 文件。作为“大脑”在应用进程中运行,包含所有 Hook 逻辑,并作为服务端监听抽象套接字。
- 客户端 (Frontend): 一个通用悬浮窗 App。被手动启动,对应用一无所知,作为客户端连接
.so,并动态请求功能列表来构建 UI。
原理
什么是 Zygisk 注入?
与依赖 Xposed/LSPosed 在 App 层面进行 Hook 不同,Zygisk 是一种更底层、更强大的注入方式。
Zygote 是 Android 系统的“应用孵化器”。所有 App 进程都是由 Zygote 进程 fork 出来的。
Magisk 的 Zygisk 功能,允许我们的自定义代码(一个 Zygisk 模块)在 Zygote 进程中运行。
我们可以在 Zygote fork 出一个新 App 进程之后,但在该 App 开始执行自己的代码之前(这个时机称为 postAppSpecialize),执行我们的逻辑。
对于我们的 Mod 需求,这意味着:当 Zygisk 检测到目标应用即将启动时,可以立即使用 dlopen 将我们的 my_hook_payload.so 直接加载到应用进程的内存空间中。
这种注入是原生的、与 App 源码无关的,并且完全绕过了 App 层的各种 Hook 检测。
什么是抽象套接字?
一旦 .so 成功注入,就运行在应用进程中。而我们的悬浮窗 App 运行在自己的进程中。两者需要跨进程通信 (IPC)。
- 传统套接字 (文件系统): 比如在
/data/local/tmp/my.sock 创建一个套接字文件。这会立即带来文件权限和 SELinux 的灾难。应用进程(作为u:r:untrusted_app:s0)几乎不可能有权限访问这个文件,悬浮窗 App 也一样。
- 抽象套接字 (Abstract UDS): 这是一个关键的 Linux 特性。的地址不在文件系统中,而是存在于一个以空字节 (
\0) 开头的特殊命名空间中(例如 \0my_mod_socket)。
使用抽象套接字的优势:
- 无视权限: 因为不落地为文件,我们完全绕过了 Android 复杂的文件权限和沙箱。
- 简单高效: 是标准的 Socket 编程,在 C++ (
.so 端) 和 Java/Kotlin (App 端) 都有成熟的 API。
- SELinux 友好: 进程间使用抽象套接字通信通常被 SELinux 策略所允许(
unix_stream_socket connectto)。
交互设计
- App (客户端) 不重要:是一个“哑终端”(Dumb Terminal),不需要知道目标应用的包名,更不需要知道功能实现的具体细节。的唯一职责是:
- 被用户手动启动。
- 启动一个后台服务。
- 不断尝试连接一个“约定好”的抽象套接字地址(例如
\0my_mod_socket)。
- 连接成功后,发送一个标准命令:
get_features。
- 接收服务端的响应(通常是 JSON),并据此动态生成菜单按钮。
.so (服务端) 成为一切的核心:
- 被 Zygisk 注入应用进程。
- 立即创建、绑定并监听
\0my_mod_socket 地址。
- 等待 App 客户端连接。
- 连接成功后,等待来自 App 的命令。
- 当收到
get_features 命令时,会立即检查自己所在的应用环境,并动态构建一个功能列表(例如 [{"id":"god_mode", "name":"上帝模式"}]),将其序列化(如 JSON)并发回给 App。
- 当收到 App 发来的功能启用命令(如
enable:god_mode)时,才去执行对应的 Hook 或内存修改。
这种设计实现了终极解耦:可以用同一个悬浮窗 App,去适配 10 款完全不同的应用,唯一需要修改的只是每款应用对应的 .so 文件。
实践
服务端 (.so)
.so 必须在新线程中启动其服务,以避免阻塞应用主线程。
逻辑: 应用启动 -> Zygisk 注入 .so -> .so (如 JNI_OnLoad) 创建一个 pthread -> 在新线程中 socket -> bind -> listen -> accept (阻塞等待 App)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84
| #include <pthread.h> #include <sys/socket.h> #include <sys/un.h> #include <unistd.h> #include <string.h>
const char* SOCKET_NAME = "\0my_mod_socket"; int g_client_fd = -1;
const char* get_game_features_json() { return "{\"features\":[{\"id\":\"demo1\",\"name\":\"demo1\"},{\"id\":\"demo2\",\"name\":\"demo2\"}]}"; }
void* socket_server_thread(void* arg) { int server_fd = socket(AF_UNIX, SOCK_STREAM, 0);
struct sockaddr_un addr; memset(&addr, 0, sizeof(addr)); addr.sun_family = AF_UNIX; memcpy(&addr.sun_path, SOCKET_NAME, strlen(SOCKET_NAME) + 1); int len = sizeof(sa_family_t) + strlen(SOCKET_NAME) + 1; if (bind(server_fd, (struct sockaddr*)&addr, len) == -1) { close(server_fd); return NULL; }
if (listen(server_fd, 1) == -1) { close(server_fd); return NULL; }
while (true) { g_client_fd = accept(server_fd, NULL, NULL); if (g_client_fd == -1) { continue; }
char buffer[1024]; while (true) { memset(buffer, 0, sizeof(buffer)); int read_bytes = read(g_client_fd, buffer, sizeof(buffer) - 1); if (read_bytes <= 0) { close(g_client_fd); g_client_fd = -1; break; } if (strncmp(buffer, "get_features", 12) == 0) { const char* features = get_game_features_json(); write(g_client_fd, features, strlen(features)); } } } close(server_fd); return NULL; }
void InitializeHooks() { pthread_t tid; pthread_create(&tid, NULL, socket_server_thread, NULL); }
|
客户端 (App)
App 被用户手动启动。不知道应用是否已运行,所以必须不断尝试连接。
逻辑: 用户手动启动 App -> FloatingWindowService 启动 -> 在后台线程中进入 while(true) 循环 -> connect -> 失败则 sleep(2) 并重试 -> 成功则跳出循环 -> 发送 get_features -> 接收 JSON -> 动态构建 UI。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79
| import android.net.LocalSocket import android.net.LocalSocketAddress import android.util.Log import java.io.IOException
class FloatingWindowService : Service() { private val SOCKET_NAME = "my_mod_socket" private var socket: LocalSocket? = null private val TAG = "MyMod_Client"
override fun onCreate() { super.onCreate() connectToSocketServer(); }
private fun connectToSocketServer() { Thread { while (true) { try { socket = LocalSocket() val socketAddress = LocalSocketAddress(SOCKET_NAME, LocalSocketAddress.Namespace.ABSTRACT) Log.d(TAG, "App-Client: 尝试连接 SO-Server...") socket!!.connect(socketAddress) Log.i(TAG, "App-Client: 已连接到 SO-Server!") requestFeatures() break; } catch (e: IOException) { Log.w(TAG, "App-Client: 连接失败,2秒后重试...") socket?.close() Thread.sleep(2000) } } }.start() } private fun requestFeatures() { if (socket == null || !socket!!.isConnected) return Thread { try { socket!!.outputStream.write("get_features".toByteArray()) val buffer = ByteArray(4096) val readBytes = socket!!.inputStream.read(buffer) if (readBytes > 0) { val featuresJson = String(buffer, 0, readBytes) Log.i(TAG, "App-Client: 收到功能列表: $featuresJson") } } catch (e: IOException) { Log.e(TAG, "App-Client: 通信错误", e) } }.start() } }
|
请自行使用android studio开发悬浮窗,设计控件等