【Android 原创】利用frida 探究对于模拟器下arm so加载

admin 2025年4月9日22:34:50评论12 views字数 18757阅读62分31秒阅读模式
作者坛账号:chenchenchen777

利用frida 探究对于模拟器下arm so加载

这里是对于模拟器下的arm下的so文件进行的分析探究,实际上对于这些的知识上,相关的文章是很少的,这次也算是对于模拟器的研究和分析了

环境搭建:

这里使用的是mumu模拟器进行的frida监控模拟器arm的so的加载过程

安装mumu模拟器:MuMu模拟器官网_安卓12模拟器_网易手游模拟器

同之前的真机端一样,在移动端安装上对应的frida-server,以及对应的PC端安装frida,操作和之前其实是大相径庭的

frida的14.2.18问题

由于我PC端的这个frida是14.2.18版本的,所以我一开始尝试的是下载对应的14.2.18版本的frida-server的x86_64的。

【Android 原创】利用frida 探究对于模拟器下arm so加载

报错

但是在模拟器中进行启动的时候,发现是会有报错的,但是还是会启动frida-server的服务的,所以还是去尝试过直接注入frida,但是程序会卡住。

【Android 原创】利用frida 探究对于模拟器下arm so加载

通过deepseek来查看了相关可能出现的问题

【Android 原创】利用frida 探究对于模拟器下arm so加载

在本机和模拟器的frida以及frida-server匹配的14.2.18版本下是会出现兼容问题的,于是这里去尝试去实现更换本地frida版本和frida-server版本的操作。

frida的16.0.11

本地端
 复制代码 隐藏代码pip install --upgrade frida==16.0.11

同时更新frida-tools

 复制代码 隐藏代码pip install --upgrade frida-tools
模拟器端

下载对应的frida-server版本

【Android 原创】利用frida 探究对于模拟器下arm so加载

这里不知道是不是模拟器的特点,还是独属于mumu模拟器的,需要把frida-server放入对应的目录(没测试过不放会有什么问题)

【Android 原创】利用frida 探究对于模拟器下arm so加载

然后就是常规的操作了,启动server服务

 复制代码 隐藏代码adb push frida-server /data/local/tmp/adb shellsuchmod 777/data/local/tmp/frida-server-16.0.11-android-x86_64./data/local/tmp/frida-server-16.0.11-android-x86_64

【Android 原创】利用frida 探究对于模拟器下arm so加载

这里启动起来之后就没有再进行报错输出了,同时尝试了最简单的frida注入操作,发现是没问题的

【Android 原创】利用frida 探究对于模拟器下arm so加载

这里是因为是以spwned启动的b站,是由frida检测的,所以程序崩溃了,但是能够实现注入

程序流程探究

按照我自己对于这个作业的要求,需要是去探究android真机和模拟器在so层执行流程之间的差别。

真机测试

这里去尝试了HOOK了b站的dlopen函数,看看执行得到的so文件是什么

 复制代码 隐藏代码functionhook_dlopen() {var interceptor = Interceptor.attach(Module.findExportByName(null"android_dlopen_ext"),        {onEnterfunction (args) {var pathptr = args[0];if (pathptr !== undefined && pathptr != null) {var path = ptr(pathptr).readCString();console.log("[LOAD]", path)                }            }        }    )return interceptor}setImmediate(hook_dlopen)

【Android 原创】利用frida 探究对于模拟器下arm so加载

这里其实是哔哩哔哩的frida检测的so的位置了,可以发现的是,我们能够去利用dlopen去实现对于加载的so文件进行打印

模拟器测试

