某游戏社区/商店Frida检测绕过实战
前言
逆向完某游戏后感觉还不过瘾,看了吾爱破解上有一些绕过 Frida 检测的教程,就随便找了个软件试试
本项目的初衷是研究Hook技术与Frida检测技术,请勿将其用于非法盈利等任何商业用途,所有获取的附件请在的24小时内自行删除。
准备
- IDA Pro
- hrtng(IDA 插件,用于反编译平坦化的代码)
- Frida(本文版本 17.1.3)
- 脑子
寻找目标模块
直接尝试用 Frida 附加会发现 Frida 被终止运行
这里猜测检测 Frida 的部分在某个SO中,参考 SO 加载流程:
我们可以尝试 Hook dlopen 函数来看看加载到哪个 SO 文件就终止了
function hook_dlopen() {
Interceptor.attach(Module.findGlobalExportByName("android_dlopen_ext"), {
onEnter: function(args) {
var pathptr = args[0];
if (pathptr) {
var path = ptr(pathptr).readCString();
console.log("Loading: " + path);
}
}
});
}
setImmediate(hook_dlopen);
可以发现加载到 libmsaoaidsec.so 进程就被终止了,所以 Frida 的检测大概率就在这个 SO 里
确定检测 Frida 的位置
参考 SO 加载流程,我们可以 Hook JNI_OnLoad 来看看检测是在这个函数执行之前还是之后
先用 IDA Pro 逆向看函数的地址
function hook_dlopen(soName = '') {
Interceptor.attach(Module.findGlobalExportByName("android_dlopen_ext"), {
onEnter: function(args) {
var pathptr = args[0];
if (pathptr) {
var path = ptr(pathptr).readCString();
console.log("Loading: " + path);
if (path.indexOf(soName) >= 0) {
console.log("Already loading: " + soName);
hook_JNI_OnLoad()
}
}
}
});
}
setImmediate(hook_dlopen, "libmsaoaidsec.so");
function hook_JNI_OnLoad(){
let module = Process.getModuleByName("libmsaoaidsec.so")
Interceptor.attach(module.base.add(0x13328 + 1), {
onEnter(args){
console.log("JNI_OnLoad")
}
})
}
可以发现程序找不到对应的模块,说明在 Hook 到 JNI_OnLoad 函数之前程序就被终止了,检测 Frida 的步骤在 JNI_OnLoad 之前
推测大概就在 .init_proc 中,我们需要在尽量早的地方中去 Hook
这里参照此文 Hook 在 sub_113F4 中的 _system_property_get 函数
function hook_system_property_get() {
var system_property_get_addr = Module.findGlobalExportByName("__system_property_get");
if (!system_property_get_addr) {
console.log("__system_property_get not found");
return;
}
Interceptor.attach(system_property_get_addr, {
onEnter: function(args) {
var nameptr = args[0];
if (nameptr) {
var name = ptr(nameptr).readCString();
if (name.indexOf("ro.build.version.sdk") >= 0) {
console.log("Found ro.build.version.sdk, need to patch");
}
}
}
});
}
同样是通过上面提到的文章,猜测此 SO 通过创建线程来运行检测逻辑,所以验证完上面可 Hook 后就通过 Hook pthread_create 来看线程的位置和相对于 libmsaoaidsec.so 的位置偏移
function hook_pthread_create() {
var pthread_create = Module.findGlobalExportByName("pthread_create");
var libmsaoaidsec = Process.getModuleByName("libmsaoaidsec.so");
if (!libmsaoaidsec) {
console.log("libmsaoaidsec.so not found");
return;
}
console.log("libmsaoaidsec.so base: " + libmsaoaidsec.base);
if (!pthread_create) {
console.log("pthread_create not found");
return;
}
Interceptor.attach(pthread_create, {
onEnter: function(args) {
var thread_ptr = args[2];
if (thread_ptr.compare(libmsaoaidsec.base) < 0 || thread_ptr.compare(libmsaoaidsec.base.add(libmsaoaidsec.size)) >= 0) {
console.log("pthread_create other thread: " + thread_ptr);
} else {
console.log("pthread_create libmsaoaidsec.so thread: " + thread_ptr + " offset: " + thread_ptr.sub(libmsaoaidsec.base));
}
},
onLeave: function(retval) {}
});
}
这样就找到检测的位置了
其实静态分析也可以找到创建线程的位置,不过可能需要翻很多函数,如果不去 Hook 的话可能会有点难找。如果不参考别人的文章的话我可能还得找一阵子才知道是子线程中检测的(
分析检测首发以及通过 Frida patch 掉检测部分
在 IDA Pro 中按 G 来跳转到上面提到的两个地址:0x175f8 和 0x16d30
0x16d30
0x16d30 有几个函数,分别是 sub_166F8、sub_16404、sub_16B8C、sub_112A0
这几个函数分别有不同的功能:
-
sub_166F8:通过检测/proc/[pid]/status中的TracerPid:是否为0,返回0或者TracerPid,若/proc/[pid]/status无法打开则返回 -1,有字符串混淆以及编译优化 -
sub_16404:传入了sub_166F8的返回值,检查TracerPid的父进程是不是自身,与前者混淆相同 -
sub_16B8C:遍历目录/proc/%d/task来获取当前进程所有线程,然后通过查看/proc/%d/task/%s/stat中线程是否属于追踪或暂停状态(检查状态码是否为T/t),是返回777,否则返回0
在查看 sub_112A0 的逻辑之前可以先查看 sub_16D30 的逻辑
逻辑非常清晰,结合前面的分析,可以判断前面三者检测,若检测到调试则终止程序,终止程序的函数即 sub_112A0,然后是死循环,每隔2秒执行一次这些判断函数
现在来看这个终止函数:
先解密存在全局变量里的 Arm64 字节码,和前面的逻辑相同,然后动态修改,调用系统的 exit_group 来终止当前进程中的所有子进程/线程
0x175f8
这个地方的函数和前面差不多,大部分的逻辑都是经过编译器优化的字符串解密过程,然后通过几个函数检测,一个函数决定是否终止程序,无限循环每隔4秒执行这些函数
在 0x17F84 处有几个函数调用,作用分别如下:
sub_17054:通过/proc/self/task遍历当前进程下的所有线程,然后再通过/proc/self/task/%s/status来查看这些线程的信息是否包含像gum-js-loop、gmain或linjector这样的特征信息
gmain:Frida 使用 Glib 库,其中的主事件循环被称为 GMainLoop。在 Frida 中gmain 表示 GMainLoop 的线程。
gum-js-loop:Gum 是 Frida 的运行时引擎,用于执行注入的 JavaScript 代码gum-js-loop 表示 Gum 引擎执行 JavaScript 代码的线程。
linjector 是一种用于 Android 设备的开源工具,它允许用户在运行时向 Android 应用程序注入动态链接库(DLL)文件。通过注入 DLL 文件,用户可以修改应用程序的行为、调试应用程序、监视函数调用等,这在逆向工程、安全研究和动态分析中是非常有用的。
-
sub_17200:遍历/proc/self/fd下的所有文件,查看路径中是否含有linjector这样的信息 -
sub_17314:通过/proc/self/maps扫描内存中有没有可读可执行的内存页,然后加入红黑树,后面遍历树,然后通过sub_14F88和sub_14A88这两个函数解析符号表和字符串表,检查是否包含_AGENT_1.0这样的特征 -
sub_1B420:终止进程的函数,和前面的sub_112A0差不多
绕过检测
此文的做法是将对应的退出代码 nop 掉,但是在本文的样本中,使用这个方法会卡首屏,进不去 APP,因此这里采用的方法是 Hook 前面的检测函数和终止进程函数,让其执行前直接返回
// 全局标志位,确保只执行一次
let isMsaBypassed = false;
function bypass_msa_full() {
if (isMsaBypassed) return;
isMsaBypassed = true;
let module = Process.getModuleByName("libmsaoaidsec.so");
if (!module) {
console.log("[-] libmsaoaidsec.so not found!");
return;
}
console.log("[*] Starting MSA SDK Bypass...");
function stub_func(offset, funcName, retValue, retType) {
let targetAddr = module.base.add(offset);
Interceptor.replace(targetAddr, new NativeCallback(function() {
return retType === 'pointer' ? ptr(retValue) : retValue;
}, retType, []));
}
// 反内存插桩 (Frida) 检测
stub_func(0x17054, "Task Scanner", 0, 'pointer');
stub_func(0x17200, "FD Scanner", 0, 'pointer');
stub_func(0x17314, "Maps Scanner", 0, 'int64');
// 反动态调试 (Ptrace) 检测
stub_func(0x166F8, "TracerPid Check", 0, 'int64');
stub_func(0x16404, "Tracer Whitelist Check", 1, 'pointer');
stub_func(0x16B8C, "Thread State 'T' Check", 0, 'int64');
// 终止进程函数
stub_func(0x1B420, "Shellcode Executor 1", 0, 'int64');
stub_func(0x1AECC, "Shellcode Executor 2", 0, 'pointer');
console.log("[+] All native anti-debugging checks neutralized.");
}
可以发现不是加载完那两个线程就退出了,能加载其它线程了
但是一段线程加载过后程序会直接崩溃,就像下图
绕过其它检测
崩溃信息里面有写是哪个地方让程序崩溃的,所以我们直接 Hook 掉这些函数就行
stub_func(0x131f8, "Init Array CRC Checker", 0, 'void');
stub_func(0x11260, "Integrity Check Node 1", 0, 'void');
stub_func(0x91a4, "Integrity Check Node 2", 0, 'void');
stub_func(0x18648, "Crash Trigger (SEGV_MAPERR)", 0, 'void');
绕过成功!
完整代码
function hook_dlopen(soName = '') {
Interceptor.attach(Module.findGlobalExportByName("android_dlopen_ext"), {
onEnter: function(args) {
var pathptr = args[0];
if (pathptr) {
var path = ptr(pathptr).readCString();
console.log("Loading: " + path);
if (path.indexOf(soName) >= 0) {
console.log("Already loading: " + soName);
// hook_JNI_OnLoad()
hook_system_property_get();
}
}
}
});
}
setImmediate(hook_dlopen, "libmsaoaidsec.so");
function hook_JNI_OnLoad(){
let module = Process.getModuleByName("libmsaoaidsec.so")
Interceptor.attach(module.base.add(0x13328 + 1), {
onEnter(args){
console.log("JNI_OnLoad")
}
})
}
function hook_system_property_get() {
var system_property_get_addr = Module.findGlobalExportByName("__system_property_get");
if (!system_property_get_addr) {
console.log("__system_property_get not found");
return;
}
Interceptor.attach(system_property_get_addr, {
onEnter: function(args) {
var nameptr = args[0];
if (nameptr) {
var name = ptr(nameptr).readCString();
if (name.indexOf("ro.build.version.sdk") >= 0) {
console.log("Found ro.build.version.sdk, need to patch");
// hook_pthread_create();
bypass()
//这里可以开始进行HOOK
}
}
}
});
}
function hook_pthread_create() {
var pthread_create = Module.findGlobalExportByName("pthread_create");
var libmsaoaidsec = Process.getModuleByName("libmsaoaidsec.so");
if (!libmsaoaidsec) {
console.log("libmsaoaidsec.so not found");
return;
}
console.log("libmsaoaidsec.so base: " + libmsaoaidsec.base);
if (!pthread_create) {
console.log("pthread_create not found");
return;
}
Interceptor.attach(pthread_create, {
onEnter: function(args) {
var thread_ptr = args[2];
if (thread_ptr.compare(libmsaoaidsec.base) < 0 || thread_ptr.compare(libmsaoaidsec.base.add(libmsaoaidsec.size)) >= 0) {
console.log("pthread_create other thread: " + thread_ptr);
} else {
console.log("pthread_create libmsaoaidsec.so thread: " + thread_ptr + " offset: " + thread_ptr.sub(libmsaoaidsec.base));
}
},
onLeave: function(retval) {}
});
}
function bypass()
{
bypass_msa_full();
}
// 全局标志位,确保只执行一次
let isMsaBypassed = false;
function bypass_msa_full() {
if (isMsaBypassed) return;
isMsaBypassed = true;
let module = Process.getModuleByName("libmsaoaidsec.so");
if (!module) {
console.log("[-] libmsaoaidsec.so not found!");
return;
}
console.log("[*] Starting MSA SDK Bypass...");
function stub_func(offset, funcName, retValue, retType) {
let targetAddr = module.base.add(offset);
Interceptor.replace(targetAddr, new NativeCallback(function() {
return retType === 'pointer' ? ptr(retValue) : retValue;
}, retType, []));
}
stub_func(0x17054, "Task Scanner", 0, 'pointer');
stub_func(0x17200, "FD Scanner", 0, 'pointer');
stub_func(0x17314, "Maps Scanner", 0, 'int64');
stub_func(0x166F8, "TracerPid Check", 0, 'int64');
stub_func(0x16404, "Tracer Whitelist Check", 1, 'pointer');
stub_func(0x16B8C, "Thread State 'T' Check", 0, 'int64');
// AI推测的函数名字,静态分析后发现不是这些作用
stub_func(0x131f8, "Init Array CRC Checker", 0, 'void');
stub_func(0x11260, "Integrity Check Node 1", 0, 'void');
stub_func(0x91a4, "Integrity Check Node 2", 0, 'void');
stub_func(0x18648, "Crash Trigger (SEGV_MAPERR)", 0, 'void');
stub_func(0x1B420, "Shellcode Executor 1", 0, 'int64');
stub_func(0x1AECC, "Shellcode Executor 2", 0, 'pointer');
console.log("[+] All native anti-debugging checks neutralized.");
}
后记/其它
在写这篇博客复现的时候,发现直接 Hook sub_16F6C 和 0x131f8 就能绕过,因为这两个地方一个是 init_proc 创建那些检测线程的地方,一个是前面提到的让程序退出的地方(实际上应该 Hook 这个地方调用的函数,不过因为是 init_proc 最后的部分,所以直接返回也能行)
其实我写博客的时候还想分析一下 0x131f8 处调用的函数,也就是 sub_8A5C,但是这个函数平坦化得太厉害了,用 hrtng 反编译的伪代码质量有点堪忧,感觉像赤石一样,丢给 AI 也感觉分析的有点问题,只能留给后面的我去看这里的逻辑了
OLLVM笑传之尝尝便
还有一点就是按照上面的代码运行一段时间后,有的时候进程会被终止,有的时候不会,不知道什么原因
最后,AI 真是太好用了你们知道吗
参考文章
- [Android 原创] bilibili XHS frida检测分析绕过
- 安卓中 SO 的加载
- 某款三国类题材游戏绕过Frida检测尝试
- [原创] frida常用检测点及其原理–一把梭方案
- 搞懂 Android Hook 的两大核心:PLT Hook 与 Inline Hook 全解析(注:这是前面让 AI 分析强平坦化代码后,我去搜索的一些相关资料,大概就是
sub_8A5C干了什么)
琼公网安备46010602001577号