找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

3432

积分

0

好友

451

主题
发表于 2026-2-11 17:19:56 | 查看: 32| 回复: 0

定位检测源:追踪 SO 加载流程

当使用 Frida 进行动态分析时,应用如果因为检测到调试环境而闪退,我们的首要任务就是找到触发检测的根源——具体是哪个 SO 库在“作祟”。

通常,这类反调试逻辑会被放置在 JNI_OnLoad.init_array 这类初始化函数中。一个很直接的判断方法是:如果应用在加载某个特定的 SO 文件后立刻崩溃,那么这个 SO 库很可能就是检测逻辑的藏身之处。

核心思路

通过 Hook 系统底层的动态库加载函数,我们可以监控应用启动过程中加载的所有 SO 文件。观察应用闪退前的最后一条加载记录,就能精准地锁定目标库。

Hook 目标:android_dlopen_ext

在 Android 7.0 (Nougat) 及更高版本中,系统加载动态库主要依赖 android_dlopen_ext 函数。相比标准的 dlopen,它提供了更丰富的扩展选项(例如从文件描述符加载),是系统加载器的核心路径,非常适合作为监控点。

函数原型

void* android_dlopen_ext(
const char* filename,       // args[0]: SO 文件路径
int flag,                   // args[1]: 加载标志
const android_dlextinfo* extinfo // args[2]: 扩展信息结构体
);

侦测脚本

