0x1 环境
案例:某黑盒v1.3.333
手机:redmi k40
frida: 16.6.6
0x2 问题
在尝试进行frida hook时发现手机正常进入,随后进程就被杀死了

需要知道是那个so在检测frida,可以hook dlopen看一下so的加载流程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| function hook_dlopen() { var android_dlopen_ext = Module.findExportByName(null, "android_dlopen_ext"); console.log("addr_android_dlopen_ext", android_dlopen_ext); Interceptor.attach(android_dlopen_ext, { onEnter: function (args) { var pathptr = args[0]; if (pathptr != null && pathptr != undefined) { var path = ptr(pathptr).readCString(); console.log("android_dlopen_ext:", path) } }, onLeave: function (retvel) { } }) }
setImmediate(hook_dlopen);
|

由so的加载流程可知,当libmsaoaidsec.so被加载之后,frida进程就被杀掉了,因此监测点在libmsaoaidsec.so中
so加载流程

Java 层:System.loadLibrary 到 dlopen
1 2 3 4 5 6 7
| static { System.loadLibrary("mylib"); }
System.load("/data/local/tmp/libmylib.so");
|
差别:
loadLibrary("mylib"):
- 会在默认搜索路径里找
libmylib.so。
- 搜索路径跟进程的
LD_LIBRARY_PATH / 系统默认路径 / app 自己的 nativeLibraryDir 有关。
load("/full/path/libxxx.so"):
- 直接给 linkermap 绝对路径,不走库名解析。
Native 层:dlopen 调用链
Java → ART → dlopen 的简化链条大致是:
- Runtime.nativeLoad(String filename, ClassLoader loader, String librarySearchPath)
- 调用到 libart 或 libnativeloader 里的加载逻辑
- 最终走到 libc 提供的
dlopen()(实现由 linker 提供)
linker 内部:真正的 ELF 加载流程
linker会先对so进行加载与链接,然后调用so的.init_proc函数,接着调用.init_array中的函数,最后才是JNI_OnLoad函数
定位检测
现在不清楚具体检测点在哪,先hook一下JNI_onLoad看看会不会触发检测
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| function hook_dlopen() { var android_dlopen_ext = Module.findExportByName(null, "android_dlopen_ext"); console.log("addr_android_dlopen_ext", android_dlopen_ext); Interceptor.attach(android_dlopen_ext, { onEnter: function (args) { var pathptr = args[0]; if (pathptr != null && pathptr != undefined) { var path = ptr(pathptr).readCString(); } }, onLeave: function (retvel) { hook_JNI_OnLoad } }) }
function hook_JNI_OnLoad(){ let module = Process.findModuleByName("libmsaoaidsec.so") Interceptor.attach(module.base.add(0xC6DC + 1), { onEnter(args){ console.log("call JNI_OnLoad") } }) } setImmediate(hook_dlopen);
|

日志没有输出,那么检测在.init_proc
0x3 解决
思路一
直接hook linker64的call_constructors来替换init_proc函数的执行
原理:
- 当 SO 被加载时,系统 Linker 会负责调用 SO 中的 .init 段和 .init_array 段(即 C++ 构造函数)。反调试检测通常最早就在这里启动。
- 这个脚本直接 Hook 了 linker64 模块内部的一个函数(偏移 0x52838 call_constructors)。
- 这个偏移通常指向 call_constructors 或者在调用构造函数之前的某个关键节点。
时机:
- 此时 libmsaoaidsec.so 已经被映射到内存(Process.findModuleByName 能找到它)。
- 但是!它的任何初始化代码、反调试线程都还没有运行。
- 这是一个**“时间静止”**的绝对安全窗口。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
| function my_hook_dlopen(soName) { var name = Module.findExportByName(null, "android_dlopen_ext"); console.log(name); Interceptor.attach(name, { onEnter: function (args) { var pathptr = args[0]; if (pathptr !== undefined && pathptr != null) { var path = ptr(pathptr).readCString(); console.log(path); if (path.indexOf(soName) != -1) { hook_call_constructors() } } }, onLeave: function (retval) { if (this.is_can_hook) { dump_so(soName); } }, }); } var first = false; function hook_call_constructors() { var linker = Process.findModuleByName("linker64"); Interceptor.attach(linker.base.add(0x52838), { onEnter: function () { var module = Process.findModuleByName("libmsaoaidsec.so"); if (module && !first) { first = true; Interceptor.replace( module.base.add(0x14400), new NativeCallback( function () { console.log("Bypassed sub_14400"); }, "void", [] ) ); Interceptor.replace( module.base.add(0x13A4C), new NativeCallback( function (vm, reserved) { console.log("[+] Fake JNI_OnLoad called!"); return 0x10006; }, "int", ["pointer", "pointer"] )) } }, }); } function main() { my_hook_dlopen("libmsaoaidsec.so"); } setImmediate(main);
|

