绕过某邦企业壳root、frida检测
本文主要分析下某梆企业壳的frida反调试
环境
设备:pixel 5(Android11已root)
app平台:Android
app版本:4.66.0
工具:
抓包:Postern + Charles
LSPosed版本:1.9.2
Magisk版本:28.1
查壳

- 检测结果表明是梆梆的壳,并且有root、模拟器检测以及各种反调试检测
绕过检测
绕过 root 检测

- 对此我们采用隐藏 Magisk + Shamiko 的方式来绕过 root 检测


- 配置好之后再打开 app 就不会检测到设备已经被 root 了
绕过 frida
在设备上运行frida-server的时候app会直接闪退

- 我们只是在设备上运行了 frida-server 在没有执行脚本的情况下 app 就会闪退
端口检测
那么,可能是对 frida-server 的默认监听端口 27042 有检测

- 修改端口后,打开app就不会闪退了
那接下来尝试执行一下 frida-hook 脚本是否能正常执行

- 可以看到有针对 frida-agent 的检测
agent 检测
通过前面查壳的结果可知,梆梆的壳的检测点在 libDexHelper.so 中。
这里的退出的提示表明是进程中的某个线程杀死了我们的注入。
hook pthread_create
那么我们就先 hook pthread_create
试试看能不能定位检测函数的位置
函数原型:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg);
thread
: 返回创建的线程 ID。
attr
: 线程属性(可以为 NULL)。
start_routine
: 线程执行函数。
arg
: 传入线程函数的参数。
function hook_pthread_create(){
var pthC_addr = Module.findExportByName("libc.so", "pthread_create");
console.log("pthC_addr >> ", pthC_addr);
Interceptor.attach(pthC_addr, {
onEnter:function(args){
console.log(args[2], Process.findModuleByAddress(args[2]).name);
}, onLeave:function(retval){
}
});
}
hook_pthread_create();
执行:
- 这里我们的脚本还是被干掉了,那可能是对
pthread_create
这个方法进行了hook检测
- 这里我们的脚本还是被干掉了,那可能是对
pthread_create
的调用流程
pthread_create()
↓
分配线程控制块(TCB)、栈空间等
↓
设置调度策略/属性(可选)
↓
调用 clone()
↓
内核创建 task_struct(共享 mm、fs、files、sighand 等)
↓
新线程执行 start_routine(arg)
- 那我们尝试调用更深一层的
clone()
试试
函数原型:
int clone(int (*fn)(void *), void *child_stack, int flags, void *arg, ... /* pid_t *ptid, struct user_desc *tls, pid_t *ctid */ );
参数 含义 fn
子进程/线程启动后要执行的函数指针,类型为 int (*fn)(void *)
child_stack
指向为子线程分配的栈顶(栈向下增长) flags
控制资源共享与行为的标志位(如 CLONE_VM
,CLONE_THREAD
等)arg
传给 fn
的参数,即fn(arg)
其余参数(可选) 只在某些 flags
开启时需要,比如CLONE_PARENT_SETTID
,CLONE_CHILD_SETTID
,用于设置ptid
、tls
、ctid
等
先试试看能不能找到clone()
是在哪个模块中调用的,和它的调用位置
var clone = Module.findExportByName(null, 'clone');
Interceptor.attach(clone,{
onEnter: function(args){
// 获取线程函数地址
var thread_func = args[0];
// 获取线程函数所在的模块
var module_name = Process.findModuleByAddress(thread_func);
if(module_name){
console.log("Thread function is located in module: " + module_name.name);
}
// 打印调用栈
console.log("Backtrace: ");
console.log(Thread.backtrace(this.context, Backtracer.ACCURATE).map(DebugSymbol.fromAddress).join('\n'));
}, onLeave:function(retval){
}
});
执行:
frida -H 127.0.0.1:6688 -f com.bybit.app -l test.js
执行结果:
Spawned `com.bybit.app`. Resuming main thread! [Remote::com.bybit.app ]-> Thread function is located in module: libc.so Backtrace: 0x71546cd5d4 libc.so!pthread_create+0x24c
- 根据打印的结果现在我们去 libc.so 中的
pthread_create+0x24c
处看看
直接跳转过去 F5 查看伪代码
__int64 __fastcall pthread_create(_QWORD *a1, __int64 a2, __int64 a3, __int64 a4) { ... *(_QWORD *)(v30 + 96) = a3; ... v32 = clone(__pthread_start, v18, 4001536LL, v30, v30 + 16, v22 + 8, v30 + 16); }
- 根据上述代码可知
a3
: 对应 start_routine(线程回调函数)*(_QWORD *)(v30 + 96) = a3
: 保存 start_routine 到新线程的内部结构中
- 根据打印的结果现在我们去 libc.so 中的
那接下来就获取一下 “线程控制块” 的位置
var clone = Module.findExportByName('libc.so', 'clone');
Interceptor.attach(clone, {
onEnter: function(args) {
// 只有当 args[3] 不为 NULL 时,才说明上层确实把 “线程控制块指针” 传进来了
if(args[3] != 0){
// 真正的用户线程函数地址
var addr = args[3].add(96).readPointer()
// 根据线程函数地址 addr,找它属于哪个模块
var so_name = Process.findModuleByAddress(addr).name;
// 获取该 so 在进程里的基址
var so_base = Module.getBaseAddress(so_name);
// 获取相对于 so_base 的偏移
var offset = (addr - so_base);
console.log("===============>", so_name, addr,offset, offset.toString(16));
}
},
onLeave: function(retval) {
}
});
执行结果:
- 打印出了 libDexHelper.so 创建的几个线程的位置
把 libDexHelper.so 创建的几个线程都 nop 掉试试
function hook_dlopen(so_name) {
// 参数 args[0] 就是即将要加载的 so 文件路径(C 字符串指针)
Interceptor.attach(Module.findExportByName(null, "android_dlopen_ext"), {
onEnter: function (args) {
// args[0] 是一个指向 char* 的指针,指向要加载的 so 路径
var pathptr = args[0];
if (pathptr !== undefined && pathptr != null) {
// 读取这个路径对应的 C 字符串
var path = ptr(pathptr).readCString();
// 如果路径里包含了我们关心的 so 名称,就把 this.match 标记为 true
if (path.indexOf(so_name) !== -1) {
this.match = true;
}
}
},
onLeave: function (retval) {
// 当 android_dlopen_ext 返回时,如果 onEnter 已经标记了 match,就说明 libDexHelper.so 加载完毕
if (this.match) {
console.log(so_name + " 加载成功");
// 找到 libDexHelper.so 在进程里实际映射的基址(Memory 仓库地址)
var base = Module.findBaseAddress(so_name);
if (base === null) {
console.error("!!加载成功,但未找到基址:", so_name);
return;
}
// 下面对之前打印出来的几个偏移(相对于基址)位置,逐个进行 NOP 补丁
patch_func_nop(base.add(346132));
patch_func_nop(base.add(332548));
patch_func_nop(base.add(376884));
patch_func_nop(base.add(378220));
patch_func_nop(base.add(403656));
}
}
});
}
function patch_func_nop(addr) {
// Memory.patchCode 用来在指定地址范围内进行写入,并在写入结束后自动恢复页面权限
// 这里长度写 8,表示我们要覆盖 8 个字节(ARM64 下两条指令分别占 4 字节)
Memory.patchCode(addr, 8, function (code) {
// ARM64 下的 NOP 指令编码:0x1F2003D5,但有时写成 0xE0 0x03 0x00 0xAA / 0xC0 0x03 0x5F 0xD6
// 这里我们分两次写,分别覆盖两条指令:
// 第一条:mov x0, x0 (等同于 nop) -> 0xE00300AA
code.writeByteArray([0xE0, 0x03, 0x00, 0xAA]);
// 第二条:nop -> 0xC0035FD6
code.writeByteArray([0xC0, 0x03, 0x5F, 0xD6]);
console.log("patch code at " + addr);
});
}
hook_dlopen("libDexHelper.so");
执行结果:
- 搞定,现在 app 就不会检测到 frida-agent 闪退了。
脱壳
脱壳试了frida-dexdump、fart 8没脱掉,暂时搞不定

fart 8也是启动 app 就会闪退
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 xiaoeryu!
评论