首页
社区
课程
招聘
[原创]小菜花的Riru原理浅析和源码阅读
发表于: 2021-11-8 14:49 14084

[原创]小菜花的Riru原理浅析和源码阅读

2021-11-8 14:49
14084

一,引言

因为最近在研究在svc bypass,frida脚本就随便写了
落地到生产环境,首先用的是xposed + inlinehook,就是用xposed hook runtime的loadLibrary,当发现目标so load的时候,就开始主动load我们的inlinehook so,inlinehook so中的逻辑就是,对目标so进行svc指令内存扫描,然后对扫描到的svc地址进行hook操作

那么问题来了,这个inlinehook so的load时机如何选:
1,after loadLibrary(目标so),这个时机的话,目标so在dlopen中都执行完init和init_arrary了,如果检测放在init中刚好人家检测完,你才注入inlinehook so,那时机就太晚了。

2,before loadLibrary(目标so),这个时机的话,你inlinehook so工作原理就是需要在内存中扫描目标so然后再hook,此时目标so还没加载,根本拿不到地址没发扫描啊,所以这个时机也不行。(ps:或许主动去getSymFromLinker(find_libraries),然后主动调用find_libraries去加载链接目标so?这个思路是写文章的时候想到的,未进行尝试,因为就想玩riru,手动狗头(ps:find_libraries是笔者阅读源码android8.1选取的装载了so却没执行init函数的一个时机))

综上所述,两个时机都不太行

那么来看 https://bbs.pediy.com/thread-268256.htm ,该文章选取的注入frida-gadget的时机点是com_android_internal_os_Zygote_nativeForkAndSpecialize,该函数是fork应用程序进程的,这个时机就比较好,fork好应用程序进程,咱们就开始hook linker的find_libraries,等目标so加载链接好之后,就开始扫描内存,对svc address进行hook操作

一切就是这么的顺其自然,嗯,大致就这个思路吧。

要对nativeForkAndSpecialize进行动手,首先应该想到的是magisk的riru模块了,riru模块提供了下面三个函数的注入时机

  • nativeForkAndSpecialize
  • nativeSpecializeAppProcess
  • nativeForkSystemServer

提供了pre和post函数,就可以随便玩了。那么开始动手搞riru模块开发,本着多多学习的原则,来看下riru源码和原理,

看源码是为了更好的cv,看原理是为了知其所以然,然后达到一个理所当然的状态。


二,Magisk

看riru之前,肯定要看下Magisk了,因为riru属于magisk模块,需要大致知道magisk是咋回事。

先来看下magisk官方文档:https://topjohnwu.github.io/Magisk/guides.html


关键点看下来就是:

1,magisk会在两个时机执行post-fs-data.sh和service.sh

2,  system.prop会被magisk加载作为系统属性

3,没特殊设置的话,magisk会挂载模块内的system文件夹,这里是存放replace/inject files的


三,Riru

Riru原理

老规矩,先来git看下readme: https://github.com/RikkaApps/Riru

其实这里就已经把riru原理介绍完了,大致总结下就是:

1,注入原理:动了ro.dalvik.vm.native.bridge,可以被系统自动dlopen特定so文件

2,hook原理:hook了android_runtime的jniRegisterNativeMethods对关键jni函数做了替换(ps:系统启动的时候先启动init进程,init进程再启动zygote进程,而zygote进程会创建java虚拟机,并为其注册jni方法)


注入原理

那么来详细看下注入原理的由来:https://blog.canyie.top/2020/08/18/nbinjection/,大致总结下就是:

1,作者分析源码的时候发现,在startVM函数流程中Runtime::Init会加载native bridge

2,LoadNativeBridge会dlopen了一个特定的so

3,这个特定的so的由来是系统属性ro.dalvik.vm.native.bridge的值

4,有特殊情况native bridge的so会被系统卸载掉,所以直接当个loader去load其他的so比较好


riru zip包分析

从git上下载riru release包解压下来看看


这几个脚本文件相对生疏一点,回过头来看下magisk文档


再打开这些脚本文件看下大致内容:

util_functions.sh 是搞了一些funtion ui_print

verify.sh 是校验文件完整性的

update-binary和customize.sh都是做一些安装过程准备的,涉及一些检查环境,解压,删除文件啥的


