某梆和某 OAID 对 Frida 的联合检测绕过尝试

AI 还是太好用了,逆向也真是太好玩了。

最近又有个逆向某个 App 的需求,很不巧手上的小米 17U 是 Android 16 的,Frida 的 Spawn 模式有点 bug 挂不上。好在手里还有台 M2 Pro 的 Mac mini,加上 Android 12 的 MuMu 模拟器也能使,以及这个 App 没有检测模拟器,所以还能凑合研究研究。

环境

  • macOS 版本的 MuMu 模拟器,ROM 版本为 Android 12
  • Florida 16.7.19(修改版的 Frida)

这里有个小插曲,最开始本来我用的是原版的 Frida,但因为秒挂所以尝试换了去特征的 Florida,还是不能直接过,于是才有了这篇文章。

另外不用 Frida 17 的原因是 API 有比较大的变化,网上很多资料都是基于老版本的,懒得再去改那些现成的脚本了。

先过某梆

定位检测位置

企业版的某梆其实在网上也有不少的资料了,大部分情况下照着来应该都能过。这里也写一下过程吧。

先 Hook 掉 dlopen,看看是挂在什么位置了:

function hook_dlopen(so_name) {
    Interceptor.attach(Module.findExportByName(null, "android_dlopen_ext"), {
        onEnter: function (args) {
            const pathptr = args[0];
            if (pathptr !== undefined && pathptr != null) {
                const path = ptr(pathptr).readCString();
                this.path = path;
                console.log("[android_dlopen_ext -> enter", path);
            }
        },
        onLeave: function (retval) {
            console.log("[android_dlopen_ext -> exit", this.path);
        }
    });
}

结果如下:

[Remote::com.xxx.xxx ]-> [android_dlopen_ext -> enter /data/app/~~GLEhsAE7TWfPIcSygdj3NQ==/com.xxx.xxx-RV_lLA6IJP_78wRegU4Etg==/oat/arm64/base.odex
[android_dlopen_ext -> exit /data/app/~~GLEhsAE7TWfPIcSygdj3NQ==/com.xxx.xxx-RV_lLA6IJP_78wRegU4Etg==/oat/arm64/base.odex
[android_dlopen_ext -> enter /data/app/~~GLEhsAE7TWfPIcSygdj3NQ==/com.xxx.xxx-RV_lLA6IJP_78wRegU4Etg==/lib/arm64/libDexHelper.so
[android_dlopen_ext -> exit /data/app/~~GLEhsAE7TWfPIcSygdj3NQ==/com.xxx.xxx-RV_lLA6IJP_78wRegU4Etg==/lib/arm64/libDexHelper.so
Process terminated

嗯确实是挂在了某梆的 libDexHelper.so 上。一般来说关于 Hook 的检测会开个子线程,于是我们 Hook 掉线程创建函数 pthread_create(),看看它在哪里创建了线程:

function hook_pthread_create() {
    const pth_create = Module.findExportByName("libc.so", "pthread_create");
    console.log("[pth_create]", pth_create);
    Interceptor.attach(pth_create, {
        onEnter: function (args) {
            const module = Process.findModuleByAddress(args[2]);
            console.log("pthread_create called, start_routine addr: " + args[2] + ", module: " + (module ? module.name : "unknown"));
            if (module != null) {
                const offset = args[2].sub(module.base);
                if (module.name.indexOf("libDexHelper.so") != -1) {
                    console.log("开启线程-->", module.name, offset);
                }
            }

        },
        onLeave: function (retval) {
        }
    });
}

输出如下:

Spawning `com.xxx.xxx`...                                             
[pth_create] 0x721f132a18
Spawned `com.xxx.xxx`. Resuming main thread!                          
[Remote::com.xxx.xxx ]-> pthread_create called, start_routine addr: 0x72067d57d0, module: libutils.so
pthread_create called, start_routine addr: 0x6f73023f90, module: libart.so
Process crashed: Bad access due to invalid address

并没有输出任何关于 libDexHelper.so 的线程创建调用。

根据资料(其实也是其他人问 AI 的结果),在 Android 上的 pthread_create() 函数最终会调到 clone(),其函数原型是:

int clone(typeof(int (void *_Nullable)) *fn,
                 void *stack,
                 int flags,
                 void *_Nullable arg, ...
                 /* pid_t *_Nullable parent_tid,
                    void *_Nullable tls,
                    pid_t *_Nullable child_tid */ );
参数说明
fn子线程/进程起始函数
child_stack子线程栈顶地址
flags控制资源共享与行为
arg传给 fn 的参数
ptid父线程写入子线程 PID 的地址
tls子线程 TLS 基址
ctid子线程写入自己 PID 的地址

pthread_create() 的原型是:

int pthread_create(pthread_t *restrict thread,
                          const pthread_attr_t *restrict attr,
                          typeof(void *(void *)) *start_routine,
                          void *restrict arg);
参数说明
thread指向 pthread_t 的指针,成功时返回新线程的 ID
attr指定线程属性(如优先级、栈大小),通常设为 NULL 使用默认属性
start_routine指向线程执行函数的指针,该函数接收一个 void * 参数并返回一个 void * 指针
arg传递给线程函数的参数,若无参数传入 NULL

从模拟器系统里吧 libc64.so 搞出来,丢到 IDA 里,找到 pthread_create() 的代码,这里我们主要关注 a3 参数:

__int64 __fastcall pthread_create(_QWORD *a1, __int64 a2, __int64 a3, __int64 a4)
{
  // ...
LABEL_21:
        *(_QWORD *)(v30 + 96) = a3;
        *(_QWORD *)(v30 + 104) = v52;
        *(_DWORD *)(v30 + 20) = *(_DWORD *)(v30 + 20) & 0x80000000 | getpid(inited) & 0x7FFFFFFF;
        pthread_rwlock_rdlock(&g_thread_creation_lock);
        v59 = 0LL;
        sigfillset64(&v59);
        _rt_sigprocmask(2LL, &v59, v30 + 120, 8LL);
        v36 = clone(__pthread_start, v19, 4001536LL, v30, v30 + 16, (char *)v23 + 8, v30 + 16);
  // ...
}

注意 *(_QWORD *)(v30 + 96) = a3; 这行,pthread_create 内部会把线程函数存到线程控制块里。TCB v30 会在下面传给 clone() 的第 4 个参数,因此我们只要能拿到 v30 的地址,加上 96 就是线程函数的地址了。于是我们接着 Hook 掉 clone()

function hook_clone() {
    const clone = Module.findExportByName('libc.so', 'clone');
    Interceptor.attach(clone, {
        onEnter: function (args) {
            // 只有当 args[3] 不为 NULL 时,才说明上层确实把 “线程控制块指针” 传进来了
            if (args[3] != 0) {
                // 真正的用户线程函数地址
                const addr = args[3].add(96).readPointer()
                // 根据线程函数地址 addr,找它属于哪个模块
                const so_name = Process.findModuleByAddress(addr).name;
                // 获取该 so 在进程里的基址
                const so_base = Module.getBaseAddress(so_name);
                // 获取相对于 so_base 的偏移
                const offset = (addr - so_base);
                console.log("===============>", so_name, addr, offset, offset.toString(16));
            }
        },
        onLeave: function (retval) {

        }
    });
}

终于好了,这里可以看到 libDexHelper.so 创建了三个线程。这里插一嘴,我跑脚本的时候有时候会遇到第 3 个线程没创建出来的问题。可以多运行几次来确定到底起了几个线程。

Spawned `com.xxx.xxx`. Resuming main thread!                          
[Remote::com.xxx.xxx ]-> ===============> libutils.so 0x72067d57d0 75728 127d0
===============> libart.so 0x6f73023f90 4341648 423f90
===============> libDexHelper.so 0x6f347348cc 383180 5d8cc
===============> libDexHelper.so 0x6f34735af4 387828 5eaf4
===============> libDexHelper.so 0x6f34740470 431216 69470
Process terminated

绕过检测

上面拿到了检测线程的偏移,一般来说常用的方法是直接 nop 掉,那我们就先来试试。修改一下对 dlopen 的 Hook:

function nopFunc(parg2) {
    Memory.protect(parg2, 4, 'rwx');  // 修改该地址的权限为可读可写
    const writer = new Arm64Writer(parg2);
    writer.putRet();   // 直接跳到 ret 返回地方,不返回值
    writer.flush();   // 写入操作刷新到目标内存,使得写入的指令生效
    writer.dispose();  // 释放 Arm64Writer 使用的资源。
    console.log("nop " + parg2 + " success");
}

function hook_dlopen(so_name) {
    Interceptor.attach(Module.findExportByName(null, "android_dlopen_ext"), {
        onEnter: function (args) {
            const pathptr = args[0];
            if (pathptr !== undefined && pathptr != null) {
                const path = ptr(pathptr).readCString();
                this.path = path;
                console.log("[android_dlopen_ext -> enter", path);
                if (path.indexOf(so_name) !== -1) {
                    this.match = true
                }
            }
        },
        onLeave: function (retval) {
            if (this.match) {
                const base = Module.findBaseAddress("libDexHelper.so")
                console.log(so_name, "加载成功", base);
                nopFunc(base.add(383180));
                nopFunc(base.add(387828));
                nopFunc(base.add(431216));
            }
            console.log("[android_dlopen_ext -> exit", this.path);
        }
    });
}

此时输出变成了:

Spawned `com.xxx.xxx`. Resuming main thread!                          
[Remote::com.xxx.xxx ]-> [android_dlopen_ext -> enter /data/app/~~GLEhsAE7TWfPIcSygdj3NQ==/com.xxx.xxx-RV_lLA6IJP_78wRegU4Etg==/oat/arm64/base.odex
[android_dlopen_ext -> exit /data/app/~~GLEhsAE7TWfPIcSygdj3NQ==/com.xxx.xxx-RV_lLA6IJP_78wRegU4Etg==/oat/arm64/base.odex
[android_dlopen_ext -> enter /data/app/~~GLEhsAE7TWfPIcSygdj3NQ==/com.xxx.xxx-RV_lLA6IJP_78wRegU4Etg==/lib/arm64/libDexHelper.so
libDexHelper.so 加载成功 0x6f354c3000
nop 0x6f355208cc success
nop 0x6f35521af4 success
nop 0x6f3552c470 success
[android_dlopen_ext -> exit /data/app/~~GLEhsAE7TWfPIcSygdj3NQ==/com.xxx.xxx-RV_lLA6IJP_78wRegU4Etg==/lib/arm64/libDexHelper.so
[android_dlopen_ext -> enter libframework-connectivity-jni.so
[android_dlopen_ext -> exit libframework-connectivity-jni.so
[android_dlopen_ext -> enter /data/app/~~GLEhsAE7TWfPIcSygdj3NQ==/com.xxx.xxx-RV_lLA6IJP_78wRegU4Etg==/lib/arm64/libmsaoaidsec.so
Process crashed: Bad access due to invalid address

此时卡到了另外一个 so 里,说明我们对于 libDexHelper.so 的 Hook 已经生效,其检测逻辑被成功绕过了。

再过 OAID

基础检测

libmsaoaidsec.so 是某联盟提供的用于获取设备 OAID 的 SDK 中的一个库,上面卡在这里说明里面也有针对 Frida 的检测。

同样我们还是 Hook 掉 clone() 来看看检测线程:

[android_dlopen_ext -> enter /data/app/~~GLEhsAE7TWfPIcSygdj3NQ==/com.xxx.xxx-RV_lLA6IJP_78wRegU4Etg==/lib/arm64/libmsaoaidsec.so
===============> libmsaoaidsec.so 0x6f348a7544 116036 1c544
===============> libmsaoaidsec.so 0x6f348a68d4 112852 1b8d4
===============> libmsaoaidsec.so 0x6f348b1e5c 159324 26e5c

依旧是起了三个线程。照例我们 nop 掉:

function hook_pthread_create() {
    const pth_create = Module.findExportByName("libc.so", "pthread_create");
    console.log("[pth_create]", pth_create);
    Interceptor.attach(pth_create, {
        onEnter: function (args) {
            const module = Process.findModuleByAddress(args[2]);
            console.log("pthread_create called, start_routine addr: " + args[2] + ", module: " + (module ? module.name : "unknown"));
            if (module != null) {
                const offset = args[2].sub(module.base);
                if (module.name.indexOf("libmsaoaidsec.so") != -1) {
                    console.log("开启线程-->", module.name, offset);
                    nopFunc(args[2])
                }
            }

        },
        onLeave: function (retval) {
        }
    });
}

很遗憾,没有像网上大部分资料那样直接过掉:

[pth_create] 0x721f132a18
pthread_create called, start_routine addr: 0x6f27b24544, module: libmsaoaidsec.so
开启线程--> libmsaoaidsec.so 0x1c544
nop 0x6f27b24544 success
pthread_create called, start_routine addr: 0x6f27b238d4, module: libmsaoaidsec.so
开启线程--> libmsaoaidsec.so 0x1b8d4
nop 0x6f27b238d4 success
pthread_create called, start_routine addr: 0x6f27b2ee5c, module: libmsaoaidsec.so
开启线程--> libmsaoaidsec.so 0x26e5c
nop 0x6f27b2ee5c success
pthread_create called, start_routine addr: 0x72067d57d0, module: libutils.so
Process crashed: Bad access due to invalid address

扩展检测

到这里有点卡壳了。一边想场外援助,准备找网安的同学问问怎么办,因为我其实感觉这个库应该不至于会有太恶心的逻辑;一边尝试求助于 AI 看看能不能分析出来。

Frida 挂掉的时候会打出来崩溃堆栈:

***
*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
Build fingerprint: 'vivo/V2304A/V2304A:12/W528JS/900:user/release-keys'
Revision: '0'
ABI: 'arm64'
Timestamp: 2026-04-24 19:52:47.727581362+0800
Process uptime: 0s
Cmdline: com.xxx.xxx
pid: 4409, tid: 4409, name: com.xxx.xxx  >>> com.xxx.xxx <<<
uid: 10077
signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0xb00000004c0dc
    x0  0000006f27b53730  x1  0000000000010000  x2  0000000000000001  x3  0000000000000001
    x4  0000000000000100  x5  0000000000000000  x6  0000000029aaaaab  x7  00000072265e2010
    x8  000000000000001f  x9  3c6fcf6e9be61f99  x10 000000721f153068  x11 000000721f153248
    x12 0000000000000000  x13 0000000000000000  x14 0000000000000006  x15 0000000000000069
    x16 0000006f27b4fa68  x17 0000006f27b2f950  x18 0000006f27b370ac  x19 0000006f27b5001c
    x20 000b00000004bf44  x21 0000000000000000  x22 000000722530e000  x23 00000000bb1e2113
    x24 000000000e3ba312  x25 00000000de366c1c  x26 0000000019437faa  x27 000000004f03bfb2
    x28 00000000e7e21422  x29 0000007fc8bca030
    lr  0000006f27b28d90  sp  0000007fc8bc8df0  pc  0000006f27b28cd8  pst 0000000060001000
backtrace:
      #00 pc 0000000000020cd8  /data/app/~~GLEhsAE7TWfPIcSygdj3NQ==/com.xxx.xxx-RV_lLA6IJP_78wRegU4Etg==/lib/arm64/libmsaoaidsec.so!libmsaoaidsec.so
      #01 pc 0000000000011f64  /data/app/~~GLEhsAE7TWfPIcSygdj3NQ==/com.xxx.xxx-RV_lLA6IJP_78wRegU4Etg==/lib/arm64/libmsaoaidsec.so
      #02 pc 00000000000095f8  /data/app/~~GLEhsAE7TWfPIcSygdj3NQ==/com.xxx.xxx-RV_lLA6IJP_78wRegU4Etg==/lib/arm64/libmsaoaidsec.so
      #03 pc 0000000000014824  /data/app/~~GLEhsAE7TWfPIcSygdj3NQ==/com.xxx.xxx-RV_lLA6IJP_78wRegU4Etg==/lib/arm64/libmsaoaidsec.so
      #04 pc 00000000000523b8  /apex/com.android.runtime/bin/linker64 (__dl__ZN6soinfo17call_constructorsEv+888) (BuildId: e367465bec65424363d0c2a4f565aad7)
      #05 pc 000000000003cc60  /apex/com.android.runtime/bin/linker64 (__dl__Z9do_dlopenPKciPK17android_dlextinfoPKv+2064) (BuildId: e367465bec65424363d0c2a4f565aad7)
      #06 pc 000000000003810c  /apex/com.android.runtime/bin/linker64 (__dl__ZL10dlopen_extPKciPK17android_dlextinfoPKv+116) (BuildId: e367465bec65424363d0c2a4f565aad7)
      #07 pc 00000000000010c8  /apex/com.android.runtime/lib64/bionic/libdl.so (android_dlopen_ext+16) (BuildId: 7ddd297e172d8189c0473076e7e7cbf5)
      #08 pc 0000000000000e08  <anonymous:722671e000>
***

从堆栈里可以看出来是挂在了 0x20cd8 的地方,问问无敌的 ChatGPT。

0x20cd80x20bdc 里面的一部分内容,于是我把 0x20bdc 完整地丢给了它:

那么 0x4801c 是什么呢?

LOAD:000000000004801C 6C 69 62 6D 73 61 6F 61 69 64+aLibmsaoaidsecS_0 DCB "libmsaoaidsec.so",0

由于 0x20cd8 里存在对于 0x2082c 的调用,ChatGPT 继续建议我把后者的汇编给它:

这个时候其实我感觉它有点偏题了,于是提醒了它这个崩溃是因为我绕过了某些安全检测之后出现的,然后它让我看看脚本有没有 Hook 掉 strstr/strcmp/fopen/read/dlopen 这些。由于 dlopen 是必须要 Hook 的,而我又没有改变它的逻辑,所以就不打算按照它这个方向查下去。回到上一次的结论里把 0x11f38 发给它分析一下,然后它还原出来了一段伪代码:

void *sub_11F38()
{
    uint8_t flag = *off_47ED8;
    char *name_or_path = sub_C7F4();

    if (flag != 0) {
        return sub_20BDC(name_or_path);
    }

    void *h = dlopen(name_or_path, RTLD_NOW);
    if (h != NULL) {
        return h;
    }

    name_or_path = sub_C7F4();
    return sub_20BDC(name_or_path);
}

尽管 sub_20BDC(name_or_path) 最后都会被执行,但根据 flag 的不同,会决定是直接通过 dlopen("libmsaoaidsec.so", RTLD_NOW) 拿 handle,还是扫描 linker solist,找 libmsaoaidsec.so 的 soinfo。

进一步地,ChatGPT 分析出来的崩溃链是:

sub_95C8
  -> sub_11F38
      -> sub_C7F4() 返回 "libmsaoaidsec.so"
      -> 因为 *off_47ED8 != 0
      -> sub_20BDC("libmsaoaidsec.so")
          -> sub_2082C() 获取 linker solist
          -> 按 API 31 soinfo 偏移遍历
          -> X20 变成坏 next 指针
          -> LDRB W8, [X20,#0x198] 崩溃

也就是说因为 *off_47ED8 这个 flag 不为 0 所以走到了 0x2082c 里然后崩了。

ChatGPT 给出了两种 Hook 方案,一种是让 sub_20BDC() 返回 NULL,另一种是直接替换 sub_11F38() 的实现,让它只返回 dlopen(name_or_path, RTLD_NOW)

由于崩溃来自 sub_20BDC() 里面,保险起见我们先尝试第二种方法:

function hook_sub_11F38(module) {
    const base = module.base;
    const fn = base.add(0x11F38);

    const dlopenPtr = Module.findExportByName(null, "dlopen");
    const dlopen = new NativeFunction(dlopenPtr, "pointer", ["pointer", "int"]);

    const namePtr = base.add(0x4801C); // "libmsaoaidsec.so"

    Interceptor.replace(fn, new NativeCallback(function () {
        console.log("[bypass sub_11F38] dlopen libmsaoaidsec.so");
        const h = dlopen(namePtr, 2);
        console.log("[bypass sub_11F38] handle =", h);
        return h;
    }, "pointer", []));
}

Hook 的时机我们放在 nop 之后:

nopFunc(args[2])
hook_sub_11F38(module)

此时发现 Frida 不再退出,App 也在正常运行了:

尝试执行一下 Java.perform(),打一下 MainActivity 里的函数,也可以正常完成:

于是第二个检测也被我们成功过掉了。

碎碎念

在过某梆的时候,最开始不知道为什么,nop 掉检测线程的时候不太好使,导致我一度以为这个 App 是定制版本的加固,所以还尝试了一下用 Stalker 来追踪,想要精细化 Hook。但折腾了一晚上都没成功。结果第二天下班回来再试的时候发现 nop 又好使了,很神奇吧。

另外最后又尝试了一下,对于这个 App 里的 libmsaoaidsec.so,如果只 Hook sub_11F38() 而不去 nop 掉那三个线程,也是可以过检测的。不过我看三个检测线程里并没有对它的调用,这一点有点奇怪。最初的目的达到了,就先不去深究这个问题了。

参考资料

【APP 逆向百例】某当劳 Frida 检测 - K哥爬虫 - 博客园

Stalker Trace 辅助定位 Frida 检测位置

b 站 frida 反检测分析绕过

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