一,引言 因为最近在研究在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编辑
,原因: