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

查看 83|回复 11
作者: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的。


image-20250327084351655.png (61.27 KB, 下载次数: 0)
下载附件
2025-4-7 12:52 上传

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


image-20250327081307720.png (488.7 KB, 下载次数: 0)
下载附件
2025-4-7 13:02 上传

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


image-20250327084509215.png (32.53 KB, 下载次数: 0)
下载附件
2025-4-7 12:52 上传

在本机和模拟器的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版本


image-20250327085149107.png (55.58 KB, 下载次数: 0)
下载附件
2025-4-7 12:52 上传

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


image-20250327085328408.png (68.22 KB, 下载次数: 0)
下载附件
2025-4-7 12:52 上传

然后就是常规的操作了,启动server服务
adb push frida-server /data/local/tmp/
adb shell
su
chmod 777 /data/local/tmp/frida-server-16.0.11-android-x86_64
./data/local/tmp/frida-server-16.0.11-android-x86_64


image-20250327090423595.png (65.43 KB, 下载次数: 0)
下载附件
2025-4-7 12:52 上传

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


image-20250327090613016.png (306.48 KB, 下载次数: 0)
下载附件
2025-4-7 12:52 上传

这里是因为是以spwned启动的b站,是由frida检测的,所以程序崩溃了,但是能够实现注入
程序流程探究
按照我自己对于这个作业的要求,需要是去探究android真机和模拟器在so层执行流程之间的差别。
真机测试
这里去尝试了HOOK了b站的dlopen函数,看看执行得到的so文件是什么
function hook_dlopen() {
    var interceptor = Interceptor.attach(Module.findExportByName(null, "android_dlopen_ext"),
        {
            onEnter: function (args) {
                var pathptr = args[0];
                if (pathptr !== undefined && pathptr != null) {
                    var path = ptr(pathptr).readCString();
                    console.log("[LOAD]", path)
                }
            }
        }
    )
    return interceptor
}
setImmediate(hook_dlopen)


image-20250327111335099.png (291.77 KB, 下载次数: 0)
下载附件
2025-4-7 12:52 上传

这里其实是哔哩哔哩的frida检测的so的位置了,可以发现的是,我们能够去利用dlopen去实现对于加载的so文件进行打印
模拟器测试
由于模拟器是X86_64的构架模拟的android机来实现的对于程序的执行,可能使用不同的 dlopen 函数变体来加载so文件,所以HOOK代码也略微的进行了修改
function hook_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, {
                onEnter: function(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.context, Backtracer.ACCURATE)
                                .map(DebugSymbol.fromAddress).join('\n') + '\n');
                        }
                    }
                }
            });
            interceptors.push(interceptor);
            console.log(`Hooked ${funcName} at ${funcPtr}`);
        }
    });
    // 返回所有拦截器以便后续管理
    return interceptors;
}
// 延迟执行以避免错过早期加载的库
setImmediate(hook_dlopen);


image-20250327113344240.png (487.01 KB, 下载次数: 0)
下载附件
2025-4-7 12:52 上传