可以直接跑通
思路二
nop检测函数
已知监测点在init_proc中,在不hook linker的情况下,思路是在.init_proc函数中找一个调用了外部函数的位置,时机越早越好

这里选取sub_123F0

接下来使用frida hook dlopen函数,当加载libmsaoaidsec.so时,在onEnter回调方法中hook _system_property_get函数,以”ro.build.version.sdk”字符串作为过滤器。
如果_system_property_get函数被调用了,那么这个时候也就是.init_proc函数刚刚调用的时候,在这个时机点可以注入我想要的代码,具体实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71
| function hook_dlopen(soName = '') { 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(); if (path.indexOf(soName) >= 0) { locate_init() } } } } ); } function locate_init() { let secmodule = null Interceptor.attach(Module.findExportByName(null, "__system_property_get"), { onEnter: function (args) { secmodule = Process.findModuleByName("libmsaoaidsec.so") var name = args[0]; if (name !== undefined && name != null) { name = ptr(name).readCString(); if (name.indexOf("ro.build.version.sdk") >= 0) { bypass() } } } } ); } function hook_pthread_create() { console.log("libmsaoaidsec.so --- " + Process.findModuleByName("libmsaoaidsec.so").base) Interceptor.attach(Module.findExportByName("libc.so", "pthread_create"), { onEnter(args) { let func_addr = args[2] console.log("The thread function address is " + func_addr) } }) } function nopFunc(parg2) { Memory.protect(parg2, 4, 'rwx'); var writer = new Arm64Writer(parg2); writer.putRet(); writer.flush(); writer.dispose(); console.log("nop " + parg2 + " success"); } function bypass(){ let module = Process.findModuleByName("libmsaoaidsec.so") nopFunc(module.base.add(0x1c544)) nopFunc(module.base.add(0x1b8d4)) nopFunc(module.base.add(0x26e5c)) }
setImmediate(hook_dlopen, "libmsaoaidsec.so")
|
实现过程中如果在system_property_get函数内hook pthread_create来找检测函数时找不到的,可能system_property_get函数触发的还不够早。因此直接打印所有 pthread_create 调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
| function hook_pthread_create() { var pth = Module.findExportByName("libc.so", "pthread_create"); console.log("[*] pthread_create =", pth);
Interceptor.attach(pth, { onEnter: function (args) {
this.start_routine = args[2]; },
onLeave: function (retval) { var entry = this.start_routine; if (!entry) return;
var entryPtr = ptr(entry); var mod = Process.findModuleByAddress(entryPtr);
var tid = Process.getCurrentThreadId(); var name = getThreadName(tid);
console.log("\n================= New Thread ================="); console.log("Thread ID :", tid); console.log("Thread Name :", name); console.log("Entry Address :", entryPtr);
if (mod) { console.log("Module :", mod.name); console.log("Offset :", entryPtr.sub(mod.base)); } else { console.log("Module : <unknown>"); }
console.log("================================================\n"); } }); }
function getThreadName(tid) { try { var f = new File("/proc/self/task/" + tid + "/comm", "r"); var name = f.readLine(); f.close(); return name.trim(); } catch (_) { return "unknown"; } }
setImmediate(hook_pthread_create);
|
找到三个检测函数

再去nop这三个函数

