本文主要分析下某梆企业壳的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,用于设置 ptidtlsctid

先试试看能不能找到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 到新线程的内部结构中

那接下来就获取一下 “线程控制块” 的位置

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 就会闪退