在面对 Frida 检测导致的闪退时,首要任务是找到是哪个 SO 库触发了检测。
通常,反调试逻辑会在JNI_OnLoad 或.init_array中执行。如果 App 在加载某个特定的 SO 后立即崩溃,那么该 SO 极大概率就是检测逻辑的所在地。
核心思路
通过 Hook 系统底层的动态库加载函数,监控 App 启动过程中加载了哪些 SO 文件。观察 App 崩溃(闪退)前的最后一条加载记录,即可锁定目标。
Hook 目标:android_dlopen_ext
在 Android 7.0 (Nougat) 及更高版本中,系统加载动态库的底层实现主要依赖android_dlopen_ext。相比标准的dlopen,它提供了更丰富的扩展能力(如从文件描述符加载、指定内存空间加载等),是系统加载器的必经之路。
函数原型:
void* android_dlopen_ext(
constchar* filename,
int flag,
const android_dlextinfo* extinfo
);
侦测脚本
(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模式启动 App 并注入上述脚本:
frida -U -f com.xingin.xhs -l hook.js
现象:
控制台快速打印出一系列 SO 加载日志,随后 App 突然闪退,Frida 会话断开。
日志片段:



结论:
加载日志停留在
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;
// 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。
intpthread_create(
pthread_t *thread,
constpthread_attr_t *attr,
void *(*start_routine) (void *),
void *arg
);
我们重点关注 start_routine(arg2),它直接指向了线程要执行的代码逻辑。
2. 核心 API:Process.findModuleByAddress(ptr)
- 作用:传入一个内存地址,Frida 会自动查询进程的内存映射表(Maps),返回该地址所属的 Module 对象(包含模块名
name、基址base等信息)。 - 用途:用于判断某个函数指针到底是属于系统库(如
libc.so),还是属于我们要分析的恶意库(libmsaoaidsec.so)。
3. 为什么使用Interceptor.replace?
- 虽然
Interceptor.attach 也能监控,但在 Native 层 Hook 这种系统级函数时,replace(完全替换实现)提供了更强的控制力。 - 我们可以选择性地阻止某些恶意线程的创建(直接不调用
pthread_create_native并返回 0),从而实现“阻断反调试线程”的效果。
结果分析
运行脚本后,当控制台输出如下信息时,我们便成功捕获了检测线程:



通过输出我们可以看到一共捕获到了3条关于libmsaoaidsec.so创建线程的数据,其函数在so中的偏移量分别是:
0x1c544, 0x1b8d4, 0x26e5c
在定位到检测线程的来源后,最简单粗暴且有效的绕过方式是 “拒绝执行”。
由于libmsaoaidsec.so 中的检测逻辑通常在子线程中死循环运行,我们可以 Hookpthread_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 创建任何线程。
3.进阶:精确打击 (基于 Offset)
如果发现“一刀切”导致 App 功能异常,请使用第 2 步中获取的Offset进行精确过滤:
结果
成功绕过,程序没用被Kill,且程序正常运行:



其它绕过方法:Hook 初始化函数 (.init_proc)
SO 库的加载流程详解
Android 系统加载一个 SO 库的顺序如下:
dlopen/android_dlopen_ext:系统调用加载器,将 SO 映射到内存。
-
.init / .init_proc:执行初始化段的代码。 -
.init_array:执行初始化数组中的函数(C++ 全局构造函数等)。 JNI_OnLoad:最后执行,通常用于注册 JNI 方法。
很多强壳或检测库(如 libmsaoaidsec.so)会将反调试检测放在 .init_proc 或 .init_array 中。
如果我们在 android_dlopen_ext 的 onLeave(即加载完成)后再去 Hook JNI_OnLoad,此时 .init 系列函数早已执行完毕,检测已经触发(App 已闪退),为时晚矣。1. 寻找更早的 Hook 时机
既然onLeave 太晚,我们需要在android_dlopen_ext 的onEnter 之后,但在.init 函数执行期间介入。
最佳策略是:寻找一个在.init_proc中被调用的外部导入函数(Import)进行 Hook。so 未加载完全,无法 Hook 导出函数,也无法通过 so 基址 + 偏移的方式 Hook 函数,所以只能通过 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. 编写精确注入脚本
我们的思路如下:
-Hookandroid_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();
}
}
});
结果
App 正常启动,日志显示 Patch 成功,且没有再出现闪退。



面对在 .init 段做检测的 SO,android_dlopen_ext + __system_property_get 是一套非常经典的组合拳。它能帮我们卡住 SO 初始化的咽喉,实现最完美的早期注入。参考文章
https://arch3rn4r.github.io/2025/03/02/frida%E7%BB%95%E8%BF%87%E...