(function () {
    // 辅助函数:获取格式化时间
    function getTime() {
        return new Date().toLocaleTimeString();
    }
    // 辅助函数:终端颜色高亮
    function color(str) {
        return "\x1b[36m" + str + "\x1b[0m";
    }

    // 1. 查找 android_dlopen_ext 导出地址
    // 第一个参数传 null 表示在所有加载的模块中查找
    var dlopen = Module.findExportByName(null, "android_dlopen_ext");
    if (!dlopen) {
        console.log("[-] android_dlopen_ext not found. Try hooking dlopen instead.");
        return;
    }
    console.log("\n
  • Sniffer started on android_dlopen_ext...\n");     Interceptor.attach(dlopen, {         onEnter: function (args) {             try {                 // args[0] 是 char* 类型的路径字符串                 var pathPtr = args[0];                 if (pathPtr.isNull()) return;                 var soPath = pathPtr.readCString();                 // 过滤无效路径                 if (!soPath || soPath.trim() === "") return;                 // 过滤掉系统库,只关注 /data/ 下的应用私有库(可选)                 // if (soPath.indexOf("/system/") !== -1) return;                 console.log(`[${getTime()}] Thread:${Process.getCurrentThreadId()} Loading => ${color(soPath)}`);             } catch (e) {                 console.log("[Error] " + e);             }         }     }); })();
  • 结果分析

    使用 spawn 模式启动目标应用并注入上述脚本:

    frida -U -f com.xingin.xhs -l hook.js

    现象
    控制台会快速打印出一系列 SO 加载日志,随后应用突然闪退,Frida 会话也随之断开。

    终端显示SO加载日志,最后一行加载的是libmsaoaidsec.so

    结论
    加载日志停留在了 libmsaoaidsec.so。这清楚地表明,当系统尝试加载并初始化这个库时,其内部的反调试机制被触发,直接导致了进程崩溃。

    深入定位:追踪检测线程

    确定 libmsaoaidsec.so 是罪魁祸首后,下一步就是找出具体执行检测的代码段。通常,为了避免阻塞主线程(导致UI卡顿),反调试逻辑会在一个独立的子线程 (pthread) 中运行,进行轮询检测。

    因此,我们需要 Hook 系统底层的线程创建函数 pthread_create,监控所有由 libmsaoaidsec.so 发起的线程创建行为,并获取其线程函数的入口地址。

    侦测脚本

    (function () {
        function now() {
            return new Date().toLocaleTimeString();
        }
        // 格式化输出对齐
        function align(label, value) {
            return (label + ":").padEnd(14, " ") + value;
        }
        function color(str) {
            return "\x1b[36m" + str + "\x1b[0m";
        }
    
        var targetOnly = false;   // 若设置为 true,则过滤系统 so,只显示第三方库
    
        // 1. 获取 pthread_create 函数地址
        var pthread_create_addr = Module.findExportByName(null, "pthread_create");
        if (!pthread_create_addr) {
            console.log("[-] pthread_create not found.");
            return;
        }
    
        // 2. 定义原函数的 Native 原型,以便后续调用
        // int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
        var pthread_create_native = new NativeFunction(
            pthread_create_addr, "int", ["pointer", "pointer", "pointer", "pointer"]
        );
    
        console.log("\n===== pthread_create monitor started =====\n");
    
        // 3. 使用 Interceptor.replace 替换原函数
        Interceptor.replace(pthread_create_addr, new NativeCallback(function (p0, p1, start_routine, arg) {
            // --- 核心逻辑:反查模块信息 ---
            // 通过线程入口地址 (start_routine) 查找它属于哪个 SO 文件
            var module = Process.findModuleByAddress(start_routine);
            var soName = module ? module.name : "unknown";
            // 计算偏移量:函数绝对地址 - 模块基址 = 静态偏移 (IDA 中的地址)
            var offset = module ? "0x" + start_routine.sub(module.base).toString(16) : "N/A";
    
            // 过滤逻辑
            if (targetOnly && soName.indexOf("lib") === 0 && soName.indexOf("libc") >= 0) {
                return pthread_create_native(p0, p1, start_routine, arg);
            }
    
            // 捕获到目标 SO 创建线程时打印
            if (soName.indexOf("msaoaidsec") !== -1) {
                console.log("----------------------------------------");
                console.log(align("Time", now()));
                console.log(align("Thread TID", Process.getCurrentThreadId())); // 当前发起创建的线程
                console.log(align("Target SO", color(soName)));
                console.log(align("Entry Addr", start_routine));
                console.log(align("Offset (IDA)", color(offset))); // 重点关注这个偏移
                console.log(align("Arg", arg));
                console.log("----------------------------------------");
            }
    
            // 4. 必须执行原函数,否则应用会崩溃或线程无法创建
            return pthread_create_native(p0, p1, start_routine, arg);
        }, "int", ["pointer", "pointer", "pointer", "pointer"]));
    })();

    技术原理解析

    1. 目标函数:pthread_create
    这是 Linux/Android 创建线程的标准 POSIX API。

    int pthread_create(
    pthread_t *thread,            // arg0: 指向线程标识符的指针
    const pthread_attr_t *attr,   // arg1: 线程属性
    void *(*start_routine) (void *), // arg2: 【关键】线程启动后执行的函数指针
    void *arg                     // arg3: 传递给启动函数的参数
    );

    我们重点关注 start_routine (arg2),它直接指向了线程要执行的代码逻辑。

    2. 核心 API:Process.findModuleByAddress(ptr)

    • 作用:传入一个内存地址,Frida 会自动查询进程的内存映射表,返回该地址所属的 Module 对象(包含模块名 name、基址 base 等信息)。
    • 用途:判断一个函数指针是属于系统库(如 libc.so),还是属于我们要分析的目标库(libmsaoaidsec.so)。

    3. 为什么使用 Interceptor.replace
    虽然 Interceptor.attach 也能监控,但在 Native 层 Hook 这类系统级函数时,replace(完全替换实现)提供了更强的控制力。我们可以选择性地阻止某些恶意线程的创建(直接不调用原函数并返回 0),从而实现“阻断反调试线程”的效果。

    结果分析

    运行脚本后,当控制台输出如下信息时,便成功捕获了检测线程:

    终端显示捕获到的三条来自libmsaoaidsec.so的线程创建记录

    通过输出我们可以看到一共捕获到了3条关于 libmsaoaidsec.so 创建线程的数据,其函数在 so 中的偏移量分别是:

    0x1c544, 0x1b8d4, 0x26e5c

    实施绕过:阻断检测线程

    在精确定位到检测线程的来源后,最直接有效的绕过方式就是 “拒绝执行”

    既然 libmsaoaidsec.so 中的检测逻辑运行在独立的子线程中,我们就可以在它“出生”前将其扼杀。具体方法是 Hook pthread_create,当识别到线程创建请求来自该 SO 库时,直接返回 0(假装创建成功),但绝不调用原始的 pthread_create 函数。这样,反调试线程永远不会被真正创建,而应用的主逻辑却会认为线程已正常启动。

    绕过脚本

    (function () {
        // 辅助函数:获取当前时间
        function now() {
            return new Date().toLocaleTimeString();
        }
    
        // 1. 获取 pthread_create 地址
        var pthread_create_addr = Module.findExportByName(null, "pthread_create");
        if (!pthread_create_addr) {
            console.log("[-] pthread_create not found.");
            return;
        }
    
        // 2. 定义原函数用于后续调用
        var pthread_create_native = new NativeFunction(
            pthread_create_addr, "int", ["pointer", "pointer", "pointer", "pointer"]
        );
    
        console.log("\n===== pthread_create killer started =====\n");
    
        // 3. 替换 (Replace) 原函数
        Interceptor.replace(pthread_create_addr, new NativeCallback(function (p0, p1, start_routine, arg) {
            // 反查模块信息
            var module = Process.findModuleByAddress(start_routine);
            var soName = module ? module.name : "unknown";
            var offset = module ? "0x" + start_routine.sub(module.base).toString(16) : "N/A";
    
            // --- 核心绕过逻辑 ---
            // 检查线程入口函数是否属于 libmsaoaidsec.so
            if (soName.indexOf("libmsaoaidsec.so") !== -1) {
                // 打印日志,确认拦截生效
                console.log(`[+] \x1b[31mBLOCKED\x1b[0m Detection Thread from: ${soName} | Offset: ${offset}`);
                // 【关键】直接返回 0
                // 在 C 语言标准中,pthread_create 返回 0 代表“成功”
                // 我们欺骗 App 说线程创建成功了,但实际上什么都没做
                return 0;
            }
    
            // 对于其他正常的线程请求,放行并执行原函数
            return pthread_create_native(p0, p1, start_routine, arg);
        }, "int", ["pointer", "pointer", "pointer", "pointer"]));
    })();

    原理解析

    1. 为什么是 return 0
    pthread_create 的函数原型定义中,返回值 0 表示 Success(成功),非 0 值表示错误码。反调试逻辑通常会检查这个返回值:

    if (pthread_create(...) != 0) {
        // 创建失败,环境异常,可能被干扰 -> 退出
        exit(0);
    }

    通过返回 0,我们完美欺骗了上层逻辑,使其认为“监控线程”正在正常运行。

    2. 副作用与风险
    目前的脚本采用的是“一刀切”的处理方式:禁止该 SO 创建任何线程

    • 如果 libmsaoaidsec.so 仅用于反调试,那么此法完美。
    • 但如果该 SO 还需要创建处理业务(如生成设备标识)的合法线程,这种“核弹级”做法可能会导致业务功能失效(例如应用无法获取OAID)。

    3. 进阶:精确打击 (基于 Offset)
    如果发现“一刀切”导致应用功能异常,可以使用第 2 步中获取的 Offset 进行精确过滤,只阻断特定偏移地址处的线程创建函数。

    结果

    成功绕过,程序没有被终止,且可以正常运行:

    终端显示Frida成功运行并拦截了来自libmsaoaidsec.so的三个检测线程

    其它绕过方法:Hook 初始化函数 (.init_proc)

    上面的方法是在线程创建时进行拦截。但有些强壳或检测库会将反调试逻辑放在 SO 库加载的早期阶段,例如 .init_proc.init_array 中执行。如果我们等到 android_dlopen_ext 执行完毕后再去 Hook,可能为时已晚——检测逻辑早已触发并导致闪退。

    SO 库的加载流程详解

    Android 系统加载一个 SO 库的顺序如下:

    1. dlopen/android_dlopen_ext:系统调用加载器,将 SO 映射到内存。
    2. .init / .init_proc:执行初始化段的代码。
    3. .init_array:执行初始化数组中的函数(C++ 全局构造函数等)。
    4. JNI_OnLoad:最后执行,通常用于注册 JNI 方法。

    很多检测库(如 libmsaoaidsec.so)会将反调试检测放在 .init_proc.init_array 中。

    1. 寻找更早的 Hook 时机

    既然在 android_dlopen_extonLeave 后介入太晚,我们就需要在它执行之后,但在 .init 函数执行期间介入。

    一个有效的策略是:寻找一个在 .init_proc 中被调用的外部导入函数(Import)进行 Hook。因为 so 在初始化阶段,其内部函数还未完全就绪,无法直接 Hook,但导入函数是来自外部库(如 libc),可以提前 Hook。

    逆向分析 .init_proc
    通过 IDA 反编译 libmsaoaidsec.so.init_proc 函数,可能会发现类似以下代码:

    void init_proc()
    {
        // ...
        // 1. 调用系统属性获取函数,获取 SDK 版本
        __system_property_get("ro.build.version.sdk", v1);
        // ...
        // 2. 各种复杂的初始化和检测逻辑
        if ( (sub_25A48() & 1) == 0 )
        {
            // ...
            sub_1BEC4(); // 可能包含反调试线程创建
        }
    }

    我们发现 .init_proc 的入口处调用了 __system_property_get。这是一个极佳的 Hook 锚点!只要我们 Hook 这个系统函数,当它被调用且参数为 ro.build.version.sdk 时,就可以断定:现在正是 libmsaoaidsec.so 执行初始化的关键时刻。此时 SO 已在内存中(基址已确定),但后续的检测线程还没来得及创建。

    2. 编写精确注入脚本

    思路如下:

    • Hook android_dlopen_ext,监听 libmsaoaidsec.so 的加载。
    • 一旦加载,立即 Hook __system_property_get
    • 当捕获到 ro.build.version.sdk 属性读取时,说明目标 SO 正在初始化。
    • 此时获取 SO 基址,并直接 NOP 掉之前发现的三个检测线程的创建函数入口。
    // NOP 函数:将目标地址指令替换为 RET (直接返回)
    function nop_64(addr) {
        try {
            // 修改内存权限
            Memory.protect(addr, 4, 'rwx');
            var w = new Arm64Writer(addr);
            // 写入 RET 指令,相当于函数直接结束,不执行任何逻辑
            // 也可以使用 w.putNop(),视具体汇编逻辑而定
            w.putRet();
            w.flush();
            w.dispose();
            console.log(`[+] Patched at ${addr}`);
        } catch (e) {
            console.error(`[-] Failed to patch at ${addr}: ${e}`);
        }
    }
    
    // 核心 Hook 逻辑
    function locate_init() {
        var sys_prop_get = Module.findExportByName(null, "__system_property_get");
        Interceptor.attach(sys_prop_get, {
            onEnter: function (args) {
                var namePtr = args[0];
                if (namePtr.isNull()) return;
    
                var name = namePtr.readCString();
                // 锚点匹配:当读取 SDK 版本时,说明处于 init_proc 早期
                if (name && name.indexOf("ro.build.version.sdk") !== -1) {
                    // 此时 SO 已在内存中,可以获取基址
                    var module = Process.findModuleByName("libmsaoaidsec.so");
                    if (module) {
                        console.log(`[+] Found module base: ${module.base}`);
                        // 需要 NOP 的三个检测线程创建点的偏移 (来自前面的 pthread_create 监控)
                        // 0x1c544, 0x1b8d4, 0x26e5c
                        var offsets = [0x1c544, 0x1b8d4, 0x26e5c];
                        offsets.forEach(function(offset) {
                            var targetAddr = module.base.add(offset);
                            console.log(`
  • Patching thread creation at offset 0x${offset.toString(16)}...`);                         nop_64(targetAddr);                     });                     // Patch 完成后,可以取消 Hook 以免影响性能(可选)                     // Interceptor.detachAll();                 }             }         }     }); } // 入口:监听 DLOpen Interceptor.attach(Module.findExportByName(null, "android_dlopen_ext"), {     onEnter: function (args) {         var pathPtr = args[0];         if (pathPtr.isNull()) return;         var path = pathPtr.readCString();         if (path.indexOf("libmsaoaidsec.so") !== -1) {             console.log(`
  • Detected loading of ${path}`);             // 开启第二阶段 Hook             locate_init();         }     } });
  • 结果

    应用正常启动,日志显示 Patch 成功,且没有再出现闪退。

    终端显示成功找到模块基址并对三个偏移地址进行了补丁操作

    总结:面对在 .init 段做检测的 SO,android_dlopen_ext + __system_property_get 是一套经典的组合拳。它能帮助我们在 SO 初始化的最早时刻介入,实现精准的代码修补,是逆向工程中绕过高强度反调试的有效手段。

    用于人格化体现的动漫角色插图




    上一篇:三星Galaxy S27系列前瞻:可变光圈或将回归,应对苹果iPhone 18 Pro竞争
    下一篇:Axios 高危漏洞 CVE-2026-25639 曝光:可致 Node.js 服务崩溃
    您需要登录后才可以回帖 登录 | 立即注册

    手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

    GMT+8, 2026-2-23 11:43 , Processed in 0.859043 second(s), 41 queries , Gzip On.

    Powered by Discuz! X3.5

    © 2025-2026 云栈社区.

    快速回复 返回顶部 返回列表