在安卓应用安全领域,libnesec.so
是一个常见的安全加固库,常用于防止应用被逆向分析和调试。本文将结合实战经验,利用 Frida 绕过 libnesec.so
的反调试检测。
一、Hook android_dlopen_ext
逆向的第一步是信息收集。我们需要准确地知道 libnesec.so
何时被加载到内存中,以便抓准时机采取行动。最直接有效的方法是 Hook android_dlopen_ext
。
通过 Hook 这个函数,我们可以在其 onEnter
和 onLeave
回调中监控所有被加载的动态库。
function hook_dlopen() {
const targetLibrary = "libnesec.so";
var baseAddress = null;
Interceptor.attach(Module.findExportByName(null, "android_dlopen_ext"), {
onEnter: function (args) {
var libraryPath = args[0].readCString();
if (libraryPath && libraryPath.includes(targetLibrary)) {
console.log("[+] Loading " + targetLibrary + " from: " + libraryPath);
this.isTargetLib = true;
}
},
onLeave: function (retval) {
if (this.isTargetLib) {
console.log("[+] " + targetLibrary + " loaded, handle: " + retval);
baseAddress = Module.findBaseAddress(targetLibrary);
console.log("[+] Base address of " + targetLibrary + " is: " + baseAddress);
// 在这里执行我们的核心绕过逻辑
bypass_anti_debug(baseAddress);
this.isTargetLib = false;
}
}
});
}
二、Hook pthread_create
libnesec.so
的一个典型反调试策略是创建新的线程来执行检测任务。这样做的好处是检测逻辑不会阻塞应用主线程,同时也增加了逆向分析的难度。我们的核心任务就是找到这些“间谍”线程,并阻止它们执行。
pthread_create
是 Linux/Android 环境下创建线程的标准函数。通过 Hook 它可以捕获所有新线程的创建动作。其方法原型如下:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg);
参数解释:
- pthread_t *thread(新线程 ID)
- pthread_attr_t *attr(线程属性,默认 NULL)
- void *(*start_routine)(void *)(线程入口函数地址)
- void *arg(传递给线程的参数)
该函数的第三个参数 start_routine
是线程的入口点函数地址,通过捕获第三个参数的值再结合so文件的基地址我们便能找出内存中对应的线程地址;其代码如下:
function bypass_anti_debug(baseAddress) {
Interceptor.attach(Module.findExportByName("libc.so", "pthread_create"), {
onEnter: function (args) {
var thread_start_routine = args[2];
var offset = thread_start_routine.sub(baseAddress);
console.log("[*] New thread created with start routine at: " + thread_start_routine);
console.log(" -> Offset from libnesec.so base: " + offset);
},
onLeave: function (retval) {}
});
}
对目标 app 注入上述代码可以观察到 pthread_create 执行的内存地址与 libnesec.so 基地址的偏移量,如下图所示 0xa0d1c、0xa1ab4。
一般情况下,反调试环境检测的线程只会拉起一次,所以我们需要构建一个空的 native 函数将偏移地址为 0xa0d1c 的函数替换换掉即可。如下代码所示:
function bypass_anti_debug(baseAddress) {
Interceptor.attach(Module.findExportByName("libc.so", "pthread_create"), {
onEnter: function (args) {
var thread_start_routine = args[2];
var offset = thread_start_routine.sub(baseAddress);
console.log("[*] New thread created with start routine at: " + thread_start_routine);
console.log(" -> Offset from libnesec.so base: " + offset);
// 关键:检查是否是我们已知的反调试函数偏移
// 这个偏移地址需要通过多次运行和观察来确定,例如 0xa05a8
if (offset.toString() === "0xa0d1c") {
console.warn("[!] Anti-debugging thread detected at offset: " + offset);
// 替换它的入口点为一个空函数,让它什么都不做
Interceptor.replace(thread_start_routine, new NativeCallback(function () {
console.log("[+] Anti-debugging thread at " + offset + " neutralized.");
return ptr(0); // 线程函数通常返回一个指针
}, 'pointer', []));
}
},
onLeave: function (retval) {}
});
}
三、总结与完整代码
绕过 libnesec.so
的反调试是一个系统工程,需要将多种技术结合起来。核心思想是:
- 尽早注入:使用
frida -f
以 spawn 模式启动应用,确保我们的脚本在所有反调试逻辑执行前加载。 - 监控加载:通过 Hook
android_dlopen_ext
来捕获libnesec.so
的加载时机。 - 定位核心:Hook
pthread_create
,通过分析反调试线程入口点的偏移地址,找到关键的反调试函数。 - 釜底抽薪:使用
Interceptor.replace
将反调试函数替换为空函数。
完整代码如下:
function hook_dlopen() {
const targetLibrary = "libnesec.so";
var baseAddress = null;
Interceptor.attach(Module.findExportByName(null, "android_dlopen_ext"), {
onEnter: function (args) {
var libraryPath = args[0].readCString();
if (libraryPath && libraryPath.includes(targetLibrary)) {
console.log("[+] Loading " + targetLibrary + " from: " + libraryPath);
this.isTargetLib = true;
}
},
onLeave: function (retval) {
if (this.isTargetLib) {
console.log("[+] " + targetLibrary + " loaded, handle: " + retval);
baseAddress = Module.findBaseAddress(targetLibrary);
console.log("[+] Base address of " + targetLibrary + " is: " + baseAddress);
// 在这里执行我们的核心绕过逻辑
bypass_anti_debug(baseAddress);
this.isTargetLib = false;
}
}
});
}
function bypass_anti_debug(baseAddress) {
Interceptor.attach(Module.findExportByName("libc.so", "pthread_create"), {
onEnter: function (args) {
var thread_start_routine = args[2];
var offset = thread_start_routine.sub(baseAddress);
console.log("[*] New thread created with start routine at: " + thread_start_routine);
console.log(" -> Offset from libnesec.so base: " + offset);
// 关键:检查是否是我们已知的反调试函数偏移
// 这个偏移地址需要通过多次运行和观察来确定,例如 0xa05a8
if (offset.toString() === "0xa0d1c") {
console.warn("[!] Anti-debugging thread detected at offset: " + offset);
// 替换它的入口点为一个空函数,让它什么都不做
Interceptor.replace(thread_start_routine, new NativeCallback(function () {
console.log("[+] Anti-debugging thread at " + offset + " neutralized.");
return ptr(0); // 线程函数通常返回一个指针
}, 'pointer', []));
}
},
onLeave: function (retval) {}
});
}
hook_dlopen()
通过以下命令使用即可:
frida -U -f com.example.app -l frida_bypass_nesec.js