因为最近在用EdXposed,对于magisk和riru很是好奇,之前也大致了解过edxp通过riru实现zygote注入进而完成ART Hook实现类Xposed,但是在准备看源码的时候发现不知道入口在哪,本来想找找有没有现成的大佬总结,发现貌似没有,于是自力更生,从magisk插件开发到riru插件到riru加载逻辑,一步步找到了edxp的代码入口。 Edxp如何编译成一个Magisk插件- module.prop 模块标识
- post-fs-data.sh post-fs-data时所执行的脚本
- service.sh 启动脚本的首选文件
EdXposed在gradle构建出产物时,会将整个项目打包成一个magisk module installer,该installer与magisk module的区别在于installer为一个zip压缩文件,同时包含META-INF文件夹,其中的update-binary.sh将作为安装前执行的脚本。
但是我们在EdXposed中并没有搜到相关的module.prop文件,只能看到module.prop.tpl模版文件,那么这两个文件是如何关联的呢。
在edxp-core的build.gradle中可以看到声明了一系列构建Task,从EdXposed的构建命令./gradlew clean :edxp-core:[zip|push]YahfaRelease开始追踪,
- def zipTask = task("zip${backendCapped}${variantCapped}", type: Zip) {
- dependsOn prepareMagiskFilesTask
- archiveName "${module_name}-${project.version}-${variantLowered}.zip"
- destinationDir file("$projectDir/release")
- from "$zipPathMagiskRelease"
- }
-
- task("push${backendCapped}${variantCapped}", type: Exec) {
- dependsOn zipTask
- workingDir "${projectDir}/release"
- def commands = ["adb", "push",
- "${module_name}-${project.version}-${variantLowered}.zip",
- "/sdcard/"]
- if (is_windows) {
- commandLine 'cmd', '/c', commands.join(" ")
- } else {
- commandLine commands
- }
- }
- }
-
- // backward compatible
- task("zip${variantCapped}") {
- dependsOn "zipYahfa${variantCapped}"
- }
- task("push${variantCapped}") {
- dependsOn "pushYahfa${variantCapped}"
- }
复制代码可以发现 pushTask只是将构建产物推送到设备sdcard下,真正打包任务在zipTask中,其依赖于prepareMagiskFilesTask,该task的作用是: - 复制template_override目录下的META-INF和so文件
- 替换和重写edconfig.tpl和module.prop.tpl,变成真正的module.prop
- def prepareMagiskFilesTask = task("prepareMagiskFiles${backendCapped}${variantCapped}", type: Delete) {
- dependsOn prepareJarsTask, "assemble${variantCapped}"
- delete file(zipPathMagiskRelease)
- doFirst {
- copy {
- from "${projectDir}/tpl/edconfig.tpl"
- into templateFrameworkPath
- rename "edconfig.tpl", "edconfig.jar"
- expand(version: "$version", backend: "$backend")
- }
- copy {
- from "${projectDir}/tpl/module.prop.tpl"
- into templateRootPath
- rename "module.prop.tpl", "module.prop"
- expand(moduleId: "$magiskModuleId", backend: "$backendCapped",
- versionName: "$version",
- versionCode: "$versionCode", authorList: "$authorList")
- filter(FixCrLfFilter.class, eol: FixCrLfFilter.CrLf.newInstance("lf"))
- }
- }
- def libPathRelease = "${buildDir}/intermediates/cmake/${variantLowered}/obj"
- doLast {
- copy {
- from "${projectDir}/template_override"
- into zipPathMagiskRelease
- }
- copy {
- from "$libPathRelease/armeabi-v7a"
- into "$zipPathMagiskRelease/system/lib"
- }
- copy {
- from "$libPathRelease/arm64-v8a"
- into "$zipPathMagiskRelease/system/lib64"
- }
- copy {
- from "$libPathRelease/x86"
- into "$zipPathMagiskRelease/system_x86/lib"
- }
- copy {
- from "$libPathRelease/x86_64"
- into "$zipPathMagiskRelease/system_x86/lib64"
- }
- }
- }
复制代码但是EdXposed作为一个Magisk插件,插件加载时所会执行的update-binary、post-fs-data.sh、customize.sh等脚本中并没有相关执行EdXp的命令,那么EdXposed是如何在Zygote进程加载时所执行呢。
EdXposed实际上还是一个riru插件,即基于riru注入zygote进程时机和riru相关的API声明来实现zygote进程执行时的加载。 Riru模块(插件)编译构建过程Riru模块本质上是一个Magisk插件,额外的地方在于新增了一个目录/data/adb/riru/modules(旧版本在/data/misc/riru/modules)下多了一个插件目录,该目录的作用在于在riru加载时通过该目录名称加载对应的插件so,具体逻辑下面具体分析。
首先看一个Riru模块如何编译构建的,入口依然是build.gradle,即从构建命令zipMagiskMoudle入手: 可以看到主要做了以下操作:
- 复制$rootDir/template/magisk_module
- 复制$rootDir/template/magisk_module/riru.sh,替换其中占位符
- 生成module.prop
- 在riru目录下生成module.prop.new
- 复制native动态链接库
- 生成sha1sum签名
- zip压缩打包
一句话总结:将项目下template/magisk_module下的如动态库、module.prop、post-fs-data.sh等magisk插件相关文件打包成magisk module install所需的zip格式。剩下的就是安装到手机中由Magisk进行加载。 - android.libraryVariants.all { variant ->
- def task = variant.assembleProvider.get()
- task.doLast {
- // clear
- delete { delete magiskDir }
-
- // copy from template
- copy {
- from "$rootDir/template/magisk_module"
- into magiskDir.path
- exclude 'riru.sh'
- }
- // copy riru.sh
- copy {
- from "$rootDir/template/magisk_module"
- into magiskDir.path
- include 'riru.sh'
- filter { line ->
- line.replaceAll('%%%RIRU_MODULE_ID%%%', moduleId)
- .replaceAll('%%%RIRU_MIN_API_VERSION%%%', moduleMinRiruApiVersion.toString())
- .replaceAll('%%%RIRU_MIN_VERSION_NAME%%%', moduleMinRiruVersionName)
- }
- filter(FixCrLfFilter.class,
- eol: FixCrLfFilter.CrLf.newInstance("lf"))
- }
- // copy .git files manually since gradle exclude it by default
- Files.copy(file("$rootDir/template/magisk_module/.gitattributes").toPath(), file("${magiskDir.path}/.gitattributes").toPath())
-
- // generate module.prop
- def modulePropText = ""
- magiskModuleProp.each { k, v -> modulePropText += "$k=$v\n" }
- modulePropText = modulePropText.trim()
- file("$magiskDir/module.prop").text = modulePropText
-
- // generate module.prop for Riru
- def riruModulePropText = ""
- moduleProp.each { k, v -> riruModulePropText += "$k=$v\n" }
- riruModulePropText = riruModulePropText.trim()
- file(riruDir).mkdirs()
-
- // module.prop.new will be renamed to module.prop in post-fs-data.sh
- file("$riruDir/module.prop.new").text = riruModulePropText
-
- // copy native files
- def nativeOutDir = file("build/intermediates/cmake/$variant.name/obj")
-
- file("$magiskDir/system").mkdirs()
- file("$magiskDir/system_x86").mkdirs()
- renameOrFail(file("$nativeOutDir/arm64-v8a"), file("$magiskDir/system/lib64"))
- renameOrFail(file("$nativeOutDir/armeabi-v7a"), file("$magiskDir/system/lib"))
- renameOrFail(file("$nativeOutDir/x86_64"), file("$magiskDir/system_x86/lib64"))
- renameOrFail(file("$nativeOutDir/x86"), file("$magiskDir/system_x86/lib"))
-
- // generate sha1sum
- fileTree("$magiskDir").matching {
- exclude "README.md", "META-INF"
- }.visit { f ->
- if (f.directory) return
- file(f.file.path + ".sha256sum").text = calcSha256(f.file)
- }
- }
- task.finalizedBy zipMagiskMoudle
- }
-
- task zipMagiskMoudle(type: Zip) {
- from magiskDir
- archiveName zipName
- destinationDir outDir
- }
复制代码 模块加载过程一个典型的riru插件的加载顺序可以简单理解为:
update_binary -> customize.sh -> post-fs-data.sh
其中customize.sh是riru重写update_binary进行执行的,其他都是遵循magisk的插件执行顺序。
接下来就通过这些shell脚本的执行来找到riru是如何定位插件、解析插件、执行插件和完成zygote注入的。
首先看riru是如何被magisk执行的,因为riru也是一个magisk插件,所以直接先看post-fs-data.sh,可以看到:
- LIBRARIES_FILE='/system/etc/public.libraries.txt'
- mkdir -p "$MODDIR/system/etc"
- cp -f $LIBRARIES_FILE "$MODDIR/$LIBRARIES_FILE"
- grep -qxF 'libriru.so' "$MODDIR/$LIBRARIES_FILE" || echo 'libriru.so' >> "$MODDIR/$LIBRARIES_FILE"
复制代码 这个版本的riru直接通过/system/etc/public.libraries.txt自动完成so的dlopen加载。
- PS:
-
- riru目前存在了三种已知的app_process注入实现:
- 1. 最早通过替换libmemtrack.so
- 2. 后来通过/system/etc/public.libraries.txt
- 3. 目前通过native bridge即设置系统属性ro.dalvik.vm.native.bridge
复制代码因此可以直接奔向riru的so,找到.initarray方法(\_attribute__((constructor))),可以看到两个关键方法调用,分别为 - <blockquote><div align="left">XHOOK_REGISTER(".*\libandroid_runtime.so$", jniRegisterNativeMethods);</div>
复制代码
和
具体.init_array代码可以看main.cpp中:- extern "C" void constructor() __attribute__((constructor));
-
- // _init_array libriru.so被dlopen后最先执行的函数
- void constructor() {
- #ifdef DEBUG_APP
- hide::hide_modules(nullptr, 0);
- #endif
-
- if (getuid() != 0)
- return;
-
- char cmdline[ARG_MAX + 1];
- get_self_cmdline(cmdline, 0);
-
- if (strcmp(cmdline, "zygote") != 0
- && strcmp(cmdline, "zygote32") != 0
- && strcmp(cmdline, "zygote64") != 0
- && strcmp(cmdline, "usap32") != 0
- && strcmp(cmdline, "usap64") != 0) {
- LOGW("not zygote (cmdline=%s)", cmdline);
- return;
- }
-
- LOGI("Riru %s (%d) in %s", RIRU_VERSION_NAME, RIRU_VERSION_CODE, cmdline);
-
- LOGI("config dir is %s", CONFIG_DIR);
-
- if (access(CONFIG_DIR "/disable", F_OK) == 0) {
- LOGI("%s exists, do nothing", CONFIG_DIR "/disable");
- return;
- }
-
- read_prop();
-
- // 通过GOT表hook libandroid_runtime.so中对jniRegisterNativeMethods方法的调用,因为libandroid_runtime.so中所有JNI方法都是通过该方法进行注册,然后再通过手动调用registeNatives来替换
- // 因此通过hook该方法可以在com.android.internal.os.Zygote#nativeForkAndSpecialize和com.android.internal.os.Zygote#nativeForkSystemServer注册时进行替换
- // Riru也因此完成了zygote进程的注入
- XHOOK_REGISTER(".*\\libandroid_runtime.so$", jniRegisterNativeMethods);
-
- if (xhook_refresh(0) == 0) {
- xhook_clear();
- LOGI("hook installed");
- } else {
- LOGE("failed to refresh hook");
- }
-
- // 加载插件
- load_modules();
-
- status::writeToFile();
- }
复制代码 XHOOK_REGISTER这里XHOOK_REGISTER是一个宏定义, - XHOOK_REGISTER(".*\libandroid_runtime.so$", jniRegisterNativeMethods);
复制代码 实际上相当于
- if (xhook_register(".*\\libandroid_runtime.so$", jniRegisterNativeMethods, (void*)
复制代码 new_jniRegisterNativeMethods和old_jniRegisterNativeMethods则是通过NEW_FUNC_DEF宏定义来进行声明:
- #define NEW_FUNC_DEF(ret, func, ...) \
- static ret (*old_##func)(__VA_ARGS__); \
- static ret new_##func(__VA_ARGS__)
-
- NEW_FUNC_DEF(int, jniRegisterNativeMethods, JNIEnv *env, const char *className,
- const JNINativeMethod *methods, int numMethods) {
- api::putNativeMethod(className, methods, numMethods);
-
- LOGD("jniRegisterNativeMethods %s", className);
-
- JNINativeMethod *newMethods = nullptr;
- if (strcmp("com/android/internal/os/Zygote", className) == 0) {
- // com/android/internal/os/Zygote注册时回调onRegisterZygote方法获取新的jniMethods列表进行替换
- newMethods = onRegisterZygote(env, className, methods, numMethods);
- } else if (strcmp("android/os/SystemProperties", className) == 0) {
- // hook android.os.SystemProperties#native_set to prevent a critical problem on Android 9
- // see comment of SystemProperties_set in jni_native_method.cpp for detail
- // 回调onRegisterSystemProperties方法
- newMethods = onRegisterSystemProperties(env, className, methods, numMethods);
- }
-
- int res = old_jniRegisterNativeMethods(env, className, newMethods ? newMethods : methods,
- numMethods);
- /*if (!newMethods) {
- NativeMethod::jniRegisterNativeMethodsPost(env, className, methods, numMethods);
- }*/
- delete newMethods;
- return res;
- }
复制代码可以看到在com/android/internal/os/Zygote注册JNI方法时会回调onRegisterZygote方法获取新的jniMethods列表进行替换,在onRegisterZygote方法中主要替换了三个方法的fnPtr指针并兼容不同的安卓版本: - nativeForkAndSpecialize
- nativeSpecializeAppProcess
- nativeForkSystemServer
这三个方法是应用进程或者系统服务进程被fork 出来的时候会调用的方法,这里以nativeForkAndSpecialize举例分析nativeForkAndSpecialize的AOP逻辑和模块中声明的forkAndSpecializePre/Post等系列方法如何被调用及调用时机。 - jint nativeForkAndSpecialize_r(
- JNIEnv *env, jclass clazz, jint uid, jint gid, jintArray gids, jint runtime_flags,
- jobjectArray rlimits, jint mount_external, jstring se_info, jstring se_name,
- jintArray fdsToClose, jintArray fdsToIgnore, jboolean is_child_zygote,
- jstring instructionSet, jstring appDataDir, jboolean isTopApp, jobjectArray pkgDataInfoList,
- jobjectArray whitelistedDataInfoList, jboolean bindMountAppDataDirs, jboolean bindMountAppStorageDirs) {
-
- // 通过nativeForkAndSpecialize_pre和nativeForkAndSpecialize_post完成了nativeForkAndSpecialize方法的AOP
- nativeForkAndSpecialize_pre(env, clazz, uid, gid, gids, runtime_flags, rlimits, mount_external,
- se_info, se_name, fdsToClose, fdsToIgnore, is_child_zygote,
- instructionSet, appDataDir, isTopApp, pkgDataInfoList, whitelistedDataInfoList,
- bindMountAppDataDirs, bindMountAppStorageDirs);
-
- jint res = ((nativeForkAndSpecialize_r_t *) JNI::Zygote::nativeForkAndSpecialize->fnPtr)(
- env, clazz, uid, gid, gids, runtime_flags, rlimits, mount_external, se_info, se_name,
- fdsToClose, fdsToIgnore, is_child_zygote, instructionSet, appDataDir, isTopApp, pkgDataInfoList,
- whitelistedDataInfoList, bindMountAppDataDirs, bindMountAppStorageDirs);
-
- nativeForkAndSpecialize_post(env, clazz, uid, res);
- return res;
- }
-
- static void nativeForkAndSpecialize_pre(
- JNIEnv *env, jclass clazz, jint &uid, jint &gid, jintArray &gids, jint &runtime_flags,
- jobjectArray &rlimits, jint &mount_external, jstring &se_info, jstring &se_name,
- jintArray &fdsToClose, jintArray &fdsToIgnore, jboolean &is_child_zygote,
- jstring &instructionSet, jstring &appDataDir, jboolean &isTopApp, jobjectArray &pkgDataInfoList,
- jobjectArray &whitelistedDataInfoList, jboolean &bindMountAppDataDirs, jboolean &bindMountAppStorageDirs) {
-
- // 遍历执行每个模块的forkAndSpecializePre方法,这里可以知道,只需要在模块中声明forkAndSpecializePre方法,即可在com.android.internal.os.Zygote#forkAndSpecialize方法执行前被调用
- for (auto module : *get_modules()) {
- if (!module->hasForkAndSpecializePre())
- continue;
-
- if (module->hasShouldSkipUid() && module->shouldSkipUid(uid))
- continue;
-
- if (!module->hasShouldSkipUid() && shouldSkipUid(uid))
- continue;
-
- module->forkAndSpecializePre(
- env, clazz, &uid, &gid, &gids, &runtime_flags, &rlimits, &mount_external,
- &se_info, &se_name, &fdsToClose, &fdsToIgnore, &is_child_zygote,
- &instructionSet, &appDataDir, &isTopApp, &pkgDataInfoList, &whitelistedDataInfoList,
- &bindMountAppDataDirs, &bindMountAppStorageDirs);
- }
- }
复制代码 load_modulesload_modules解析如下: - void load_modules() {
- DIR *dir;
- struct dirent *entry;
- char path[PATH_MAX];
- void *handle;
- const int riruApiVersion = RIRU_API_VERSION;
-
- if (!(dir = _opendir(MODULES_DIR))) return;
-
- // 遍历/data/adb/riru/modules目录下的文件夹
- while ((entry = _readdir(dir))) {
- if (entry->d_type != DT_DIR) continue;
-
- // 获取文件夹名称
- auto name = entry->d_name;
- if (name[0] == '.') continue;
-
- // 根据文件夹名称拼接so路径:/system/lib/libriru_%s.so,这一步操作也是在riru插件的customize.sh中完成
- snprintf(path, PATH_MAX, MODULE_PATH_FMT, name);
-
- if (access(path, F_OK) != 0) {
- PLOGE("access %s", path);
- continue;
- }
-
- handle = dlopen(path, 0);
- if (!handle) {
- LOGE("dlopen %s failed: %s", path, dlerror());
- continue;
- }
-
- // 找到so的init方法,这里也是为什么riru的官方文档中指出必须要有一个导出的init函数
- // 如果没有init方法,则将不会被认为是一个合法的riru module,后续get_modules也无法获取到
- auto init = (RiruInit_t *) dlsym(handle, "init");
- if (!init) {
- LOGW("%s does not export init", path);
- cleanup(handle, path);
- continue;
- }
-
- // 1. pass riru api version, return module's api version
- auto apiVersion = (int *) init((void *) &riruApiVersion);
- if (apiVersion == nullptr) {
- LOGE("%s returns null on step 1", path);
- cleanup(handle, path);
- continue;
- }
-
- if (*apiVersion < RIRU_MIN_API_VERSION || *apiVersion > RIRU_API_VERSION) {
- LOGW("unsupported API %s: %d", name, *apiVersion);
- cleanup(handle, path);
- continue;
- }
-
- // 2. create and pass Riru struct by module's api version
- auto module = new RiruModule(strdup(name));
- module->handle = handle;
- module->apiVersion = *apiVersion;
-
- if (*apiVersion == 9) {
- auto info = init_module_v9(module->token, init);
- if (info == nullptr) {
- LOGE("%s returns null on step 2", path);
- cleanup(handle, path);
- continue;
- }
- module->info(info);
- }
-
- // 3. let the module to do some cleanup jobs
- init(nullptr);
-
- // 缓存插件信息到一个vector中,后续通过get_modules()方法才能找到对应的插件信息
- get_modules()->push_back(module);
-
- LOGI("module loaded: %s (api %d)", module->name, module->apiVersion);
- }
-
- closedir(dir);
-
- status::getStatus()->hideEnabled = access(ENABLE_HIDE_FILE, F_OK) == 0;
- if (status::getStatus()->hideEnabled) {
- LOGI("hide is enabled");
- auto modules = get_modules();
- auto names = (const char **) malloc(sizeof(char *) * modules->size());
- int names_count = 0;
- for (auto module : *get_modules()) {
- if (strcmp(module->name, MODULE_NAME_CORE) == 0) continue;
- if (!module->supportHide) {
- LOGI("module %s does not support hide", module->name);
- continue;
- }
- names[names_count] = module->name;
- names_count += 1;
- }
- hide::hide_modules(names, names_count);
- } else {
- PLOGE("access " ENABLE_HIDE_FILE);
- LOGI("hide is not enabled");
- }
-
- for (auto module : *get_modules()) {
- if (module->hasOnModuleLoaded()) {
- LOGV("%s: onModuleLoaded", module->name);
- // 回调module的_onModuleLoaded方法
- module->onModuleLoaded();
- }
- }
- }
复制代码
EdXposed如何依赖riruEdXposed本身也是一个riru插件,但是如何证明呢。
我们可以知道如果作为一个riru插件,必然需要 - 在/data/adb/riru/modules或者/data/misc/riru/modules下存在一个插件文件夹。
- 存在与插件文件夹相同名称的/system/lib/libriru_%s.so
- so中可能有init或者forkAndSpecializePre等zygote进程相关的方法
在edxp-core中,依然通过build.gradle为入口分析zip打包的逻辑,可以看到也是一个标准的magisk module installer,其中customize.sh中有一行关键代码创建了riru插件的相关目录:
- # 创建riru的module目录,这里可以看出edxp同时是一个riru module,riru通过检测目录加载对应的libriru_xxx.so
- [[ -d "${RIRU_TARGET}" ]] || mkdir -p "${RIRU_TARGET}" || abort "! Can't mkdir -p ${RIRU_TARGET}"
-
- # RIRU_TARGET变量的赋值过程
- RIRU_PATH="/data/misc/riru"
- # 为libriru_edxp.so设置随机后缀, libriru_edxp.so -> libriru_{randomNum}.so
- # libriru_edxp.so由edxp-core:src/main/cpp/main相关c文件编译得出
- # getRandomNameExist方法作用为获取一个/proc/sys/kernel/random/uuid不存在的四位随机数
- RIRU_EDXP="$(getRandomNameExist 4 "libriru_" ".so" "
- /system/lib
- /system/lib64
- ")"
- RIRU_MODULES="${RIRU_PATH}/modules"
- RIRU_TARGET="${RIRU_MODULES}/${RIRU_EDXP}"
复制代码通过这段脚本可以看出edxp插件在magisk加载时创建了一个riru插件目录,并且把libriru_edxp.so重命名成一个随机后缀的so文件由riru进行加载。
接着分析libriru_edxp.so,通过CMakeLists.txt可以知道是由edxp-core中编译得到,查看edxp-core/src/main/cpp/main/src/main.cpp中可以看到确实声明了riru中的一些方法模版钩子: - nativeForkAndSpecializePre
- nativeForkAndSpecializePost
- nativeForkSystemServerPre
- nativeForkSystemServerPost
- specializeAppProcessPre
- specializeAppProcessPost
- onModuleLoaded
- shouldSkipUid
等方法,一切都可以想明白了。接下来就是edxp如何进行ART Hook。 PS
|