稍微关键点的是,搞了个文件夹,移动了一下文件,记住这个$(magisk --path)/.magisk/modules/riru-core目录,来设备中看下大致长啥样

这下清晰多了,文件结构就和上文Magisk Modules介绍的一摸一样,那么根据上文介绍的magisk情况,可以明确的是:

1,system.prop会被magisk加载作为系统属性,当前system.prop的内容为

ro.dalvik.vm.native.bridge=libriruloader.so,嘿,这不注入原理和上面说的对应上了。

2,magisk会在两个时机执行post-fs-data.sh和service.sh,打开看下这两个文件有没有啥关键信息



稍微关键点的好像就这句了:

flock "module.prop"
unshare -m sh -c "/system/bin/app_process -Djava.class.path=rirud.apk /system/bin --nice-name=rirud riru.Daemon $(magisk -V) $(magisk --path) $(getprop ro.dalvik.vm.native.bridge)&"

那就来源码rirud riru.Daemon看下干了嘛


private void onRiruLoad() {
    allowRestart = true;
    Log.i(TAG, "Riru loaded, reset native bridge to " + DaemonUtils.getOriginalNativeBridge() + "...");
    DaemonUtils.resetNativeBridgeProp(DaemonUtils.getOriginalNativeBridge());

    Log.i(TAG, "Riru loaded, stop rirud socket...");
    serverThread.stopServer();

    var loadedModules = DaemonUtils.getLoadedModules().toArray();

    StringBuilder sb = new StringBuilder();
    if (loadedModules.length == 0) {
        sb.append(DaemonUtils.res.getString(R.string.empty));
    } else {
        sb.append(loadedModules[0]);
        for (int i = 1; i < loadedModules.length; ++i) {
            sb.append(", ");
            sb.append(loadedModules[i]);
        }
    }

    if (DaemonUtils.hasIncorrectFileContext()) {
        DaemonUtils.writeStatus(R.string.bad_file_context_loaded, loadedModules.length, sb);
    } else {
        DaemonUtils.writeStatus(R.string.loaded, loadedModules.length, sb);
    }
}

大致看了下,好像就是搞了个socket 暴露了一些api用来进程通信了然后还有一些环境操作,比如riru load之后把native bridge设置会原来的,那就大致先这样吧。


riru loader源码分析

回过头来看system.prop的native bridge的libriruloader.so,根据riru/src/main/cpp/CMakeLists.txt可知,其入口为riru/src/main/cpp/loader/loader.cpp,关键代码如下

