Zygisk-v27.0源码阅读

注意
本文最后更新于 2024-03-06,文中内容可能已过时。

隔了很久再读Magisk源码中关于Zygisk的部分,上次翻源码还是v25.0,这次已经更新到了v27.0。粗略扫了眼,变化的地方还是挺多的,想搜索一下关键字也基本上搜索不到,懒得重新过一遍源码,既然是关于zygisk,那就以(zygisk_enabled)作为关键搜索词切入

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
void load_modules() {
    ......

    if (zygisk_enabled) {
        string native_bridge_orig = get_prop(NBPROP);
        if (native_bridge_orig.empty()) {
            native_bridge_orig = "0";
        }
        native_bridge = native_bridge_orig != "0" ? ZYGISKLDR + native_bridge_orig : ZYGISKLDR;
        set_prop(NBPROP, native_bridge.data());
        // Weather Huawei's Maple compiler is enabled.
        // If so, system server will be created by a special Zygote which ignores the native bridge
        // and make system server out of our control. Avoid it by disabling.
        if (get_prop("ro.maple.enable") == "1") {
            set_prop("ro.maple.enable", "0");
        }
        inject_zygisk_libs(system);
    }
    ......
}

定位到load_modules函数,发现这里竟然使用了native_bridge_orig,对应的变量名也做了个效果,NBPROP?

1
#define NBPROP          "ro.dalvik.vm.native.bridge"

这是也要仿照riru了吗?

继续load_modules,ZYGISKLDR对应的是libzygisk.so,也就是src/core/zygisk目录,从之前的riru原理理解这篇文章中,已经知道对于native_bridge的使用大概流程如下

  • 调用LoadNativeBridge函数
  • dlopen native_bridge对应的so动态库
  • dlsym kNativeBridgeInterfaceSymbol获取callbacks,kNativeBridgeInterfaceSymbol的值是NativeBridgeItf
  • 调用isCompatibleWith处理

对应的看下Zygisk相对应的实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// src/core/zygisk/entry.cpp

extern "C" [[maybe_unused]] NativeBridgeCallbacks NativeBridgeItf{
    .version = 2,
    .padding = {},
    .isCompatibleWith = &is_compatible_with,
};

static bool is_compatible_with(uint32_t) {
    zygisk_logging();
    hook_functions();
    ZLOGD("load success\n");
    return false;
}

这里注意两点:

  1. hook_functions根据之前版本的的Zygisk实现来看,应该就是JNI hook的地方
  2. 既然是JNI hook,那么表明Zygisk并没有像riru那样存在中转的riruloader.so,相当于直接调用了riru.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
// src/core/zygisk/hook.cpp

void hook_functions() {
    default_new(g_hook);
    g_hook->hook_plt();
}

void HookContext::hook_plt() {
    ino_t android_runtime_inode = 0;
    dev_t android_runtime_dev = 0;
    ino_t native_bridge_inode = 0;
    dev_t native_bridge_dev = 0;

    for (auto &map : lsplt::MapInfo::Scan()) {
        if (map.path.ends_with("/libandroid_runtime.so")) {
            android_runtime_inode = map.inode;
            android_runtime_dev = map.dev;
        } else if (map.path.ends_with("/libnativebridge.so")) {
            native_bridge_inode = map.inode;
            native_bridge_dev = map.dev;
        }
    }

    PLT_HOOK_REGISTER(native_bridge_dev, native_bridge_inode, dlclose);
    PLT_HOOK_REGISTER(android_runtime_dev, android_runtime_inode, fork);
    PLT_HOOK_REGISTER(android_runtime_dev, android_runtime_inode, unshare);
    PLT_HOOK_REGISTER(android_runtime_dev, android_runtime_inode, androidSetCreateThreadFunc);
    PLT_HOOK_REGISTER(android_runtime_dev, android_runtime_inode, selinux_android_setcontext);
    PLT_HOOK_REGISTER_SYM(android_runtime_dev, android_runtime_inode, "__android_log_close", android_log_close);

    if (!lsplt::CommitHook())
        ZLOGE("plt_hook failed\n");

    // Remove unhooked methods
    plt_backup.erase(
            std::remove_if(plt_backup.begin(), plt_backup.end(),
            [](auto &t) { return *std::get<3>(t) == nullptr;}),
            g_hook->plt_backup.end());
}

