Zygisk 注入与通用悬浮窗

在 Android Mod 领域,我们的目标是构建一个强大的注入后端(.so)和一个灵活的 UI 前端(悬浮窗)。一个平庸的设计是将二者紧密耦合,而一个出色的设计,则是我们今天要探讨的:.so 作为“大脑”和服务端,悬浮窗 App 只是一个“空壳”和客户端。

这一架构的完整原理依赖三个核心组件:

  1. 注入层 (Injector): Magisk Zygisk 模块,负责将 .so 动态库注入目标应用进程。
  2. 服务端 (Backend): 注入的 .so 文件。作为“大脑”在应用进程中运行,包含所有 Hook 逻辑,并作为服务端监听抽象套接字。
  3. 客户端 (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)。

使用抽象套接字的优势:

  1. 无视权限: 因为不落地为文件,我们完全绕过了 Android 复杂的文件权限和沙箱。
  2. 简单高效: 是标准的 Socket 编程,在 C++ (.so 端) 和 Java/Kotlin (App 端) 都有成熟的 API。
  3. SELinux 友好: 进程间使用抽象套接字通信通常被 SELinux 策略所允许(unix_stream_socket connectto)。

交互设计

  • App (客户端) 不重要:是一个“哑终端”(Dumb Terminal),不需要知道目标应用的包名,更不需要知道功能实现的具体细节。的唯一职责是:
      1. 被用户手动启动。
      1. 启动一个后台服务。
      1. 不断尝试连接一个“约定好”的抽象套接字地址(例如 \0my_mod_socket)。
      1. 连接成功后,发送一个标准命令:get_features
      1. 接收服务端的响应(通常是 JSON),并据此动态生成菜单按钮。
  • .so (服务端) 成为一切的核心
      1. 被 Zygisk 注入应用进程。
      1. 立即创建、绑定并监听 \0my_mod_socket 地址。
      1. 等待 App 客户端连接。
      1. 连接成功后,等待来自 App 的命令。
      1. 当收到 get_features 命令时,会立即检查自己所在的应用环境,并动态构建一个功能列表(例如 [{"id":"god_mode", "name":"上帝模式"}]),将其序列化(如 JSON)并发回给 App。
      1. 当收到 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; // 全局保存客户端 FD

// (示例)序列化我们的功能列表
const char* get_game_features_json() {
// 实际中,这里会根据当前应用动态构建
return "{\"features\":[{\"id\":\"demo1\",\"name\":\"demo1\"},{\"id\":\"demo2\",\"name\":\"demo2\"}]}";
}

// Socket 线程的主函数
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 长度要包含开头的 '\0'
memcpy(&addr.sun_path, SOCKET_NAME, strlen(SOCKET_NAME) + 1);

// 绑定,注意 sizeof(addr) 是错的,要精确计算长度
int len = sizeof(sa_family_t) + strlen(SOCKET_NAME) + 1;
if (bind(server_fd, (struct sockaddr*)&addr, len) == -1) {
// LOGE("SO-Server: 绑定失败");
close(server_fd);
return NULL;
}

if (listen(server_fd, 1) == -1) {
// LOGE("SO-Server: 监听失败");
close(server_fd);
return NULL;
}

// LOGD("SO-Server: 在 %s 上监听,等待 App 客户端...", SOCKET_NAME);

// 循环接受连接(允许客户端断开后重连)
while (true) {
g_client_fd = accept(server_fd, NULL, NULL);
if (g_client_fd == -1) {
continue; // 接受失败,继续下一次循环
}

// LOGI("SO-Server: 客户端 (App) 已连接!");

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) {
// LOGW("SO-Server: 客户端断开连接");
close(g_client_fd);
g_client_fd = -1;
break; // 客户端断开,跳出内层循环,等待新连接
}

// LOGD("SO-Server: 收到命令: %s", buffer);

// ★ 核心协议:响应 "get_features"
if (strncmp(buffer, "get_features", 12) == 0) {
const char* features = get_game_features_json();
write(g_client_fd, features, strlen(features));
}
// ... 处理其他命令,例如 "enable:god_mode"
}
}
close(server_fd);
return NULL;
}

// Zygisk 注入后调用的主函数
// (或 JNI_OnLoad, 或构造函数)
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() {

// 约定好的地址,Java/Kotlin API 自动处理开头的 \0
private val SOCKET_NAME = "my_mod_socket"
private var socket: LocalSocket? = null
private val TAG = "MyMod_Client"

override fun onCreate() {
super.onCreate()
// ... 创建悬浮窗 UI 骨架 ...

// 启动连接线程
connectToSocketServer();
}

private fun connectToSocketServer() {
Thread {
// ★ 核心:连接重试循环
// 这是处理“App 先于应用启动”这一竞态条件的关键
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()

// (在这里可以启动一个读取线程,持续监听来自 SO 的主动推送)

break; // 成功后跳出重试循环

} catch (e: IOException) {
// 连接失败 (应用未启动或 SO 未就绪)
Log.w(TAG, "App-Client: 连接失败,2秒后重试...")
socket?.close()
Thread.sleep(2000) // 等待 2 秒重试
}
}
}.start()
}

// 连接成功后,立即请求功能列表
private fun requestFeatures() {
if (socket == null || !socket!!.isConnected) return

Thread {
try {
// 1. 发送 "get_features" 命令
socket!!.outputStream.write("get_features".toByteArray())

// 2. 读取响应
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")

// 3. (在主线程) 解析 JSON 并动态构建 UI
// runOnUiThread { buildMenuFromJson(featuresJson) }
}
} catch (e: IOException) {
Log.e(TAG, "App-Client: 通信错误", e)
// (此处应触发重连逻辑)
}
}.start()
}

// ...
}

请自行使用android studio开发悬浮窗,设计控件等