本文将对结合android so加载机制绕过libsaoaidsec进行一些探索思考
使用到工具及环境
pixel 2 android 10
frida 16.1.4
bilibili 7.76.0 apk
确定存在检测机制的库
可以看到小破站app存在frida的检测机制,frida的进程过了一会就被kill了
下面就想办法绕过这个检测机制,最直接的思路就是找到检测的函数,然后hook掉它
通常app对frida的检测函数都被写在native层,要想找到检测函数,首先找到它所处的so库。那么该如何找到它所属的库呢?
我们可以从android的so加载机制入手。每加载一个自定义so库,并在加载前和加载后分别打印相关信息,如果加载到某个库后frida进程被kill,那么就说明检测机制存在于这个库
Java有两种方式加载库文件,一种是System.load(),另一种是System.loadLibrary()。android采用第二种。
追溯loadLibrary0,这个函数前面都是对参数的一些判断逻辑,包括文件名是否规范及库文件是否存在(findLibrary(libraryName)),加载库的函数是nativeLoad
追溯nativeLoad,发现它是一个jni函数
根据jni函数特定的命名规则,在源码中搜索Runtime_nativeLoad(类名_函数名)
查看LoadNativeLibrary
这里使用OpenNativeLibrary加载native库
追踪OpenNativeLibrary。如果caller_location不为nullptr,则查找导出的命名空间,并使android_dlopen_ext加载库,如果没有找到命名空间或caller_location为nullptr,则直接使用dlopen加载库
caller_location通常不会是nullptr,指向调用该方法的类的dex文件位置。所以继续跟踪android_dlopen_ext
跟进__loader_android_dlopen_ext
我们可以hook其中的android_dlopen_ext,而且它的第一个参数就是库的路径
代码如下:
function hook_android_dlopen_ext() {
Interceptor.attach(Module.findExportByName(null, "android_dlopen_ext"),
{
onEnter: function (args) {
this.fileName = args[0].readCString()
console.log(`dlopen onEnter: ${this.fileName}`)
}, onLeave: function(retval){
console.log(`dlopen onLeave fileName: ${this.fileName}`)
}
}
);
}
运行后可以看到在libmsaoaidsec库这儿终止了,说明检测机制在libmsaoaidsec中
注入时机
那么该如何定位检测函数呢?先来列举几个检测机制中可能会存在的函数:
ptrace | 用于跟踪进程,PTRACE_TRACEME 可以被用来检测当前进程是否被跟踪 |
---|---|
pthread_create | 创建新线程的函数,可以用来检测新线程的创建,有时用于检测Frida注入的线程 |
openat和open | 这些函数可以被用来检测特定的文件路径,例如:/data/local/tmp/下的Frida特征文件 |
strstr和strcmp | 字符串处理函数,可以被用来检测特定的字符串模式,如 "frida"、"gum-js-loop" 等 |
readlinkat | 用于读取符号链接指向的文件路径,可能被用来检测Frida相关的文件 |
__system_property_get | 用于获取系统属性,被用来检测Frida相关的属性 |
pthread_startPv和start_thread | 这些是与线程启动相关的函数,被用来检测线程的启动行为 |
dlsym | 用于动态链接库中符号的解析,被用来检测Frida的符号 |
其中pthread_create接收一个函数指针(图中a3),它会创建a3函数的线程
所以libmsaoaidsec如果有pthread_create,我们可以尝试hook住它,它接收的函数就有可能是检测函数。那么接下来就要解决如何hook libmsaoaidsec库中的pthread_create。
但是hook的时机很重要。我们要赶在检测函数被调用前注入,否则frida进程就被kill了
首先我们要清楚Android链接器(linker)在加载.so库时会按顺序执行.init、.init_array(init_array是ELF文件格式中的一部分,它包含了一个函数指针数组)和JNI_OnLoad。检测函数
大概率会在这两个时期中的某一个启动。
.init、.init_array
.init、.init_array是在do_dlopen中执行的,我们回到先前分析的调用链的最后:do_dlopen
soinfo* si = find_library(ns, translated_name, flags, extinfo, caller);
find_library函数来查找指定的库。这个函数根据命名空间ns、转换后的库名translated_name、加载标志flags、扩展信息extinfo和调用者caller来查找库。如果找到了库,si将指向库的soinfo结构;如果没有找到,si将为nullptr
si->call_constructors();
call_constructors负责调用库中的.init段和.init_array段中的函数
JNI_OnLoad
JNI_OnLoad函数的执行是在LoadNativeLibrary函数中
可以回到调用链的LoadNativeLibrary,可以看到JNI_Onload的相关调用代码,而且这些代码是在OpenNativeLibrary之后的
所以我们可以在原先的hook脚本中假如对jni_onload是否调用的判断,来确定检测机制调用的时机
function hook_android_dlopen_ext() {
Interceptor.attach(Module.findExportByName(null, "android_dlopen_ext"),
{
onEnter: function (args) {
this.fileName = args[0].readCString()
console.log(`dlopen onEnter: ${this.fileName}`)
}, onLeave: function(retval){
console.log(`dlopen onLeave fileName: ${this.fileName}`)
if(this.fileName != null && this.fileName.indexOf("libmsaoaidsec.so") >= 0){
let JNI_OnLoad = Module.getExportByName(this.fileName, 'JNI_OnLoad')
console.log(`dlopen onLeave JNI_OnLoad: ${JNI_OnLoad}`)
}
}
}
);
}
结果发现在JNI_Onload没被打印了。所以检测机制在JNI_OnLoad之前,即.init,.init_array执行中。因此我们可以尝试hook call_constructors,在onEnter中注入代码
先找到call_constructors的offset
使用adb把设备的linker64拉下来
/apex/com.android.runtime/bin/linker64
于是可以编写hook到call_constructors
let linker64_base_addr = Module.getBaseAddress('linker64')
let offset = 0x50cf8 // __dl__ZN6soinfo17call_constructorsEv
let call_constructors = linker64_base_addr.add(offset)
时机把握好了之后,我们就可以尝试hook pthread_create
在android系统中,pthread_create位于libc.so,hook代码如下:
function hook_pthred_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 Called function address is: ${func_addr}`)
}
})
}
整体代码如下:
确定检测函数
执行脚本
在输出中,我们看到了两个不同的函数地址被多次提及:
0x7ba4cbcaa8被多次提及。
0x7ae60228d4和0x7ae602de5c也被多次提及。
而根据输出,libmsaoaidsec.so的基地址是0x7ae6007000。我们可以通过计算这些函数地址与基地址的差值与libmsaoaidsec.so的大小的比较来判断它们是否属于同一个库。
先来看看库的大小
对于0x7ae60228d4和0x7ae602de5c:
0x7ae60228d4-0x7ae6007000=0x1b8d4
0x7ae602de5c-0x7ae6007000=0x26e5c
这两个地址与基地址的差值都在合理的范围内,表明它们很可能是libmsaoaidsec.so中的函数。
对于 0x7ba4cbcaa8:
0x7ba4cbcaa8-0x7ae6007000=0xbecb5aa8
这个差值远远超出了libmsaoaidsec.so的大小699ff0,因此它不可能是libmsaoaidsec.so中的函数。这个地址很可能属于另一个库或应用程序的其他部分
adb pull下libmsaoaidsec.so,用IDA打开libmsaoaidsec.so,查找偏移
查看0x1b8d4地址处对应的函数
这个函数是一个死循环,它会不停的调用三个函数 sub_1AE48、sub_1AB54、sub_1B730
sub_1AE48
从/proc/pid/status文件中提取并返回TracerPid的值,并根据该值判断进程是否正在被调试
sub_1AB54
打开sub_1AE48中返回的TracerPid进程的/proc/pid/status文件
sub_1B730
遍历线程的stat文件,如果状态为t则返回777
sub_234E0,这个函数进行了动态解密,执行代码退出进程的操作
于是大致可以看出sub_1b8d4存在检测逻辑。
ctrl+X 使用交叉引用向上查找,这里的v26应该就是pthred_create
而调用v26的则是sub_1B924
接下来我们可以替换创建检测进程的函数sub_1B924来绕过frida检测
最终代码:
function hook_dlopen() {
Interceptor.attach(Module.findExportByName(null, "android_dlopen_ext"),
{
onEnter: function (args) {
this.fileName = args[0].readCString()
console.log(`dlopen onEnter: ${this.fileName}`)
if (this.fileName !== undefined && this.fileName.indexOf("libmsaoaidsec.so") >= 0) {
hook_linker_call_constructors()
}
}, onLeave: function (retval) {
console.log(`dlopen onLeave fileName: ${this.fileName}`)
if (this.fileName != null && this.fileName.indexOf("libmsaoaidsec.so") >= 0) {
let JNI_OnLoad = Module.getExportByName(this.fileName, 'JNI_OnLoad')
console.log(`dlopen onLeave JNI_OnLoad: ${JNI_OnLoad}`)
}
}
}
);
}
function hook_linker_call_constructors() {
let linker64_base_addr = Module.getBaseAddress('linker64')
let offset = 0x50cf8 // __dl__ZN6soinfo17call_constructorsEv
let call_constructors = linker64_base_addr.add(offset)
let listener = Interceptor.attach(call_constructors,{
onEnter:function(args){
console.log('hook_linker_call_constructors onEnter')
let secmodule = Process.findModuleByName("libmsaoaidsec.so")
if (secmodule != null){
// hook_pthred_create()
hook_sub_1b924()
listener.detach()
}
}
})
}
function hook_pthred_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 Called function address is: ${func_addr}`)
}
})
}
function hook_sub_1b924() {
let secmodule = Process.findModuleByName("libmsaoaidsec.so")
Interceptor.replace(secmodule.base.add(0x1B924), new NativeCallback(function () {
console.log(`hook_sub_1b924 >>>>>>>>>>>>>>>>> replace`)
}, 'void', []));
}
setImmediate(hook_dlopen)
参考文章
https://xiaoeeyu.github.io/2024/08/09/%E7%BB%95%E8%BF%87%E7%88%B1%E5%A5%87%E8%89%BAlibmsaoaidsec-so%E7%9A%84Frida%E6%A3%80%E6%B5%8B/
http://www.yxfzedu.com/article/8993
文章评论