由于模拟器是X86_64的构架模拟的android机来实现的对于程序的执行,可能使用不同的 dlopen 函数变体来加载so文件,所以HOOK代码也略微的进行了修改

 复制代码 隐藏代码functionhook_dlopen() {// Hook 所有可能的 dlopen 变体const dlopenFuncs = ['android_dlopen_ext','dlopen','__loader_dlopen'    ];let interceptors = [];    dlopenFuncs.forEach(funcName => {let funcPtr = Module.findExportByName(null, funcName);if (funcPtr) {let interceptor = Interceptor.attach(funcPtr, {onEnterfunction(args) {var pathptr = args[0];if (pathptr && !pathptr.isNull()) {var path = ptr(pathptr).readCString();console.log("[LOAD]", path);// 检查是否是可疑的检测库if (path && (path.includes("libtt") || path.includes("libbili") ||                             path.includes("security") || path.includes("protect"))) {console.warn("!!! Possible Frida detection library loaded:", path);// 打印调用栈可以帮助定位谁加载了这个库console.log(Thread.backtrace(this.contextBacktracer.ACCURATE)                                .map(DebugSymbol.fromAddress).join('n') + 'n');                        }                    }                }            });            interceptors.push(interceptor);console.log(`Hooked ${funcName} at ${funcPtr}`);        }    });// 返回所有拦截器以便后续管理return interceptors;}// 延迟执行以避免错过早期加载的库setImmediate(hook_dlopen);

【Android 原创】利用frida 探究对于模拟器下arm so加载

对比结果引发的思考和问题

其实是可以发现模拟器也是挂掉了,但是这里会引起思考的是为什么,在真机测试下的HOOK的dlopen相关函数得到的so文件和在模拟器下HOOK得到的so文件一点不一样

这里b站的版本是7.76.0的版本,在这个版本下面我是写过相关的一个绕过frida检测的帖子的,这个frida检测的so是libmsaoaidsec.so,但是在模拟器端这个so甚至于没有被  'android_dlopen_ext', 'dlopen','__loader_dlopen'

这几个HOOK函数都没有将这个libmsaoaidsec.so捕获到,那么可能会去考虑执行流程的问题。

架构之间的兼容问题

由于其实从一开始对于模拟器的研究很少,所以会去考虑的是为什么,在X86或者是X86_64的架构的模拟器可以去实现ARM架构的指令集。

通过搜索可以得知是主要依赖于 动态二进制翻译 和 系统级的兼容层技术

动态二进制翻译(Dynamic Binary Translation, DBT)
  • 作用
    :实时将 ARM 指令逐块翻译为 x86_64 指令。
  • 流程
    1. 拦截 ARM 程序的指令流。
    2. 将每段 ARM 指令翻译为等效的 x86_64 指令。
    3. 缓存翻译后的代码,后续执行直接调用缓存。
系统调用转发
  • ARM 程序发出的系统调用(如文件读写)会被捕获,并转发到宿主系统(x86_64)的 Linux 内核。
  • 例如,ARM 的 open() 系统调用会映射为 x86_64 的 sys_open()

以上是引发思考之后提问deepseek得到的解答

个人理解

所以这里其实和在安卓逆向中的VMP很像了,自定义架构去定义自己的加密函数,实现按照不同的操作(像加减乘除,位运算这些)。

同时也能理解为什么在android虚拟机或者是云手机会需要逆向了,动态二进制翻译按照我们逆向手的思路其实就是HOOK每一条指令,然后修改代码,从ARM转为x86_64,实现一个架构的转变。

同时,也是得益于资料的搜索,开始想着和研究AOSP源码的对于模拟器的执行流程处理

AOSP源码中的对于模拟器的执行流程处理

我以前发表过Android真机端的so文件的真实的执行流程的

【新提醒】Android SO文件加载过程探究 - 吾爱破解 - 52pojie.cn

但是这篇文章只是局限于了对于android真机也就是arm的处理,并没有对于这个真实情况下的模拟器进行so层的执行流程分析处理,这里正好利用mumu模拟器看看,对于不同架构下的处理。

Android Native Bridge 机制

首先我们需要了解到的是Android Native Bridge 机制,在 MuMu 模拟器中执行 ARM 架构的 SO 文件,其核心依赖 二进制翻译 和 Android Native Bridge 机制,这里面就包含了我们之前所说的动态二进制翻译,从而实现支持跨架构运行的一个过程。

梳理真机下的执行流程

我们首先是通过System.load()进入

 复制代码 隐藏代码@CallerSensitivepublicstaticvoidload(String filename) {    Runtime.getRuntime().load0(Reflection.getCallerClass(), filename);}

此方法最终调用 Runtime.load0(),然后进入 nativeLoad() 函数。

Runtime_nativeLoad → vm->LoadNativeLibrary 在这里之后调用了OpenNaitveLibrary

【Android 原创】利用frida 探究对于模拟器下arm so加载

进入 JavaVMExt::LoadNativeLibrary 方法后,最终会调用 dlopen 进行真正的 SO 文件加载。

 复制代码 隐藏代码vm->LoadNativeLibrary(env, filename.c_str(), javaLoader, caller, &error_msg);

在 Android 12 及以上版本,会调用 android_dlopen_ext 返回 __loader_android_dlopen_ext

 复制代码 隐藏代码void* __loader_android_dlopen_ext(constchar* filename,int flags,const android_dlextinfo* extinfo,constvoid* caller_addr) {return dlopen_ext(filename, flags, extinfo, caller_addr);}

该方法最终调用 dlopen_ext()

 复制代码 隐藏代码staticvoiddlopen_ext(constchar* filename,int flags,const android_dlextinfo* extinfo,constvoid* caller_addr) {  ScopedPthreadMutexLocker locker(&g_dl_mutex);void* result = do_dlopen(filename, flags, extinfo, caller_addr);return result;}

do_dlopen(filename, flags, extinfo, caller_addr)

在 do_dlopen() 中,会调用 find_library() 进行 SO 文件的真正加载。

 复制代码 隐藏代码soinfo* si = find_library(ns, translated_name, flags, extinfo, caller);

这里是对于soinfo的赋值,同时在这里开始调用so的.init_proc函数,接着调用.init_array中的函数,最后才是JNI_OnLoad函数。最后到达find_libraries 执行最后的处理。

构架检查

同时在find_library的位置,会出现对于ELF文件头读取的字段,通过解析 SO 文件的 ELF 头e_machine 字段)判断架构是否匹配。来确定对于构架的检测

【Android 原创】利用frida 探究对于模拟器下arm so加载

初始化Native Bridge

当发现了对应的架构不符合ARM,需要进行Native Bridge转换时,那么就会实现初始化Native Bridge

【Android 原创】利用frida 探究对于模拟器下arm so加载

Native Bridge 触发

OpenNaitveLibrary函数中,会去判断是否对于触发Native Bridge

核心决策点:选择原生 dlopen 或 Native Bridge 加载

【Android 原创】利用frida 探究对于模拟器下arm so加载

通过在这里对于Native Bridge的是否对目标 SO 启用 Native Bridge

【Android 原创】利用frida 探究对于模拟器下arm so加载

当判断到架构并不匹配的时候,就需要去利用Native Bridge 转换架构实现跨架构执行操作,那么就不会进入dlopen的函数,而是进入NativeBridgeLoadLibrary

【Android 原创】利用frida 探究对于模拟器下arm so加载

流程图

个人对于模拟器加载so的过程的理解:

【Android 原创】利用frida 探究对于模拟器下arm so加载

新的问题

在模拟器中,跨架构的 SO 加载会通过 NativeBridgeLoadLibrary 函数(而非标准 dlopen)完成

有了流程图是对于模拟器执行so文件流程有了一个了解,但是我们需要其实是HOOK,那么我们如何像在真机端这种去HOOK dlopen函数就能够去得到dlopen的参数从而得到加载的so文件名

那么是否我们能够去HOOK NativeBridgeLoadLibrary 函数呢?NativeBridgeLoadLibrary是在Native Bridge 库里面的函数,同时也是AOSP源码中的函数。我们能否去定位到这个so文件呢?