替换了XHOOK框架使用了自己实现的PLT_HOOK,对应的这个项目LSPlt

  1. fork机制和之前是相同的,提前fork
  2. unshare对于新的namespace划分时unmount掉其中的Magisk特征
  3. androidSetCreateThreadFunc
     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
    
    DCL_HOOK_FUNC(static void, androidSetCreateThreadFunc, void *func) {
        ZLOGD("androidSetCreateThreadFunc\n");
        g_hook->hook_jni_env();
        old_androidSetCreateThreadFunc(func);
    }
    
    void HookContext::hook_jni_env() {
        using method_sig = jint(*)(JavaVM **, jsize, jsize *);
        auto get_created_vms = reinterpret_cast<method_sig>(
                dlsym(RTLD_DEFAULT, "JNI_GetCreatedJavaVMs"));
        if (!get_created_vms) {
            for (auto &map: lsplt::MapInfo::Scan()) {
                if (!map.path.ends_with("/libnativehelper.so")) continue;
                void *h = dlopen(map.path.data(), RTLD_LAZY);
                if (!h) {
                    ZLOGW("Cannot dlopen libnativehelper.so: %s\n", dlerror());
                    break;
                }
                get_created_vms = reinterpret_cast<method_sig>(dlsym(h, "JNI_GetCreatedJavaVMs"));
                dlclose(h);
                break;
            }
            if (!get_created_vms) {
                ZLOGW("JNI_GetCreatedJavaVMs not found\n");
                return;
            }
        }
    
        JavaVM *vm = nullptr;
        jsize num = 0;
        jint res = get_created_vms(&vm, 1, &num);
        if (res != JNI_OK || vm == nullptr) {
            ZLOGW("JavaVM not found\n");
            return;
        }
        JNIEnv *env = nullptr;
        res = vm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6);
        if (res != JNI_OK || env == nullptr) {
            ZLOGW("JNIEnv not found\n");
            return;
        }
    
        // Replace the function table in JNIEnv to hook RegisterNatives
        memcpy(&new_env, env->functions, sizeof(*env->functions));
        new_env.RegisterNatives = &env_RegisterNatives;
        old_env = env->functions;
        env->functions = &new_env;
    }
    
    static jint env_RegisterNatives(
            JNIEnv *env, jclass clazz, const JNINativeMethod *methods, jint numMethods) {
        auto className = get_class_name(env, clazz);
        if (className == "com/android/internal/os/Zygote") {
            // Restore JNIEnv as we no longer need to replace anything
            env->functions = g_hook->old_env;
    
            vector<JNINativeMethod> newMethods(methods, methods + numMethods);
            vector<JNINativeMethod> &backup = g_hook->jni_backup[className];
            HOOK_JNI(nativeForkAndSpecialize);
            HOOK_JNI(nativeSpecializeAppProcess);
            HOOK_JNI(nativeForkSystemServer);
            return g_hook->old_env->RegisterNatives(env, clazz, newMethods.data(), numMethods);
        } else {
            return g_hook->old_env->RegisterNatives(env, clazz, methods, numMethods);
        }
    }
    
    这里就不多说了,还是熟系的配方

Magisk团队找了更直接的方法来修改sepolicy,像他们在details.md所说的

1
2
3
4
5
6
7
8
9
## Pre-Init

`magiskinit` will replace `init` as the first program to run.

- Early mount required partitions. On legacy system-as-root devices, we switch root to system; on 2SI devices, we patch the original `init` to redirect the 2nd stage init file to magiskinit and execute it to mount partitions for us.
- Inject magisk services into `init.rc`
- On devices using monolithic policy, load sepolicy from `/sepolicy`; otherwise we hijack nodes in selinuxfs with FIFO, set `LD_PRELOAD` to hook `security_load_policy` and assist hijacking on 2SI devices, and start a daemon to wait until init tries to load sepolicy.
- Patch sepolicy rules. If we are using "hijack" method, load patched sepolicy into kernel, unblock init and exit daemon
- Execute the original `init` to continue the boot process

使用LD_PRELOAD hook security_load_policy

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// src/init/selinux.cpp

if (access("/system/bin/init", F_OK) == 0) {
    // On 2SI devices, the 2nd stage init file is always a dynamic executable.
    // This meant that instead of going through convoluted methods trying to alter
    // and block init's control flow, we can just LD_PRELOAD and replace the
    // security_load_policy function with our own implementation.
    dump_preload();
    setenv("LD_PRELOAD", "/dev/preload.so", 1);
}

preload.so对应的是preload.c

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// src/init/preload.c

static void preload_init() {
    // Make sure our next exec won't get bugged
    unsetenv("LD_PRELOAD");
    unlink("/dev/preload.so");
}

int security_load_policy(void *data, size_t len) {
    int (*load_policy)(void *, size_t) = dlsym(RTLD_NEXT, "security_load_policy");
    // Skip checking errors, because if we cannot find the symbol, there
    // isn't much we can do other than crashing anyways.
    int result = load_policy(data, len);

    // Wait for ack
    int fd = open("/sys/fs/selinux/enforce", O_RDONLY);
    char c;
    read(fd, &c, 1);
    close(fd);

    return result;
}

在使用monolithic策略的设备上,Magisk直接从/sepolicy文件中加载sepolicy规则。这个文件通常位于系统的根目录下,用于存储selinux策略。这种方式比较简单直接,不需要进行额外的hook操作

但是在其他的设备上,Magisk使用FIFO(命名管道)劫持selinuxfs中的节点,以实现selinux hook。具体来说,Magisk会创建一个FIFO文件,并挂载到selinuxfs中的"load"和"enforce"节点上,用于接收selinux策略和enforce值。这样一来,即使系统中没有/sepolicy文件,Magisk也可以通过劫持selinuxfs中的节点,来实现selinux hook

在2SI设备上,由于第二阶段的init文件是一个动态可执行文件,而不是静态的/init可执行文件,因此Magisk还需要协助劫持selinuxfs。具体来说,Magisk会在init进程启动之前,通过LD_PRELOAD的方式,将自己的preload.so库注入到init进程中,并替换security_load_policy函数为自己的实现,以实现selinux hook。后面Magisk启动守护程序,等待init进程尝试加载selinux策略文件。当init进程启动时,Magisk的钩子函数会拦截security_load_policy的调用,并将selinux策略文件和enforce值写入FIFO文件中,以实现自定义的selinux策略

相关内容