对比结果引发的思考和问题
其实是可以发现模拟器也是挂掉了,但是这里会引起思考的是为什么,在真机测试下的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 指令。
  • 流程
    [ol]
  • 拦截 ARM 程序的指令流。
  • 将每段 ARM 指令翻译为等效的 x86_64 指令。
  • 缓存翻译后的代码,后续执行直接调用缓存。
    [/ol]

    系统调用转发

  • 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()进入
    @CallerSensitive
    public static void load(String filename) {
        Runtime.getRuntime().load0(Reflection.getCallerClass(), filename);
    }
    此方法最终调用 Runtime.load0(),然后进入 nativeLoad() 函数。
    Runtime_nativeLoad → vm->LoadNativeLibrary 在这里之后调用了OpenNaitveLibrary


    image-20250327152546719.png (407.27 KB, 下载次数: 0)
    下载附件
    2025-4-7 12:52 上传

    进入 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(const char* filename,
                                      int flags,
                                      const android_dlextinfo* extinfo,
                                      const void* caller_addr) {
      return dlopen_ext(filename, flags, extinfo, caller_addr);
    }
    该方法最终调用 dlopen_ext()。
    static void* dlopen_ext(const char* filename,
                            int flags,
                            const android_dlextinfo* extinfo,
                            const void* 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 字段)判断架构是否匹配。来确定对于构架的检测


    image-20250327144832227.png (71.21 KB, 下载次数: 0)
    下载附件
    2025-4-7 12:52 上传

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


    image-20250327145536865.png (137.97 KB, 下载次数: 0)
    下载附件
    2025-4-7 12:52 上传

    Native Bridge 触发
    OpenNaitveLibrary函数中,会去判断是否对于触发Native Bridge
    核心决策点:选择原生 dlopen 或 Native Bridge 加载


    image-20250327144020937.png (168.45 KB, 下载次数: 0)
    下载附件
    2025-4-7 12:52 上传

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


    image-20250327144235434.png (65 KB, 下载次数: 0)
    下载附件
    2025-4-7 12:52 上传

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


    image-20250327160216764.png (38.46 KB, 下载次数: 0)
    下载附件
    2025-4-7 12:52 上传

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


    image-20250327160017569.png (24.94 KB, 下载次数: 0)
    下载附件
    2025-4-7 12:52 上传

    新的问题
    在模拟器中,跨架构的 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 函数地址
      const NBLoadLib = 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}`);
        }
      });
    });
    发现实际上是没有这个函数的


    image-20250327175223748.png (255.61 KB, 下载次数: 0)
    下载附件
    2025-4-7 12:52 上传

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


    image-20250327180712354.png (399.77 KB, 下载次数: 0)
    下载附件
    2025-4-7 12:52 上传

    NativeBridgeItf
    NativeBridgeItf是 Native Bridge 的核心接口表,通常包含 loadLibrary,但是不知道的是loadLibrary对于这个结构体中的偏移位置。
    这里我的想法是,不知道偏移地址就进行爆破HOOK,把所有可能loadLibrary出现在NativeBridgeItf的结构体的偏移都进行hook
    Java.perform(() => {
      const NativeBridgeItf = Module.findExportByName("libhoudini.so", "NativeBridgeItf");
      const callbacks = NativeBridgeItf.readPointer();
      // 遍历 0x0 ~ 0x50 的偏移
      for (let offset = 0; offset
    但是这里出现了内存报错


    image-20250327191723879.png (251.21 KB, 下载次数: 0)
    下载附件
    2025-4-7 12:52 上传

    这里开始去了解了对于这个NativeBridgeItf 结构体的细节内容,实际上是存在版本差异的。
    1. 低版本(Android 5.0~7.0)
    核心函数集中在偏移 0x0 ~ 0x28
    struct NativeBridgeItf {
        // 基础字段
        uint32_t version;          // 版本号 (e.g., 1)
        uint32_t padding;          // 对齐填充 (64位下可能不存在)
        // 函数指针表
        bool (*initialize)(const struct NativeBridgeRuntimeCallbacks* runtime_cbs);  // 0x8 (64位)
        void* (*loadLibrary)(const char* libpath, int flag);                        // 0x10 (64位)
        void* (*getTrampoline)(void* handle, const char* name, const char* 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+)
    struct NativeBridgeItf {
        uint32_t version;          // 版本号 (e.g., 2 或 3)
        uint32_t padding;
        // 基础函数(与低版本相同)
        bool (*initialize)(...);   // 0x8
        void* (*loadLibrary)(...); // 0x10
        // 扩展函数(新增)
        void* (*loadLibraryExt)(const char* libpath, int flag, void* extinfo);  // 0x18
        void* (*getTrampolineExt)(...);                                         // 0x20
        void* (*createNamespace)(...);                                          // 0x28
        // ... 其他扩展函数
    };
    引入扩展接口 loadLibraryExt,可能替代或补充 loadLibrary,偏移可能后移(如 0x18)
    在固定的一个NativeBridgeItf 结构体偏移之下的这个loadLibrary也是固定的,那么我们其实可以去考虑得到这个结构体在对应偏移的位置去得到相应的地址


    image-20250327203903554.png (13.28 KB, 下载次数: 0)
    下载附件
    2025-4-7 12:52 上传

    这里能够看到mumu模拟器模拟的是android12版本的,所以对应的这个NativeBridgeItf也是对应的高版本上的结合体。
    这里实际上一直在报错,按照的就是内存错误之类的信息,所以我还是打算去老老实实的看源码
    IDA分析结构体
    这里对于这些结合体老老实实看看是什么参数


    image-20250327205853669.png (70.49 KB, 下载次数: 0)
    下载附件
    2025-4-7 12:52 上传

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


    image-20250327212109716.png (21.82 KB, 下载次数: 0)
    下载附件
    2025-4-7 12:52 上传

    可以看到这里是固定的,那么我们就去找对应结构的属性成员进行一个数据的打印
    还原真实的NativeBridgeItf结构体数据
    这里比对了对于安卓12以及IDA源码,直接去add对于的成员属性偏移来得到对应的结构体的值


    image-20250327214153938.png (66.62 KB, 下载次数: 0)
    下载附件
    2025-4-7 13:03 上传

    Java.perform(() => {
        console.log("\n====== 环境信息 ======");
        console.log(` 进程架构: ${Process.arch}`);
        console.log(` 当前线程ID: ${Process.getCurrentThreadId()}`);
        const Build = 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 符号信息 ======");
        const NativeBridgeItf = 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}`);
    });


    image-20250327213130139.png (744.91 KB, 下载次数: 0)
    下载附件
    2025-4-7 12:52 上传

    能够看到这里我们也是把对应的成员属性的值给打印出来了
    但是虽然我们这里将这些数据都给打印出来了,但是我们其实实际上需要的参数就是loadLibrary参数,因为我们一直希望干的事情就是HOOK loadLibrary参数来类比于HOOK dlopen
    Java.perform(() => {
        console.log("\n====== 环境信息 ======");
        console.log(` 进程架构: ${Process.arch}`);
        console.log(` 当前线程ID: ${Process.getCurrentThreadId()}`);
        const Build = 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 符号信息 ======");
        const NativeBridgeItf = 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, {
            onEnter: function(args) {
                // 根据实际函数原型判断参数,这里假设第一个参数为要加载的库路径
                var libName = Memory.readCString(args[0]);
                console.log(" loadLibraryExt called with library name: " + libName);
            },
            onLeave: function(retval) {
                console.log(" loadLibraryExt returned: " + retval);
            }
        });
    });
    HOOK loadLibraryExt
    有了地址去直接HOOK,利用HOOK loadLibraryExt去实现得到在模拟器中加载过的so文件


    image-20250327223501072.png (2.06 MB, 下载次数: 0)
    下载附件
    2025-4-7 13:03 上传

    这里终于是得到了最终的结果了,也是类比于真机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}`);
        const NativeBridgeItf = Module.findExportByName("libhoudini.so", "NativeBridgeItf");
        if (!NativeBridgeItf) {
            console.error("[!] 找不到 NativeBridgeItf 符号");
            return;
        }
        console.log(` NativeBridgeItf 符号地址: ${NativeBridgeItf}`);
        // 遍历结构体指针
        console.log("\n====== 结构体指针信息 ======");
        const offsets = {
            version: 0x0, padding: 0x4, initialize: 0x8, loadLibrary: 0x10,
            getTrampoline: 0x18, isSupported: 0x20, getAppEnv: 0x28,
            isCompatibleWith: 0x30, getSignalHandler: 0x38, unloadLibrary: 0x40,
            getError: 0x48, isPathSupported: 0x50, unused_initAnonymousNamespace: 0x58,
            createNamespace: 0x60, linkNamespaces: 0x68, loadLibraryExt: 0x70,
            getVendorNamespace: 0x78, getExportedNamespace: 0x80, preZygoteFork: 0x88
        };
        Object.keys(offsets).forEach(name => {
            console.log(` ${name} addr: ${NativeBridgeItf.add(offsets[name]).readPointer()}`);
        });
        // Hook loadLibraryExt
        Interceptor.attach(NativeBridgeItf.add(offsets.loadLibraryExt).readPointer(), {
            onEnter: function(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();
                }
            },
            onLeave: function(retval) {
                console.log(` loadLibraryExt returned: ${retval}`);
            }
        });
    });
    function hook_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, {
            onEnter: function(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);
                }
            }
        });
    }
    function call_function(){
        console.log("alearly patch frida");
    }
    function hook_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, {
            onEnter: function(args) {
                var thread_ptr = args[2];
                if (thread_ptr.compare(libmsaoaidsec.base) = 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)}`);
                    [0x1c544, 0x1b8d4, 0x26e5c].forEach(offset => {
                        Interceptor.replace(libmsaoaidsec.base.add(offset),
                            new NativeCallback(() => console.log(`Interceptor.replace: 0x${offset.toString(16)}`), "void", [])
                        );
                    });
                }
            }
        });
    }


    image-20250327233428501.png (2.23 MB, 下载次数: 0)
    下载附件
    2025-4-7 12:53 上传

    这里绕过了frida检测,调用了call_function函数,打印了一串字符串
    总结:
    这里只是对于在模拟器上如何实现跨架构进行so加载的探究流程,由于时间有点短,其中很多的细节没有细致的研究,只是对于整个模拟器so加载流程做了一个小小的判断。
    我类比于hook dlopen的方法想去HOOK NativeBridgeLoadLibrary 想去得到对应的frida检测的so文件,所以去还原了NativeBridgeItf这个结构体,也是最终去得到了loadLibraryExt函数地址,进行了HOOK loadLibraryExt,也是得到了和之前手机端一样的结果了
    之后也是复现了自己在手机端的绕过frida检测,也是成功绕过了
    但是确实有了一些对于android虚拟机的理解,比如这种跨架构的过程,好比逆向过程中把每一个指令进行拦截然后用对应的架构语言去解释,然后实现在自己的架构里面进行虚拟加载,确实对于安卓虚拟机有了一些认识。

    模拟器, 下载次数

  • klop   

    有两个疑问,楼主帮忙解答一下
    1.通过nb加载arm的libmsaoaidsec.so,那这个so的x86指令实际是执行在houdini的内存中,为啥你可以通过Process.findModuleByName找到这个x86模块,并进行后续的hook?
    2.libmsaoaidsec.so通过nb转换后,pthread_create实际是通过nb的SYS_clone实现的,并不会调用pthread_create的api,如何做到hook?
    IllusionOfTimme   

    通过hook “android_dlopen_ext”,已经确定了加载了某个.so文件。那该如何找到这个.so文件的基址并hook里面的函数呢?我尝试使用Process.findModuleByName()去获取该.so的基址,结果so.base 为null直接报错。使用Process.enumerateModules()枚举应用加载的.so,里面也不包含“android_dlopen_ext”加载的.so。
    NewType   

    用心讨论,共获提升!
    grunt   

    用心讨论,共获提升!
    阿清   

    能出一期 frida  hook 雷电模拟器 so的嘛
    longxy001   

    厉害厉害,分析的很透彻
    pzdd   

    感谢分享
    zhangxg   

    学习了,感谢分享
    lopk666   

    很高深,学习了,感谢分享~
    您需要登录后才可以回帖 登录 | 立即注册

    返回顶部