Xphone 开发心得

Xphone 开发心得与 Shizuku 源码分析

前言

项目地址: Xphone

自己开发了一个 戒掉短视频和手机游戏的 app , 开发过程中从 shizuku 中学习到了非 root 手机持久化获取 adb 权限的方法 特来记录一番.

Xphone 开发心得

android 内核为 linux 系统, linux 将系统权限划分为 root 和 非root权限,由于厂商不希望用户自定义去修改内核因此默认ban了root权限,但是呢开发者又需要能管理其他app的权限, 于是 android 以 root 权限启动init.rc及其配置的系统服务(包含 adbd) ,然后adbd又被设置为 shell 权限

1
2
setgid(AID_SHELL);
setuid(AID_SHELL);

而这个adb_shell 权限是能够操控其他app的, 因此如果我们能够维持一个 adb_shell 这样的高权限,就能过实现当前 OutPhone 的核心功能 任意隐藏/显示 app.

那么我们该如何维持高权限呢? 这里就不得不参考一下 shizuku 的代码实现了,下面是 shizuku 代码实现分析.

在 shizuku 中需要用户认证 adb 调试, 通过无线/有限/root的方式实现权限认证, 这里以无线认证的方式进行一步一步分析源码

寻找 adb 端口

AdbMdns.kt 用于在当前的局域网中寻找服务和对应的 IP port 寻找过程如下

  1. 通过 discoverServices 函数匹配服务名称 “_adb-tls-pairing._tcp”
1
nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, listener)

这里使用了 callback 的机制, 传入 listener 当找到服务名称之后就去回掉 onServiceFound

1
2
3
 private fun onServiceFound(info: NsdServiceInfo) {
        nsdManager.resolveService(info, ResolveListener(this))
    }

而 onServiceFound 调用 resolveService 方法去获取 IP 和 port , 获取成功后回 onServiceResolved

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
private fun onServiceResolved(resolvedService: NsdServiceInfo) {
        if (running && NetworkInterface.getNetworkInterfaces()
                .asSequence()
                .any { networkInterface ->
                    networkInterface.inetAddresses
                        .asSequence()
                        .any { resolvedService.host.hostAddress == it.hostAddress }
                }
            && isPortAvailable(resolvedService.port)
        ) {
            serviceName = resolvedService.serviceName
            observer.onChanged(resolvedService.port)
        }
    }

该函数遍历当前手机的所有网卡 IP 然后和刚刚解析到的地址进行对比, 如果相同则尝试 bind 判断端口是否被占用.

当ip端口被找到之后,会callback onChanged 函数将找到的 port 传出, 这里的 onChanged 属于 observer 类

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
    private val observer = Observer<Int> { port ->
        Log.i(tag, "Pairing service port: $port")
        if (port <= 0) return@Observer

        // Since the service could be killed before user finishing input,
        // we need to put the port into Intent
        val notification = createInputNotification(port)

        getSystemService(NotificationManager::class.java).notify(notificationId, notification)
    }

onChanged 函数调用 NotificationManager.notify 创建输入的通知输入对应的匹配码, 而在 notification 对象中指定用户需要启动的 Intent 类为 AdbPairingService (这里创建了一个 PendingIntent 包装 Intent ****)

 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
private fun createInputNotification(port: Int): Notification {
    return Notification.Builder(this, notificationChannel)
        .setColor(getColor(R.color.notification))
        .setContentTitle(getString(R.string.notification_adb_pairing_service_found_title))
        .setSmallIcon(R.drawable.ic_system_icon)
        .addAction(replyNotificationAction(port))
        .build()
}

private fun replyNotificationAction(port: Int): Notification.Action {
    // Ensure pending intent is created
    val action = replyNotificationAction

    PendingIntent.getForegroundService(
        this,
        replyRequestId,
        replyIntent(this, port),
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
            PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
        else
            PendingIntent.FLAG_UPDATE_CURRENT
    )

    return action
}

private fun replyIntent(context: Context, port: Int): Intent {
    return Intent(context, AdbPairingService::class.java).setAction(replyAction).putExtra(portKey, port)
}

而当前类正在运行当中因此用户输入匹配码并且点击发送之后,就会回到 onStartCommand 函数

 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
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
      val notification = when (intent?.action) {
          startAction -> {
              onStart()
          }
          replyAction -> {
              val code = RemoteInput.getResultsFromIntent(intent)?.getCharSequence(remoteInputResultKey) ?: ""
              val port = intent.getIntExtra(portKey, -1)
              if (port != -1) {
                  onInput(code.toString(), port)
              } else {
                  onStart()
              }
          }
          stopAction -> {
              stopForeground(STOP_FOREGROUND_REMOVE)
              stopSelf()
              null
          }
          else -> {
              return START_NOT_STICKY
          }
      }
      if (notification != null) {
          try {
              startForeground(notificationId, notification,
                  ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST)
          } catch (e: Throwable) {
              Log.e(tag, "startForeground failed", e)

              if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
                  && e is ForegroundServiceStartNotAllowedException) {
                  getSystemService(NotificationManager::class.java).notify(notificationId, notification)
              }
          }
      }
      return START_REDELIVER_INTENT
  }

并且匹配到 replyAction ,用于提取 port 和 code, 然后再调用 onInput

 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
// 代码位置: AdbPairingService.kt lines 143-164
private fun onInput(code: String, port: Int): Notification {
    GlobalScope.launch(Dispatchers.IO) {
        val host = "127.0.0.1"

        // 1. 获取/生成 ADB 密钥
        val key = try {
            // AdbKey 的构造函数会尝试读取现有密钥或生成新密钥
            AdbKey(PreferenceAdbKeyStore(ShizukuSettings.getPreferences()), "shizuku")
        } catch (e: Throwable) {
            e.printStackTrace()
            return@launch
        }

        // 2. 发起配对连接
        // 使用 AdbPairingClient 进行 TLS 配对
        AdbPairingClient(host, port, code, key).runCatching {
            start()
        }.onFailure {
            handleResult(false, it)
        }.onSuccess {
            handleResult(it, null)
        }
    }

    return workingNotification
}

和 adb 服务器进行认证

onInput 调用 AdbKey , 他和 adb 建立连接 并且生成rsa密钥,并且保存到本地的adb 服务上

在 AdbKey 的 初始化函数中

 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
	
init {
        this.encryptionKey = getOrCreateEncryptionKey() ?: error("Failed to generate encryption key with AndroidKeyManager.")

        this.privateKey = getOrCreatePrivateKey()
        this.publicKey = KeyFactory.getInstance("RSA").generatePublic(RSAPublicKeySpec(privateKey.modulus, RSAKeyGenParameterSpec.F4)) as RSAPublicKey

        val signer = JcaContentSignerBuilder("SHA256withRSA").build(privateKey)
        val x509Certificate = X509v3CertificateBuilder(X500Name("CN=00"),
                BigInteger.ONE,
                Date(0),
                Date(2461449600 * 1000),
                Locale.ROOT,
                X500Name("CN=00"),
                SubjectPublicKeyInfo.getInstance(publicKey.encoded)
        ).build(signer)
        this.certificate = CertificateFactory.getInstance("X.509")
                .generateCertificate(ByteArrayInputStream(x509Certificate.encoded)) as X509Certificate

        Log.d(TAG, privateKey.toString())
}

val adbPublicKey: ByteArray by unsafeLazy { 
    publicKey.adbEncoded(name) 
}

该函数大概做了四件事情

  1. 获取加密密钥 (Encryption Key):  Android Keystore 系统 (ANDROID_KEYSTORE) 获取或生成一个 AES-256 (GCM模式) 的密钥
  2. 获取/生成 RSA 私钥 (Private Key): 从 ShizukuSettings.SharedPreferences 中读取密钥,如果为空则重新生成 RSA 密钥,并且通过 AES 加密密钥并存储到 SharedPreferences 中
  3. 生成 x509Certificate 证书
  4. 通过 by unsafeLazy 懒加载模式 生成 publicKey 这个 key 用于 后续服务器 adb 认证

利用刚刚生成的 key 和adb服务器进行通信 调用 AdbPairingClient 的 start 函数

 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