__used __attribute__((constructor)) void Constructor() {
    if (getuid() != 0) {
        return;
    }

    std::string_view cmdline = getprogname();

    if (cmdline != "zygote" &&
        cmdline != "zygote32" &&
        cmdline != "zygote64" &&
        cmdline != "usap32" &&
        cmdline != "usap64") {
        LOGW("not zygote (cmdline=%s)", cmdline.data());
        return;
    }

    LOGI("Riru %s (%d) in %s", riru::versionName, riru::versionCode, cmdline.data());
    LOGI("Android %s (api %d, preview_api %d)", android_prop::GetRelease(),
         android_prop::GetApiLevel(),
         android_prop::GetPreviewApiLevel());

    constexpr auto retries = 5U;
    RirudSocket rirud{retries};

    if (!rirud.valid()) {
        LOGE("rirud connect fails");
        return;
    }

    std::string magisk_path = rirud.ReadMagiskTmpfsPath();
    if (magisk_path.empty()) {
        LOGE("failed to obtain magisk path");
        return;
    }

    BuffString<PATH_MAX> riru_path;
    riru_path += magisk_path;
    riru_path += "/.magisk/modules/riru-core/lib";
#ifdef __LP64__    riru_path += "64";#endif    riru_path += "/libriru.so";

    auto *handle = DlopenExt(riru_path, 0);
    if (handle) {
        auto init = reinterpret_cast<void (*)(void *, const char *, const RirudSocket &)>(dlsym(
                handle, "init"));
        if (init) {
            init(handle, magisk_path.data(), rirud);
        } else {
            LOGE("dlsym init %s", dlerror());
        }
    } else {
        LOGE("dlopen riru.so %s", dlerror());
    }

#ifdef HAS_NATIVE_BRIDGE    auto native_bridge = rirud.ReadNativeBridge();
    if (native_bridge.empty()) {
        LOGW("Failed to read original native bridge from socket");
        return;
    }

    LOGI("original native bridge: %s", native_bridge.data());

    if (native_bridge == "0") {
        return;
    }

    original_bridge = dlopen(native_bridge.data(), RTLD_NOW);
    if (original_bridge == nullptr) {
        LOGE("dlopen failed: %s", dlerror());
        return;
    }

    auto *original_native_bridge_itf = dlsym(original_bridge, "NativeBridgeItf");
    if (original_native_bridge_itf == nullptr) {
        LOGE("dlsym failed: %s", dlerror());
        return;
    }

    int sdk = 0;
    std::array<char, PROP_VALUE_MAX + 1> value;
    if (__system_property_get("ro.build.version.sdk", value.data()) > 0) {
        sdk = atoi(value.data());
    }

    auto callbacks_size = 0;
    if (sdk >= __ANDROID_API_R__) {
        callbacks_size = sizeof(NativeBridgeCallbacks<__ANDROID_API_R__>);
    } else if (sdk == __ANDROID_API_Q__) {
        callbacks_size = sizeof(NativeBridgeCallbacks<__ANDROID_API_Q__>);
    } else if (sdk == __ANDROID_API_P__) {
        callbacks_size = sizeof(NativeBridgeCallbacks<__ANDROID_API_P__>);
    } else if (sdk == __ANDROID_API_O_MR1__) {
        callbacks_size = sizeof(NativeBridgeCallbacks<__ANDROID_API_O_MR1__>);
    } else if (sdk == __ANDROID_API_O__) {
        callbacks_size = sizeof(NativeBridgeCallbacks<__ANDROID_API_O__>);
    } else if (sdk == __ANDROID_API_N_MR1__) {
        callbacks_size = sizeof(NativeBridgeCallbacks<__ANDROID_API_N_MR1__>);
    } else if (sdk == __ANDROID_API_N__) {
        callbacks_size = sizeof(NativeBridgeCallbacks<__ANDROID_API_N__>);
    } else if (sdk == __ANDROID_API_M__) {
        callbacks_size = sizeof(NativeBridgeCallbacks<__ANDROID_API_M__>);
    }

    memcpy(NativeBridgeItf, original_native_bridge_itf, callbacks_size);
#endif}

大致总结下就是:

1,从magisk_path/.magisk/modules/riru-core/lib下dlopen libriru.so,如果有init sym的话执行之

2,如果原来有native bridge的话就和rirud进程通信获取一下,然后仿照源码逻辑操作一波


riru.so 源码分析

接下来看libriru.so, 根据riru/src/main/cpp/CMakeLists.txt可知,其入口为riru/src/main/cpp/entry.cpp,关键代码如下

extern "C" [[gnu::visibility("default")]] [[maybe_unused]] void// NOLINTNEXTLINEinit(void *handle, const char* magisk_path, const RirudSocket& rirud) {
    self_handle = handle;

    magisk::SetPath(magisk_path);
    hide::PrepareMapsHideLibrary();
    jni::InstallHooks();
    modules::Load(rirud);
}

正好和上面libriruloader.so中的操作对应,执行libriru.so的init函数


hide::PrepareMapsHideLibrary

先来看hide::PrepareMapsHideLibrary(), 就是从libriruhide.so获取了下riru_hide_func,先记住这个riru_hide_func,下面会用到

void PrepareMapsHideLibrary() {
    auto hide_lib_path = magisk::GetPathForSelfLib("libriruhide.so");

    // load riruhide.so and run the hide    LOGD("dlopen libriruhide");
    riru_hide_handle = DlopenExt(hide_lib_path.c_str(), 0);
    if (!riru_hide_handle) {
        LOGE("dlopen %s failed: %s", hide_lib_path.c_str(), dlerror());
        return;
    }
    riru_hide_func = reinterpret_cast<riru_hide_t *>(dlsym(riru_hide_handle, "riru_hide"));
    if (!riru_hide_func) {
        LOGE("dlsym failed: %s", dlerror());
        dlclose(riru_hide_handle);
        return;
    }
}