提出了疑问,自己的一些猜测,确实可以去尝试一下

这里我为了去确定NativeBridgeLoadLibrary函数,产生过这样的代码

 复制代码 隐藏代码Java.perform(() => {// 加载 Native Bridge 库const libnb = Module.load("libhoudini.so");// 获取 NativeBridgeLoadLibrary 函数地址constNBLoadLib = Module.getExportByName("libhoudini.so""NativeBridgeLoadLibrary");// Hook 函数Interceptor.attach(NBLoadLib, {onEnter(args) {const libpath = args[0].readCString(); // 第一个参数是 SO 路径const flag = args[1];                  // 第二个参数是标志位 (int)console.log(`[NB] 加载库: ${libpath}, flags=${flag}`);// 可选:篡改加载路径(如重定向到其他 SO)if (libpath.includes("target.so")) {        args[0].writeUtf8String("/data/local/tmp/fake.so");console.log("已重定向 SO 路径!");      }    },onLeave(retval) {console.log(`[NB] 返回句柄: ${retval}`);    }  });});

发现实际上是没有这个函数的 【Android 原创】利用frida 探究对于模拟器下arm so加载

去查看了对应的libhoudini.so文件,发现实际上的是有函数符号,但是并没有NativeBridgeLoadLibrary函数
 复制代码 隐藏代码Java.perform(() => {let libnb = Module.enumerateExportsSync("libhoudini.so");    libnb.forEach(exp =>console.log(exp.name));  });

打印了一下符号

【Android 原创】利用frida 探究对于模拟器下arm so加载

NativeBridgeItf

NativeBridgeItf是 Native Bridge 的核心接口表,通常包含 loadLibrary,但是不知道的是loadLibrary对于这个结构体中的偏移位置。

这里我的想法是,不知道偏移地址就进行爆破HOOK,把所有可能loadLibrary出现在NativeBridgeItf的结构体的偏移都进行hook

 复制代码 隐藏代码Java.perform(() => {constNativeBridgeItf = Module.findExportByName("libhoudini.so""NativeBridgeItf");const callbacks = NativeBridgeItf.readPointer();// 遍历 0x0 ~ 0x50 的偏移for (let offset = 0; offset < 0x50; offset += Process.pointerSize) {const func = callbacks.add(offset).readPointer();Interceptor.attach(func, {onEnter(args) {console.log(`[测试] 偏移 ${offset.toString(16)} 被调用,参数: ${args[0]}`);      }    });  }});

但是这里出现了内存报错

【Android 原创】利用frida 探究对于模拟器下arm so加载

这里开始去了解了对于这个NativeBridgeItf 结构体的细节内容,实际上是存在版本差异的。
1. 低版本(Android 5.0~7.0)

核心函数集中在偏移 0x0 ~ 0x28

 复制代码 隐藏代码structNativeBridgeItf {// 基础字段uint32_t version;          // 版本号 (e.g., 1)uint32_t padding;          // 对齐填充 (64位下可能不存在)// 函数指针表bool (*initialize)(conststruct NativeBridgeRuntimeCallbacks* runtime_cbs);  // 0x8 (64位)void* (*loadLibrary)(constchar* libpath, int flag);                        // 0x10 (64位)void* (*getTrampoline)(void* handle, constchar* name, constchar* shorty);  // 0x18 (64位)bool (*isCompatibleWith)(uint32_t bridge_version);                          // 0x20 (64位)void* (*getNativeAddress)(void* arm_address);                               // 0x28 (64位)// ... 其他扩展函数};

结构体中的函数指针名通常为 loadLibrary,对应偏移固定(如 64 位环境下偏移 0x10

2.高版本(Android 8.0+)
 复制代码 隐藏代码structNativeBridgeItf {uint32_t version;          // 版本号 (e.g., 2 或 3)uint32_t padding;// 基础函数(与低版本相同)bool (*initialize)(...);   // 0x8void* (*loadLibrary)(...); // 0x10// 扩展函数(新增)void* (*loadLibraryExt)(constchar* libpath, int flag, void* extinfo);  // 0x18void* (*getTrampolineExt)(...);                                         // 0x20void* (*createNamespace)(...);                                          // 0x28// ... 其他扩展函数};

引入扩展接口 loadLibraryExt,可能替代或补充 loadLibrary,偏移可能后移(如 0x18

在固定的一个NativeBridgeItf 结构体偏移之下的这个loadLibrary也是固定的,那么我们其实可以去考虑得到这个结构体在对应偏移的位置去得到相应的地址

【Android 原创】利用frida 探究对于模拟器下arm so加载

这里能够看到mumu模拟器模拟的是android12版本的,所以对应的这个NativeBridgeItf也是对应的高版本上的结合体。

这里实际上一直在报错,按照的就是内存错误之类的信息,所以我还是打算去老老实实的看源码

IDA分析结构体

这里对于这些结合体老老实实看看是什么参数

【Android 原创】利用frida 探究对于模拟器下arm so加载

可以比对这这个IDA得到的结构体的结构去看看AOSP源码

【Android 原创】利用frida 探究对于模拟器下arm so加载

可以看到这里是固定的,那么我们就去找对应结构的属性成员进行一个数据的打印

还原真实的NativeBridgeItf结构体数据

这里比对了对于安卓12以及IDA源码,直接去add对于的成员属性偏移来得到对应的结构体的值

【Android 原创】利用frida 探究对于模拟器下arm so加载

 复制代码 隐藏代码Java.perform(() => {console.log("n====== 环境信息 ======");console.log(` 进程架构: ${Process.arch}`);console.log(` 当前线程ID: ${Process.getCurrentThreadId()}`);constBuild = Java.use("android.os.Build");console.log("n====== 模块信息 ======");const libhoudini = Module.findBaseAddress("libhoudini.so");if (!libhoudini) {console.error("[!] libhoudini.so 未加载");return;    }console.log(` libhoudini.so 基址: ${libhoudini}`);console.log("n====== NativeBridgeItf 符号信息 ======");constNativeBridgeItf = Module.findExportByName("libhoudini.so""NativeBridgeItf");if (!NativeBridgeItf) {console.error("[!] 找不到 NativeBridgeItf 符号");return;    }console.log(` NativeBridgeItf 符号地址: ${NativeBridgeItf}`);console.log(` NativeBridgeItf 符号地址 偏移寻址 : ${libhoudini.add(0x0701D00)}`);console.log("n====== 结构体指针信息 ======");const callbacks = NativeBridgeItf.readInt();console.log(` version: ${callbacks}`);const padding = NativeBridgeItf.add(0x4).readInt();console.log(` padding: ${padding}`);const initialize = NativeBridgeItf.add(0x8).readPointer();console.log(` initialize addr: ${initialize}`);const loadLibrary = NativeBridgeItf.add(0x10).readPointer();console.log(` loadLibrary addr: ${loadLibrary}`);const getTrampoline = NativeBridgeItf.add(0x18).readPointer();console.log(` getTrampoline addr: ${getTrampoline}`);const isSupported = NativeBridgeItf.add(0x20).readPointer();console.log(` isSupported addr: ${isSupported}`);const getAppEnv = NativeBridgeItf.add(0x28).readPointer();console.log(` getAppEnv addr: ${getAppEnv}`);const isCompatibleWith = NativeBridgeItf.add(0x30).readPointer();console.log(` isCompatibleWith addr: ${isCompatibleWith}`);const getSignalHandler = NativeBridgeItf.add(0x38).readPointer();console.log(` getSignalHandler addr: ${getSignalHandler}`);const unloadLibrary = NativeBridgeItf.add(0x40).readPointer();console.log(` unloadLibrary addr: ${unloadLibrary}`);const getError = NativeBridgeItf.add(0x48).readPointer();console.log(` getError addr: ${getError}`);const isPathSupported = NativeBridgeItf.add(0x50).readPointer();console.log(` isPathSupported addr: ${isPathSupported}`);const unused_initAnonymousNamespace = NativeBridgeItf.add(0x58).readPointer();console.log(` unused_initAnonymousNamespace addr: ${unused_initAnonymousNamespace}`);const createNamespace = NativeBridgeItf.add(0x60).readPointer();console.log(` createNamespace addr: ${createNamespace}`);const linkNamespaces = NativeBridgeItf.add(0x68).readPointer();console.log(` linkNamespaces addr: ${linkNamespaces}`);const loadLibraryExt = NativeBridgeItf.add(0x70).readPointer();console.log(` loadLibraryExt addr: ${loadLibraryExt}`);const getVendorNamespace = NativeBridgeItf.add(0x78).readPointer();console.log(` getVendorNamespace addr: ${getVendorNamespace}`);const getExportedNamespace = NativeBridgeItf.add(0x80).readPointer();console.log(` getExportedNamespace addr: ${getExportedNamespace}`);const preZygoteFork = NativeBridgeItf.add(0x88).readPointer();console.log(` preZygoteFork addr: ${preZygoteFork}`);});

【Android 原创】利用frida 探究对于模拟器下arm so加载

能够看到这里我们也是把对应的成员属性的值给打印出来了

但是虽然我们这里将这些数据都给打印出来了,但是我们其实实际上需要的参数就是loadLibrary参数,因为我们一直希望干的事情就是HOOK loadLibrary参数来类比于HOOK dlopen

 复制代码 隐藏代码Java.perform(() => {console.log("n====== 环境信息 ======");console.log(` 进程架构: ${Process.arch}`);console.log(` 当前线程ID: ${Process.getCurrentThreadId()}`);constBuild = Java.use("android.os.Build");console.log("n====== 模块信息 ======");const libhoudini = Module.findBaseAddress("libhoudini.so");if (!libhoudini) {console.error("[!] libhoudini.so 未加载");return;    }console.log(` libhoudini.so 基址: ${libhoudini}`);console.log("n====== NativeBridgeItf 符号信息 ======");constNativeBridgeItf = Module.findExportByName("libhoudini.so""NativeBridgeItf");if (!NativeBridgeItf) {console.error("[!] 找不到 NativeBridgeItf 符号");return;    }console.log(` NativeBridgeItf 符号地址: ${NativeBridgeItf}`);console.log(` NativeBridgeItf 符号地址 偏移寻址 : ${libhoudini.add(0x0701D00)}`);console.log("n====== 结构体指针信息 ======");const callbacks = NativeBridgeItf.readInt();console.log(` version: ${callbacks}`);const padding = NativeBridgeItf.add(0x4).readInt();console.log(` padding: ${padding}`);const initialize = NativeBridgeItf.add(0x8).readPointer();console.log(` initialize addr: ${initialize}`);const loadLibrary = NativeBridgeItf.add(0x10).readPointer();console.log(` loadLibrary addr: ${loadLibrary}`);const getTrampoline = NativeBridgeItf.add(0x18).readPointer();console.log(` getTrampoline addr: ${getTrampoline}`);const isSupported = NativeBridgeItf.add(0x20).readPointer();console.log(` isSupported addr: ${isSupported}`);const getAppEnv = NativeBridgeItf.add(0x28).readPointer();console.log(` getAppEnv addr: ${getAppEnv}`);const isCompatibleWith = NativeBridgeItf.add(0x30).readPointer();console.log(` isCompatibleWith addr: ${isCompatibleWith}`);const getSignalHandler = NativeBridgeItf.add(0x38).readPointer();console.log(` getSignalHandler addr: ${getSignalHandler}`);const unloadLibrary = NativeBridgeItf.add(0x40).readPointer();console.log(` unloadLibrary addr: ${unloadLibrary}`);const getError = NativeBridgeItf.add(0x48).readPointer();console.log(` getError addr: ${getError}`);const isPathSupported = NativeBridgeItf.add(0x50).readPointer();console.log(` isPathSupported addr: ${isPathSupported}`);const unused_initAnonymousNamespace = NativeBridgeItf.add(0x58).readPointer();console.log(` unused_initAnonymousNamespace addr: ${unused_initAnonymousNamespace}`);const createNamespace = NativeBridgeItf.add(0x60).readPointer();console.log(` createNamespace addr: ${createNamespace}`);const linkNamespaces = NativeBridgeItf.add(0x68).readPointer();console.log(` linkNamespaces addr: ${linkNamespaces}`);const loadLibraryExt = NativeBridgeItf.add(0x70).readPointer();console.log(` loadLibraryExt addr: ${loadLibraryExt}`);const getVendorNamespace = NativeBridgeItf.add(0x78).readPointer();console.log(` getVendorNamespace addr: ${getVendorNamespace}`);const getExportedNamespace = NativeBridgeItf.add(0x80).readPointer();console.log(` getExportedNamespace addr: ${getExportedNamespace}`);const preZygoteFork = NativeBridgeItf.add(0x88).readPointer();console.log(` preZygoteFork addr: ${preZygoteFork}`);Interceptor.attach(loadLibraryExt, {onEnterfunction(args) {// 根据实际函数原型判断参数,这里假设第一个参数为要加载的库路径var libName = Memory.readCString(args[0]);console.log(" loadLibraryExt called with library name: " + libName);        },onLeavefunction(retval) {console.log(" loadLibraryExt returned: " + retval);        }    });});

HOOK loadLibraryExt

有了地址去直接HOOK,利用HOOK loadLibraryExt去实现得到在模拟器中加载过的so文件

【Android 原创】利用frida 探究对于模拟器下arm so加载

这里终于是得到了最终的结果了,也是类比于真机HOOK dlopen一样了,这里去HOOK 了loadLibraryExt得到了和手机端一样的结果,这里的最终的libmsaoaidsec.so,就是对于加载过的so文件的输出结果了

绕过frida检测

【新提醒】bilibili XHS frida检测分析绕过 - 吾爱破解 - 52pojie.cn

这里选择的APP是 哔哩哔哩的7.76.0,其中有一个原因就是我之前写过一篇关于这个版本的frida检测绕过,这里我已经和手机端一样的能够去定位frida检测的so文件了。

  • frida的patch点是在JNI_Onload之前,init之后的。
  • 具体的patch点是在__system_property_get("ro.build.version.sdk")的时机
  • HOOK的pthread_create函数,发现在libmsaoaidsec.so里面开启了三个线程
  • 绕过的操作就是直接patch了这三个线程绕过的
 复制代码 隐藏代码Java.perform(() => {console.log("n====== 环境信息 ======");console.log(` 进程架构: ${Process.arch}`);console.log(` 当前线程ID: ${Process.getCurrentThreadId()}`);const libhoudini = Module.findBaseAddress("libhoudini.so");if (!libhoudini) {console.error("[!] libhoudini.so 未加载");return;    }console.log(` libhoudini.so 基址: ${libhoudini}`);constNativeBridgeItf = Module.findExportByName("libhoudini.so""NativeBridgeItf");if (!NativeBridgeItf) {console.error("[!] 找不到 NativeBridgeItf 符号");return;    }console.log(` NativeBridgeItf 符号地址: ${NativeBridgeItf}`);// 遍历结构体指针console.log("n====== 结构体指针信息 ======");const offsets = {version0x0padding0x4initialize0x8loadLibrary0x10,getTrampoline0x18isSupported0x20getAppEnv0x28isCompatibleWith0x30getSignalHandler0x38unloadLibrary0x40,getError0x48isPathSupported0x50unused_initAnonymousNamespace0x58,createNamespace0x60linkNamespaces0x68loadLibraryExt0x70,getVendorNamespace0x78getExportedNamespace0x80preZygoteFork0x88    };Object.keys(offsets).forEach(name => {console.log(${name} addr: ${NativeBridgeItf.add(offsets[name]).readPointer()}`);    });// Hook loadLibraryExtInterceptor.attach(NativeBridgeItf.add(offsets.loadLibraryExt).readPointer(), {onEnterfunction(args) {var libName = Memory.readCString(args[0]);console.log(` loadLibraryExt called with: ${libName}`);if (libName.includes("libmsaoaidsec.so")) {console.log("hooking libmsaoaidsec.so");hook_system_property_get();            }        },onLeavefunction(retval) {console.log(` loadLibraryExt returned: ${retval}`);        }    });});functionhook_system_property_get() {var addr = Module.findExportByName(null"__system_property_get");if (!addr) {console.log("__system_property_get not found");return;    }console.log("hooking __system_property_get");Interceptor.attach(addr, {onEnterfunction(args) {var name = ptr(args[0]).readCString();if (name.includes("ro.build.version.sdk")) {console.log("Found ro.build.version.sdk, patching...");setTimeout(hook_pthread_create, 100);            }        }    });}functioncall_function(){console.log("alearly patch frida");}functionhook_pthread_create() {var pthread_create = Module.findExportByName("libc.so""pthread_create");if (!pthread_create) {console.log("pthread_create not found");return;    }var libmsaoaidsec = Process.findModuleByName("libmsaoaidsec.so");if (!libmsaoaidsec) {console.log("libmsaoaidsec.so not found");return;    }console.log(`libmsaoaidsec.so base: ${libmsaoaidsec.base}`);Interceptor.attach(pthread_create, {onEnterfunction(args) {var thread_ptr = args[2];if (thread_ptr.compare(libmsaoaidsec.base) < 0 ||                 thread_ptr.compare(libmsaoaidsec.base.add(libmsaoaidsec.size)) >= 0) {console.log(`pthread_create other thread: ${thread_ptr}`);            } else {console.log(`pthread_create libmsaoaidsec.so thread: ${thread_ptr}, offset: ${thread_ptr.sub(libmsaoaidsec.base)}`);                [0x1c5440x1b8d40x26e5c].forEach(offset => {Interceptor.replace(libmsaoaidsec.base.add(offset), newNativeCallback(() =>console.log(`Interceptor.replace: 0x${offset.toString(16)}`), "void", [])                    );                });            }        }    });}

【Android 原创】利用frida 探究对于模拟器下arm so加载

这里绕过了frida检测,调用了call_function函数,打印了一串字符串

总结:

这里只是对于在模拟器上如何实现跨架构进行so加载的探究流程,由于时间有点短,其中很多的细节没有细致的研究,只是对于整个模拟器so加载流程做了一个小小的判断。

我类比于hook dlopen的方法想去HOOK NativeBridgeLoadLibrary 想去得到对应的frida检测的so文件,所以去还原了NativeBridgeItf这个结构体,也是最终去得到了loadLibraryExt函数地址,进行了HOOK loadLibraryExt,也是得到了和之前手机端一样的结果了

之后也是复现了自己在手机端的绕过frida检测,也是成功绕过了

但是确实有了一些对于android虚拟机的理解,比如这种跨架构的过程,好比逆向过程中把每一个指令进行拦截然后用对应的架构语言去解释,然后实现在自己的架构里面进行虚拟加载,确实对于安卓虚拟机有了一些认识。

原文始发于微信公众号(吾爱破解论坛):【Android 原创】利用frida 探究对于模拟器下arm so加载

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年4月9日22:34:50
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   【Android 原创】利用frida 探究对于模拟器下arm so加载https://cn-sec.com/archives/3933811.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息