fun start(): Boolean {
    // 1. 建立基础的 TLS 连接
    // 这是一个同步阻塞调用,会建立 TCP 连接并完成 TLS 握手,同时通过 Conscrypt 导出关键的密钥材料
    setupTlsConnection()

    // 2. 更新状态:开始交换 SPAKE2 协议消息
    state = State.ExchangingMsgs

    // 3. 执行核心的密钥交换协议 (SPAKE2 msg exchange)
    // 这个函数会通过网络发送和接收数据包,验证双方是否拥有相同的"密码"(即配对码)
    // 如果失败返回 false,配对终止
    if (!doExchangeMsgs()) {
        state = State.Stopped
        return false
    }

    // 4. 更新状态:交换设备信息
    // 密码验证成功后,双方交换具体的设备标识和对端公钥 (Peer Info)
    state = State.ExchangingPeerInfo

    // 5. 执行信息交换
    // 这里会将我们自己的 RSA 公钥发送给手机,告诉它“请信任这台电脑/APP”
    if (!doExchangePeerInfo()) {
        state = State.Stopped
        return false
    }

    // 6. 配对成功完成
    state = State.Stopped
    return true
}

private fun setupTlsConnection() {
    // 1. 创建普通 TCP Socket
    // host = "127.0.0.1" (通常通过端口转发或直接连), port 由 mDNS 发现
    socket = Socket(host, port)
    
    // 2. 禁用 Nagle 算法
    // 确保数据包立即发送,对这种交互式握手协议很重要,避免延迟
    socket.tcpNoDelay = true

    // 3. 升级为 TLS (SSL) Socket
    // key.sslContext 是之前配置好的,包含自签名证书作为客户端证书
    // 注意:这里的 TLS 握手并不验证服务器证书(因为是匿名/自签名),安全性依赖后续的配对码验证
    val sslContext = key.sslContext
    val sslSocket = sslContext.socketFactory.createSocket(socket, host, port, true) as SSLSocket
    
    // 4. 开始 TLS 握手
    // 阻塞直到握手完成。此时建立了加密通道,但双方还没验证身份(因为都是自签名)
    sslSocket.startHandshake()
    Log.d(TAG, "Handshake succeeded.")

    // 5. 获取输入输出流
    // 后续的所有通信都在这个 TLS 隧道内进行
    inputStream = DataInputStream(sslSocket.inputStream)
    outputStream = DataOutputStream(sslSocket.outputStream)

    // --- 核心安全逻辑开始:生成 SPAKE2 共享密码 ---

    // 6. 将用户输入的配对码转换为字节 (e.g., "123456" -> bytes)
    val pairCodeBytes = pairCode.toByteArray()

    // 7. 导出 TLS 密钥材料 (RFC 5705 Keying Material Exporters)
    // 这是关键! 只有 Conscrypt (BoringSSL/OpenSSL) 支持。
    // 它从 TLS 握手的 master secret 中派生出一段新的密钥数据。
    // 这保证了只有刚才参与了这次特定 TLS 握手的双方才能得到个这数据。
    val keyMaterial = Conscrypt.exportKeyingMaterial(sslSocket, kExportedKeyLabel, null, kExportedKeySize)
    
    // 8. 混合密码:拼接 [配对码] + [TLS密钥材料]
    val passwordBytes = ByteArray(pairCode.length + keyMaterial.size)
    pairCodeBytes.copyInto(passwordBytes) // 先填配对码
    keyMaterial.copyInto(passwordBytes, pairCodeBytes.size) // 再填导出的密钥材料

    // 9. 初始化 SPAKE2 配对上下文 (Native 调用)
    // 使用这个混合后的 "passwordBytes" 初始化 SPAKE2 算法。
    // 只有当双方都有相同的配对码 AND 相同的 TLS 导出密钥时,后续的 doExchangeMsgs 才会成功。
    // 这有效地防止了中间人攻击(MITM),因为中间人即使拦截了配对码,也无法拥有正确的 TLS master secret。
    val pairingContext = PairingContext.create(passwordBytes)
    
    checkNotNull(pairingContext) { "Unable to create PairingContext." }
    this.pairingContext = pairingContext
}