jni::InstallHooks()

接着看jni::InstallHooks()

void jni::InstallHooks() {
    XHOOK_REGISTER(".*\\libandroid_runtime.so$", jniRegisterNativeMethods)

    if (xhook_refresh(0) == 0) {
        xhook_clear();
        LOGI("hook installed");
    } else {
        LOGE("failed to refresh hook");
    }
    //  省略多行代码
}

来看XHOOK_REGISTER这个宏定义

#define XHOOK_REGISTER(PATH_REGEX, NAME) \
    if (xhook_register(PATH_REGEX, #NAME, (void*) new_##NAME, (void **) &old_##NAME) != 0) \
        LOGE("failed to register hook " #NAME "."); \

再来看个宏和函数

#define NEW_FUNC_DEF(ret, func, ...) \
    using func##_t = ret(__VA_ARGS__); \
    static func##_t *old_##func; \
    static ret new_##func(__VA_ARGS__)

NEW_FUNC_DEF(int, jniRegisterNativeMethods, JNIEnv *env, const char *className,
             const JNINativeMethod *methods, int numMethods) {
    LOGD("jniRegisterNativeMethods %s", className);

    auto newMethods = handleRegisterNative(className, methods, numMethods);
    int res = old_jniRegisterNativeMethods(env, className, newMethods ? newMethods.get() : methods,
                                           numMethods);
    /*if (!newMethods) {        NativeMethod::jniRegisterNativeMethodsPost(env, className, methods, numMethods);    }*/    return res;
}

这样一来,new_jniRegisterNativeMethods和old_jniRegisterNativeMethods就有了(ps:和va核心io那里的写法很像吧,大佬们优雅的写法如出一辙)

该来看关键点handleRegisterNative了


适配下版本,比较下method的name和signature,替换下函数指针,hook原理就来了,点开看下nativeForkAndSpecialize_r代码如下:

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(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, is_child_zygote, res);
    return res;
}

这就是pre和post函数的由来了。

替换了目标函数的函数指针后,返回新的JNINativeMethod[],再由真正的old_jniRegisterNativeMethods去注册jni函数,这样就完成了hook注入,riru完成暴露了自己的api


modules::Load(rirud)

接下来就开始加载基于riru开发的模块(modules::Load(rirud))了



LoadModule(id, path, magisk_module_path)

先看LoadModule(id, path, magisk_module_path);如下所示dlopen和init来了



hide::HideFromMaps()

再看hide::HideFromMaps();往下跟一下关键点就到了riruhide.so的riru_hide和do_hide

void HidePathsFromMaps(const std::set<std::string_view> &names) {
    if (!riru_hide_func) return;

    LOGD("do hide");
    riru_hide_func(names);

    // cleanup riruhide.so    LOGD("dlclose");
    if (dlclose(riru_hide_handle) != 0) {
        LOGE("dlclose failed: %s", dlerror());
        return;
    }
}


module.onModuleLoaded()

再看module.onModuleLoaded()就是riru模块提供的另一个api了,在riru提供的开发模块中可以看到注释介绍

static void onModuleLoaded() {
    // Called when this library is loaded and "hidden" by Riru (see Riru's hide.cpp)
        // If you want to use threads, start them here rather than the constructors
            // __attribute__((constructor)) or constructors of static variables,
                // or the "hide" will cause SIGSEGV
                }

至此,riru模块加载原理的来龙去脉就根据源码看完了。。。


四,参考文献

Riru原理浅析和EdXposed入口分析:https://bbs.pediy.com/thread-263018.htm

and  

上面提到的所有链接


[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

最后于 2021-11-8 15:14 被huaerxiela编辑 ,原因:
收藏
免费 7
支持
分享
最新回复 (5)
雪    币: 6212
活跃值: (6447)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
2
花蛤牛逼
2021-11-8 15:06
0
雪    币: 19
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
3
666
2021-11-9 15:43
0
雪    币: 107
活跃值: (419)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
学习了....
2021-11-9 20:40
0
雪    币: 120
活跃值: (1597)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
5
学习了....
2023-4-5 22:42
0
雪    币: 3525
活跃值: (31011)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
6
感谢分享
2023-4-6 00:08
1
游客
登录 | 注册 方可回帖
返回
//