令人失望的是,进程依然被杀死了,样本似乎有完整性检验
思路三
fake替换
拦截pthread_create,将检测线程的入口函数替换为空函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97
|
var fake_thread_func = new NativeCallback(function (arg) { console.log(" [+] 成功拦截检测线程,当前通过假函数执行 (无害化处理)"); return ptr(0); }, 'pointer', ['pointer']);
function hook_pthread_create() { var pth_create_addr = Module.findExportByName("libc.so", "pthread_create"); console.log("[*] Hooking pthread_create at: " + pth_create_addr);
if (!pth_create_addr) { console.error("[-] 无法找到 pthread_create,脚本无法运行"); return; }
Interceptor.attach(pth_create_addr, { onEnter: function (args) {
var func_addr = args[2]; var module = Process.findModuleByAddress(func_addr); if (module != null && module.name.indexOf("libmsaoaidsec.so") !== -1) { var offset = func_addr.sub(module.base); console.log("[*] 发现 MSA 尝试创建线程 | 偏移: " + offset);
if (offset.equals(0x1c544) || offset.equals(0x1b8d4) || offset.equals(0x26e5c)) { console.warn(" [!] ⚠️ 命中反调试/检测线程 (偏移: " + offset + ")"); console.warn(" [!] 正在替换线程入口 -> fake_thread_func"); args[2] = fake_thread_func; } else { console.log(" [?] 发现未知的 MSA 线程 (偏移: " + offset + "),暂时放行..."); } } }, onLeave: function (retval) { } }); }
function monitor_dlopen() { var android_dlopen_ext = Module.findExportByName(null, "android_dlopen_ext"); if (android_dlopen_ext) { Interceptor.attach(android_dlopen_ext, { onEnter: function (args) { var pathPtr = args[0]; if (pathPtr) { var path = pathPtr.readCString(); if (path && path.indexOf("libmsaoaidsec.so") >= 0) { console.log("[+] 目标 SO 正在加载: " + path); } } } }); } }
function main() { console.log("========================================"); console.log(" MSA Anti-Debug Bypass Script Loaded "); console.log("========================================"); monitor_dlopen();
hook_pthread_create(); }
setImmediate(main);
|

进程依然被杀死了
既然不能替换入口函数,那我们来试试替换pthread_create
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
| function create_fake_pthread_create() { const fake_pthread_create = Memory.alloc(4096) Memory.protect(fake_pthread_create, 4096, "rwx") Memory.patchCode(fake_pthread_create, 4096, code => { const cw = new Arm64Writer(code, { pc: ptr(fake_pthread_create) }) cw.putRet() }) return fake_pthread_create } function hook_dlsym() { var count = 0 console.log("=== HOOKING dlsym ===") var interceptor = Interceptor.attach(Module.findExportByName(null, "dlsym"), { onEnter: function (args) { const name = ptr(args[1]).readCString() console.log("[dlsym]", name) if (name == "pthread_create") { count++ } }, onLeave: function(retval) { if (count == 1) { retval.replace(fake_pthread_create) } else if (count == 2) { retval.replace(fake_pthread_create) } else if(count==3){ retval.replace(fake_pthread_create) interceptor.detach() } } } ) return Interceptor } 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) if (path.indexOf("libmsaoaidsec.so") > -1) { hook_dlsym() } } } } ) return interceptor }
var fake_pthread_create = create_fake_pthread_create() var dlopen_interceptor = hook_dlopen()
|

下面是思路三两个脚本的对比:
- 拦截时机与切入点不同
- 脚本 A (hook_pthread_create) —— 【安检门拦截】
- 切入点:libc.so 的 pthread_create 函数。
- 逻辑:这是创建线程的必经之路(安检门)。脚本守在这里,检查每一个要过关(创建)的线程。如果发现是 MSA 的“坏人”(通过偏移量判断),就把他拦下来,换成一个“假人”(空函数)放过去。
- 时机:运行时(Runtime)。是在 MSA 已经拿到了创建线程的能力,真正要去执行创建动作的那一瞬间拦截。
- 脚本 B (hook_dlsym) —— 【供应链投毒/发假证件】
- 切入点:dlsym 函数。
- 逻辑:MSA 刚启动时,会问系统:“请给我 pthread_create 的地址,我要用它”。脚本拦截了这个请求,给 MSA 返回了一个假的地址(指向一个只有 RET 指令的内存块)。MSA 以为自己拿到了真的函数,高高兴兴地拿回去用。
- 时机:准备阶段(Resolution)。是在 MSA 还没开始创建线程,正在寻找“工具”的时候,给它一个假工具。
- 对抗“Inline Hook 检测”的能力不同
- 脚本 A (风险较高)
- 它直接 Hook 了全局的 pthread_create。
- Frida 会修改 pthread_create 的头部指令(插入跳转代码)。
- 弱点:如果 MSA 在调用 pthread_create 之前,先检查一下这个函数的头部指令是不是原厂的(检测 Inline Hook),就会发现被修改了,从而崩溃或自杀。
- 脚本 B (极度隐蔽)
- 它完全没有碰 pthread_create 函数本身。真正的 pthread_create 依然是纯净的原厂状态。
- 它只是骗了 MSA,让 MSA 去调用另一个地方。
- 优势:即使 MSA 疯狂检查 pthread_create 有没有被 Hook,也查不出任何问题,因为它确实没被 Hook。
0x4 参考
https://bbs.kanxue.com/thread-285950.htm#msg_header_h2_1
https://bbs.kanxue.com/thread-289359.htm
https://www.52pojie.cn/forum.php?mod=viewthread&tid=2008459&highlight=libmsaoaidsec.so