AdbPairingClient 的 start 函数中就完成了和 adb 服务器的信息认证,之后通过 AdbClient 与服务器建立连接之后, 通知栏显示配对完成

启动后台服务

后续启动 shizuku 后 进入主界面点击无线模式的启动之后,会调用 onCreate ,该函数会 初始化 viewModel

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
private class ViewModel(context: Context, root: Boolean, host: String?, port: Int) : androidx.lifecycle.ViewModel() {

    private val sb = StringBuilder()
    private val _output = MutableLiveData<Resource<StringBuilder>>()

    val output = _output as LiveData<Resource<StringBuilder>>

    init {
        try {
            if (root) {
                startRoot()
            } else {
                startAdb(host!!, port)
            }
        } catch (e: Throwable) {
            postResult(e)
        }
    }
 }

而在 viewModel 中调用 startAdb 进行持久化权限维持

 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
 private fun startAdb(host: String, port: Int) {
        sb.append("Starting with wireless adb in port $port...").append('\n').append('\n')
        postResult()

        GlobalScope.launch(Dispatchers.IO) {
            val key = try {
                AdbKey(PreferenceAdbKeyStore(ShizukuSettings.getPreferences()), "shizuku")
            } catch (e: Throwable) {
                e.printStackTrace()
                sb.append('\n').append(Log.getStackTraceString(e))

                postResult(AdbKeyException(e))
                return@launch
            }

            AdbClient(host, port, key).runCatching {
                connect()
                shellCommand(Starter.internalCommand) {
                    sb.append(String(it))
                    postResult()
                }
                close()
            }.onFailure {
                it.printStackTrace()

                sb.append('\n').append(Log.getStackTraceString(it))
                postResult(it)
            }
        }
    }
    
    
    
//   对应的 internalCommand
   
   object Starter {

    private val starterFile = File(application.applicationInfo.nativeLibraryDir, "libshizuku.so")

    val userCommand: String = starterFile.absolutePath

    val adbCommand = "adb shell $userCommand"

    val internalCommand = "$userCommand --apk=${application.applicationInfo.sourceDir}"
}

持久化的方式就是 让 adb shell 去执行 libshizuku.so —apk=”base.apk” 而 libshizuku.so 的代码如下

  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
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
#include <cstdio>
#include <cstdlib>
#include <fcntl.h>
#include <unistd.h>
#include <dirent.h>
#include <ctime>
#include <cstring>
#include <libgen.h>
#include <sys/stat.h>
#include <sys/system_properties.h>
#include <cerrno>
#include <string>
#include <termios.h>
#include "android.h"
#include "misc.h"
#include "selinux.h"
#include "cgroup.h"
#include "logging.h"

#ifdef DEBUG
#define JAVA_DEBUGGABLE
#endif

#define perrorf(...) fprintf(stderr, __VA_ARGS__)

#define EXIT_FATAL_SET_CLASSPATH 3
#define EXIT_FATAL_FORK 4
#define EXIT_FATAL_APP_PROCESS 5
#define EXIT_FATAL_UID 6
#define EXIT_FATAL_PM_PATH 7
#define EXIT_FATAL_KILL 9
#define EXIT_FATAL_BINDER_BLOCKED_BY_SELINUX 10

#define PACKAGE_NAME "moe.shizuku.privileged.api"
#define SERVER_NAME "shizuku_server"
#define SERVER_CLASS_PATH "rikka.shizuku.server.ShizukuService"

#if defined(__arm__)
#define ABI "arm"
#elif defined(__i386__)
#define ABI "x86"
#elif defined(__x86_64__)
#define ABI "x86_64"
#elif defined(__aarch64__)
#define ABI "arm64"
#endif

