专栏名称: 吾爱破解论坛
吾爱破解论坛致力于软件安全与病毒分析的前沿,丰富的技术版块交相辉映,由无数热衷于软件加密解密及反病毒爱好者共同维护,留给世界一抹值得百年回眸的惊艳,沉淀百年来计算机应用之精华与优雅,任岁月流转,低调而奢华的技术交流与探索却
目录
相关文章推荐
51好读  ›  专栏  ›  吾爱破解论坛

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

吾爱破解论坛  · 公众号  · 互联网安全  · 2025-04-09 09:25

主要观点总结

文章主要讨论了在模拟器下对ARM架构的so文件加载的探究过程,包括环境搭建、frida的版本问题、程序流程探究,并比较了真机与模拟器在so层执行流程上的差异。通过hook dlopen函数和NativeBridgeLoadLibrary函数,获取了加载的so文件,并成功绕过了frida检测。

关键观点总结

关键观点1: 环境搭建与frida版本问题

使用了mumu模拟器进行frida监控,并解决了frida 14.2.18版本在模拟器中的兼容性问题,升级到16.0.11版本。

关键观点2: 程序流程探究

探究了android真机和模拟器在so层执行流程的差异,并成功hook了b站的dlopen函数,获取了加载的so文件。

关键观点3: 绕过frida检测

通过hook NativeBridgeLoadLibrary函数,成功获取了模拟器加载的so文件,并绕过了frida检测。

关键观点4: 模拟器加载so过程的理解

理解了模拟器加载so的过程,通过NativeBridgeLoadLibrary函数实现跨架构的SO加载,并探讨了Android Native Bridge机制的作用。

关键观点5: 新的问题与后续思考

提出了新的问题,如如何hook NativeBridgeLoadLibrary函数,以及如何得到对应的frida检测的so文件,并进行了后续的探究和尝试。


正文

请到「今天看啥」查看全文


作者 坛账号: 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的。


报错

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


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


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


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


然后就是常规的操作了,启动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


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


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

程序流程探究

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

真机测试

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

 复制代码 隐藏代码
function hook_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)


这里其实是哔哩哔哩的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, {
                 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);


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

其实是可以发现模拟器也是挂掉了,但是这里会引起思考的是为什么,在真机测试下的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() 进入

 复制代码 隐藏代码
@CallerSensitive
public static void load(String filename) {
    Runtime.getRuntime().load0(Reflection.getCallerClass(), filename);
}

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

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


进入 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 voiddlopen_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 字段)判断架构是否匹配。来确定对于构架的检测


初始化 Native Bridge

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


Native Bridge 触发

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

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


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


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


流程图

个人对于模拟器加载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}`);
    }
  });
});

发现实际上是没有这个函数的


去查看了对应的libhoudini.so文件,发现实际上的是有函数符号,但是并没有NativeBridgeLoadLibrary函数


 复制代码 隐藏代码
Java.perform(() => {
    let libnb = Module.enumerateExportsSync("libhoudini.so");
    libnb.forEach(exp => console.log(exp.name));
  });

打印了一下符号


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]}`);
      }
    });
  }
});

但是这里出现了内存报错


这里开始去了解了对于这个 NativeBridgeItf 结构体的细节内容,实际上是存在版本差异的。


1. 低版本(Android 5.0~7.0)

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

 复制代码 隐藏代码
struct NativeBridgeItf {
    // 基础字段
    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+)
 复制代码 隐藏代码
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也是固定的,那么我们其实可以去考虑得到这个结构体在对应偏移的位置去得到相应的地址


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

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

IDA分析结构体

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


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


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

还原真实的NativeBridgeItf结构体数据

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


 复制代码 隐藏代码
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}`);
});


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

但是虽然我们这里将这些数据都给打印出来了,但是我们其实实际上需要的参数就是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文件


这里终于是得到了最终的结果了,也是类比于真机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 = {
        version0x0padding0x4initialize0x8loadLibrary 0x10,
        getTrampoline0x18isSupported0x20getAppEnv0x28
        isCompatibleWith0x30getSignalHandler0x38unloadLibrary0x40,
        getError0x48isPathSupported0x50unused_initAnonymousNamespace0x58,
        createNamespace0x60linkNamespaces0x68loadLibraryExt0x70,
        getVendorNamespace0x78getExportedNamespace0x80preZygoteFork0x88
    };

    Object.keys(offsets).forEach(name => {
        console.log(${name} addr: ${NativeBridgeItf.add(offsets[name]).readPointer()}`);
    });

    // Hook loadLibraryExt
    Interceptor.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", [])
                    );
                });
            }
        }
    });
}


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

总结:

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

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

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

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


-官方论坛

www.52pojie.cn


👆👆👆

公众号 设置“星标”, 不会错过 新的消息通知
开放注册、精华文章和周边活动 等公告

图片







请到「今天看啥」查看全文