static void run_server(const char *dex_path, const char *main_class, const char *process_name) {
    if (setenv("CLASSPATH", dex_path, true)) {
        LOGE("can't set CLASSPATH\n");
        exit(EXIT_FATAL_SET_CLASSPATH);
    }

#define ARG(v) char **v = nullptr; \
    char buf_##v[PATH_MAX]; \
    size_t v_size = 0; \
    uintptr_t v_current = 0;
#define ARG_PUSH(v, arg) v_size += sizeof(char *); \
if (v == nullptr) { \
    v = (char **) malloc(v_size); \
} else { \
    v = (char **) realloc(v, v_size);\
} \
v_current = (uintptr_t) v + v_size - sizeof(char *); \
*((char **) v_current) = arg ? strdup(arg) : nullptr;

#define ARG_END(v) ARG_PUSH(v, nullptr)

#define ARG_PUSH_FMT(v, fmt, ...) snprintf(buf_##v, PATH_MAX, fmt, __VA_ARGS__); \
    ARG_PUSH(v, buf_##v)

#ifdef JAVA_DEBUGGABLE
#define ARG_PUSH_DEBUG_ONLY(v, arg) ARG_PUSH(v, arg)
#define ARG_PUSH_DEBUG_VM_PARAMS(v) \
    if (android_get_device_api_level() >= 30) { \
        ARG_PUSH(v, "-Xcompiler-option"); \
        ARG_PUSH(v, "--debuggable"); \
        ARG_PUSH(v, "-XjdwpProvider:adbconnection"); \
        ARG_PUSH(v, "-XjdwpOptions:suspend=n,server=y"); \
    } else if (android_get_device_api_level() >= 28) { \
        ARG_PUSH(v, "-Xcompiler-option"); \
        ARG_PUSH(v, "--debuggable"); \
        ARG_PUSH(v, "-XjdwpProvider:internal"); \
        ARG_PUSH(v, "-XjdwpOptions:transport=dt_android_adb,suspend=n,server=y"); \
    } else { \
        ARG_PUSH(v, "-Xcompiler-option"); \
        ARG_PUSH(v, "--debuggable"); \
        ARG_PUSH(v, "-agentlib:jdwp=transport=dt_android_adb,suspend=n,server=y"); \
    }
#else
#define ARG_PUSH_DEBUG_VM_PARAMS(v)
#define ARG_PUSH_DEBUG_ONLY(v, arg)
#endif

    char lib_path[PATH_MAX]{0};
    snprintf(lib_path, PATH_MAX, "%s/lib/%s", dirname(dex_path), ABI);

    ARG(argv)
    ARG_PUSH(argv, "/system/bin/app_process")
    ARG_PUSH_FMT(argv, "-Djava.class.path=%s", dex_path)
    ARG_PUSH_FMT(argv, "-Dshizuku.library.path=%s", lib_path)
    ARG_PUSH_DEBUG_VM_PARAMS(argv)
    ARG_PUSH(argv, "/system/bin")
    ARG_PUSH_FMT(argv, "--nice-name=%s", process_name)
    ARG_PUSH(argv, main_class)
    ARG_PUSH_DEBUG_ONLY(argv, "--debug")
    ARG_END(argv)

    LOGD("exec app_process");

    if (execvp((const char *) argv[0], argv)) {
        exit(EXIT_FATAL_APP_PROCESS);
    }
}

static void start_server(const char *path, const char *main_class, const char *process_name) {
    pid_t pid = fork();
    switch (pid) {
        case -1: {
            perrorf("fatal: can't fork\n");
            exit(EXIT_FATAL_FORK);
        }
        case 0: {
            LOGD("child");
            setsid();
            chdir("/");
            int fd = open("/dev/null", O_RDWR);
            if (fd != -1) {
                dup2(fd, STDIN_FILENO);
                dup2(fd, STDOUT_FILENO);
                dup2(fd, STDERR_FILENO);
                if (fd > 2) close(fd);
            }
            run_server(path, main_class, process_name);
        }
        default: {
            printf("info: shizuku_server pid is %d\n", pid);
            printf("info: shizuku_starter exit with 0\n");
            exit(EXIT_SUCCESS);
        }
    }
}

static int check_selinux(const char *s, const char *t, const char *c, const char *p) {
    int res = se::selinux_check_access(s, t, c, p, nullptr);
#ifndef DEBUG
    if (res != 0) {
#endif
    printf("info: selinux_check_access %s %s %s %s: %d\n", s, t, c, p, res);
    fflush(stdout);
#ifndef DEBUG
    }
#endif
    return res;
}

static int switch_cgroup() {
    int pid = getpid();
    if (cgroup::switch_cgroup("/acct", pid)) {
        printf("info: switch cgroup succeeded, cgroup in /acct\n");
        return 0;
    }
    if (cgroup::switch_cgroup("/dev/cg2_bpf", pid)) {
        printf("info: switch cgroup succeeded, cgroup in /dev/cg2_bpf\n");
        return 0;
    }
    if (cgroup::switch_cgroup("/sys/fs/cgroup", pid)) {
        printf("info: switch cgroup succeeded, cgroup in /sys/fs/cgroup\n");
        return 0;
    }
    char buf[PROP_VALUE_MAX + 1];
    if (__system_property_get("ro.config.per_app_memcg", buf) > 0 &&
        strncmp(buf, "false", 5) != 0) {
        if (cgroup::switch_cgroup("/dev/memcg/apps", pid)) {
            printf("info: switch cgroup succeeded, cgroup in /dev/memcg/apps\n");
            return 0;
        }
    }
    printf("warn: can't switch cgroup\n");
    fflush(stdout);
    return -1;
}

int main(int argc, char *argv[]) {
    std::string apk_path;
    for (int i = 0; i < argc; ++i) {
        if (strncmp(argv[i], "--apk=", 6) == 0) {
            apk_path = argv[i] + 6;
        }
    }

    uid_t uid = getuid();
    if (uid != 0 && uid != 2000) {
        perrorf("fatal: run Shizuku from non root nor adb user (uid=%d).\n", uid);
        exit(EXIT_FATAL_UID);
    }

    se::init();

    if (uid == 0) {
        switch_cgroup();

        if (android_get_device_api_level() >= 29) {
            printf("info: switching mount namespace to init...\n");
            switch_mnt_ns(1);
        }
    }

    if (uid == 0) {
        char *context = nullptr;
        if (se::getcon(&context) == 0) {
            int res = 0;

            res |= check_selinux("u:r:untrusted_app:s0", context, "binder", "call");
            res |= check_selinux("u:r:untrusted_app:s0", context, "binder", "transfer");

            if (res != 0) {
                perrorf("fatal: the su you are using does not allow app (u:r:untrusted_app:s0) to connect to su (%s) with binder.\n",
                        context);
                exit(EXIT_FATAL_BINDER_BLOCKED_BY_SELINUX);
            }
            se::freecon(context);
        }
    }

    printf("info: starter begin\n");
    fflush(stdout);

    // kill old server
    printf("info: killing old process...\n");
    fflush(stdout);

    foreach_proc([](pid_t pid) {
        if (pid == getpid()) return;

        char name[1024];
        if (get_proc_name(pid, name, 1024) != 0) return;

        if (strcmp(SERVER_NAME, name) != 0)
            return;

        if (kill(pid, SIGKILL) == 0)
            printf("info: killed %d (%s)\n", pid, name);
        else if (errno == EPERM) {
            perrorf("fatal: can't kill %d, please try to stop existing Shizuku from app first.\n", pid);
            exit(EXIT_FATAL_KILL);
        } else {
            printf("warn: failed to kill %d (%s)\n", pid, name);
        }
    });

    if (access(apk_path.c_str(), R_OK) == 0) {
        printf("info: use apk path from argv\n");
        fflush(stdout);
    }

    if (apk_path.empty()) {
        auto f = popen("pm path " PACKAGE_NAME, "r");
        if (f) {
            char line[PATH_MAX]{0};
            fgets(line, PATH_MAX, f);
            trim(line);
            if (strstr(line, "package:") == line) {
                apk_path = line + strlen("package:");
            }
            pclose(f);
        }
    }

    if (apk_path.empty()) {
        perrorf("fatal: can't get path of manager\n");
        exit(EXIT_FATAL_PM_PATH);
    }

    printf("info: apk path is %s\n", apk_path.c_str());
    if (access(apk_path.c_str(), R_OK) != 0) {
        perrorf("fatal: can't access manager %s\n", apk_path.c_str());
        exit(EXIT_FATAL_PM_PATH);
    }

    printf("info: starting server...\n");
    fflush(stdout);
    LOGD("start_server");
    start_server(apk_path.c_str(), SERVER_CLASS_PATH, SERVER_NAME);
}

该代码修改当前进程命名空间和资源空间, 用于避免被系统杀死以及获得全局视角(启动服务器进程和 Init 进程位于同一个命名空间)

 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
    if (uid == 0) {
        switch_cgroup();

        if (android_get_device_api_level() >= 29) {
            printf("info: switching mount namespace to init...\n");
            switch_mnt_ns(1);
        }
    }
    
    static int switch_cgroup() {
    int pid = getpid();
    if (cgroup::switch_cgroup("/acct", pid)) {
        printf("info: switch cgroup succeeded, cgroup in /acct\n");
        return 0;
    }
    if (cgroup::switch_cgroup("/dev/cg2_bpf", pid)) {
        printf("info: switch cgroup succeeded, cgroup in /dev/cg2_bpf\n");
        return 0;
    }
    if (cgroup::switch_cgroup("/sys/fs/cgroup", pid)) {
        printf("info: switch cgroup succeeded, cgroup in /sys/fs/cgroup\n");
        return 0;
    }
    char buf[PROP_VALUE_MAX + 1];
    if (__system_property_get("ro.config.per_app_memcg", buf) > 0 &&
        strncmp(buf, "false", 5) != 0) {
        if (cgroup::switch_cgroup("/dev/memcg/apps", pid)) {
            printf("info: switch cgroup succeeded, cgroup in /dev/memcg/apps\n");
            return 0;
        }
    }
    printf("warn: can't switch cgroup\n");
    fflush(stdout);
    return -1;
}

int switch_mnt_ns(int pid) {
    char mnt[32];
    snprintf(mnt, sizeof(mnt), "/proc/%d/ns/mnt", pid);
    if (access(mnt, R_OK) == -1) return -1;

    int fd = open(mnt, O_RDONLY);
    if (fd < 0) return -1;

    int res = setns(fd, 0);
    close(fd);
    return res;
}

之后就检查 se_linux 规则,是否允许不被信任的 app 进行 binder 通信,如果同意则调用 start_server 启动名为 shizuku_server 的服务器

 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
static void start_server(const char *path, const char *main_class, const char *process_name) {
    pid_t pid = fork();
    switch (pid) {
        case -1: {
            perrorf("fatal: can't fork\n");
            exit(EXIT_FATAL_FORK);
        }
        case 0: {
            LOGD("child");
            setsid();
            chdir("/");
            int fd = open("/dev/null", O_RDWR);
            if (fd != -1) {
                dup2(fd, STDIN_FILENO);
                dup2(fd, STDOUT_FILENO);
                dup2(fd, STDERR_FILENO);
                if (fd > 2) close(fd);
            }
            run_server(path, main_class, process_name);
        }
        default: {
            printf("info: shizuku_server pid is %d\n", pid);
            printf("info: shizuku_starter exit with 0\n");
            exit(EXIT_SUCCESS);
        }
    }
}

启动方式也很简单

  1. fork 创建子进程, 让修改 pid 资源目录 文件描述符等让子进程独立开
  2. 调用 run_server
 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
static void run_server(const char *dex_path, const char *main_class, const char *process_name) {
    if (setenv("CLASSPATH", dex_path, true)) {
        LOGE("can't set CLASSPATH\n");
        exit(EXIT_FATAL_SET_CLASSPATH);
    }

#define ARG(v) char **v = nullptr; \
    char buf_##v[PATH_MAX]; \
    size_t v_size = 0; \
    uintptr_t v_current = 0;
#define ARG_PUSH(v, arg) v_size += sizeof(char *); \
if (v == nullptr) { \
    v = (char **) malloc(v_size); \
} else { \
    v = (char **) realloc(v, v_size);\
} \
v_current = (uintptr_t) v + v_size - sizeof(char *); \
*((char **) v_current) = arg ? strdup(arg) : nullptr;

#define ARG_END(v) ARG_PUSH(v, nullptr)

#define ARG_PUSH_FMT(v, fmt, ...) snprintf(buf_##v, PATH_MAX, fmt, __VA_ARGS__); \
    ARG_PUSH(v, buf_##v)

#ifdef JAVA_DEBUGGABLE
#define ARG_PUSH_DEBUG_ONLY(v, arg) ARG_PUSH(v, arg)
#define ARG_PUSH_DEBUG_VM_PARAMS(v) \
    if (android_get_device_api_level() >= 30) { \
        ARG_PUSH(v, "-Xcompiler-option"); \
        ARG_PUSH(v, "--debuggable"); \
        ARG_PUSH(v, "-XjdwpProvider:adbconnection"); \
        ARG_PUSH(v, "-XjdwpOptions:suspend=n,server=y"); \
    } else if (android_get_device_api_level() >= 28) { \
        ARG_PUSH(v, "-Xcompiler-option"); \
        ARG_PUSH(v, "--debuggable"); \
        ARG_PUSH(v, "-XjdwpProvider:internal"); \
        ARG_PUSH(v, "-XjdwpOptions:transport=dt_android_adb,suspend=n,server=y"); \
    } else { \
        ARG_PUSH(v, "-Xcompiler-option"); \
        ARG_PUSH(v, "--debuggable"); \
        ARG_PUSH(v, "-agentlib:jdwp=transport=dt_android_adb,suspend=n,server=y"); \
    }
#else
#define ARG_PUSH_DEBUG_VM_PARAMS(v)
#define ARG_PUSH_DEBUG_ONLY(v, arg)
#endif

    char lib_path[PATH_MAX]{0};
    snprintf(lib_path, PATH_MAX, "%s/lib/%s", dirname(dex_path), ABI);

    ARG(argv)
    ARG_PUSH(argv, "/system/bin/app_process")
    ARG_PUSH_FMT(argv, "-Djava.class.path=%s", dex_path)
    ARG_PUSH_FMT(argv, "-Dshizuku.library.path=%s", lib_path)
    ARG_PUSH_DEBUG_VM_PARAMS(argv)
    ARG_PUSH(argv, "/system/bin")
    ARG_PUSH_FMT(argv, "--nice-name=%s", process_name)
    ARG_PUSH(argv, main_class)
    ARG_PUSH_DEBUG_ONLY(argv, "--debug")
    ARG_END(argv)

    LOGD("exec app_process");

    if (execvp((const char *) argv[0], argv)) {
        exit(EXIT_FATAL_APP_PROCESS);
    }
}

这里就是通过 execvp 去执行 app_process 启动 com.safe.discipline.server.ResidentServer JAVA进程. 并且这个 JAVA 拥有shell (pid = 200)进程权限. 最终其实就是在 adb shell 中执行如下指令

1
2
3
4
5
6
7
8
9
app_process [java-options] cmd-dir start-class-name [options]

/system/bin/app_process \
  -Djava.class.path=/data/app/~~.../moe.shizuku.manager.../base.apk \
  -Dshizuku.library.path=/data/app/~~.../moe.shizuku.manager.../lib/arm64 \
  /system/bin \
  --nice-name=shizuku_server \
  moe.shizuku.starter.ServiceStarter \
  --debug  # (如果开启了DEBUG)

而 ResidentServer 这里其实就是建立 binder 进行通信(具体内容有空再去分析一下)

自此 shizuku 就已经成功启动带有adb shell 权限的 Java server , 后续其他 app 需要 adb 操作权限只需要通过 binder 和这个 server 进行通信即可.

Xphone 功能


而我自己开发的这个 app 就是基于 shizuku 实现的 自定义 APP 隐藏目前存在如下功能

  1. 快速选择 APP 进行隐藏
  2. 对 APP 进行分类
  3. 指定时间和星期自动隐藏/显示APP
  4. 存在强制模式 在强制模式中显示 app 消耗自定义次数, 并且关闭强制模式时可以开启时延,避免冲动娱乐

下面是带开发的 app 功能, 等什么时候有空的时候再做一下吧

  1. 选中 app 指定时间才显示出来,其余时间一直隐藏
Licensed under CC BY-NC-SA 4.0
comments powered by Disqus