【APP 逆向百例】某当劳 Frida 检测
逆向分析某当劳APP apk 7.0.15.1版本,利用Frida注入发现闪退问题。通过hook dlopen监控动态库加载,发现某梆加固libDexHelper.so用于Frida检测。进一步分析so文件并hook pthread_create和clone函数,成功绕过检测。 2025-9-8 09:45:6 Author: www.freebuf.com(查看原文) 阅读量:8 收藏

7WQWGU.png

声明

本文章中所有内容仅供学习交流使用,不用于其他任何目的,不提供完整代码,抓包内容、敏感网址、数据接口等均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关!

本文章未经许可禁止转载,禁止任何修改后二次传播,擅自使用本文讲解的技术而导致的任何意外,作者均不负责,若有侵权,请在公众号【K哥爬虫】联系作者立即删除!

逆向目标

  • 目标:某当劳 APP

  • apk 版本:7.0.15.1

  • 下载地址:aHR0cHM6Ly93d3cuZG93bmt1YWkuY29tL2FuZHJvaWQvMTU0OTg1Lmh0bWw=

逆向分析

直接注入 frida 代码,frida 命令如下:

frida -U -f com.mcdonalds.gma.cn -l .\3.js

结果如下:

EPv78J.png

发现闪退,老样子,按照之前的思路,我们可以先 hook dlopen方法,监控动态库的加载情况。

dlopen 原型函数:

void *dlopen(const char *filename, int flag);
参数说明
filenameso 文件的路径,例如 "libfoo.so"或完整路径 /data/app/.../libfoo.so。传 NULL表示获取主程序自身句柄。
flag加载选项,常见值:• RTLD_LAZY:按需解析符号(调用时绑定)• RTLD_NOW:立即解析所有未定义符号 • RTLD_GLOBAL:符号导出,可被后续库使用 • RTLD_LOCAL:符号仅在本库内可见(默认)

hook android_dlopen_ext代码如下:

function hook_dlopen() {
 var android_dlopen_ext = Module.findExportByName(null, "android_dlopen_ext");
 Interceptor.attach(android_dlopen_ext, {
     onEnter: function (args) {
         var path_ptr = args[0];
         var path = ptr(path_ptr).readCString();
         console.log("[android_dlopen_ext -> enter", path);
         if (args[0].readCString() != null && args[0].readCString().indexOf("libmsaoaidsec.so") >= 0) {
             // hook_call_constructors()
             hook_pth()
        }
    },
     onLeave: function (retval) {
         console.log("android_dlopen_ext -> leave")

    }
});
}
hook_dlopen()

EPvEtG.png

可以看到,程序虽然在我们的 libmsaoaidsec.so 退出了,但是在这之前加载了一个 libDexHelper.so 文件,通过 MT 管理器查看可知,是某梆加固,而某梆加固都是 libDexHelper.so 进行 frida 检测的,所以我们应该先分析这个文件:

EPvQoB.png

so 文件分析

知道在这个 so 文件加密之后,我们直接 hook pthread_create看看创建了哪些线程,hook 代码如下:

function hook_pthread_create(){
 var pthread_create_addr = Module.findExportByName("libc.so", "pthread_create");
 console.log("pthread_create_addr: ", pthread_create_addr);
 Interceptor.attach(pthread_create_addr,{
     onEnter:function(args){
         console.log(args[2], Process.findModuleByAddress(args[2]).name);
    },onLeave:function(retval){
    }
});
}
hook_pthread_create();

EPvqyt.png

发现并没有 libDexHelper.so 相关的线程,这是什么原因呢?遇事不决问 ai,下面是 GPT 给出的部分答案:

EPvuUb.png

EPvwse.png

GPT 给出了重要结论,pthread_create最终会调用 clone方法。

clone是 Linux/Android 系统的一个 底层系统调用,用于创建 线程或进程,它比 fork更灵活,是 pthread_create的底层实现基础。

clone 函数原型如下:

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

那我们直接调用 clone 函数试看看,hook clone 代码如下:

var clone = Module.findExportByName(null, 'clone');

Interceptor.attach(clone, {
 onEnter: function(args) {
     // 获取线程函数地址
     var func = args[0];
     // 获取线程函数所在模块
     var module = Process.findModuleByAddress(func);
     if (module) {
         console.log("Thread function is located in module: " + module.name);
    }

     // 打印调用栈
     console.log("Backtrace:");
     console.log(Thread.backtrace(this.context, Backtracer.ACCURATE)
        .map(DebugSymbol.fromAddress)
        .join('\n'));
},
 onLeave: function(retval) {
     // 可在这里打印返回值或做后续处理
}
});

EPvHEP.png

发现多次调用同一个线程创建,通过下面命令,把 lib.so 文件拷贝到电脑:

adb pull /system/lib64/libc.so ./libc64.so 

ida 分析

我们用 ida 打开 libc.so 文件,搜索 pthread_create查看它的地址:

EPvKcw.png

我们主要关注上面 a3 的值,按住 tab 键找到 pthread_create的地址:

EPvbj6.png

0x7278e88aa8 libc.so!pthread_create+0x290

根据上面的输出加上偏移得到 clone 函数的最终地址为 0xAFAA8:

0x00000000000AF818 + 0x290 = 0x00000000000AFAA8

按 g 跳转到该地址,如下所示:

EPvGtf.png

clone(__pthread_start, v19, 4001536, v31, v31 + 16, v23 + 8, v31 + 16);
参数说明
__pthread_start线程函数入口(pthread 内部包装 start_routine
v19新线程栈顶地址
4001536clone flags(如 CLONE_VM)
v31入口函数参数(通常封装了 start_routine + arg)
v31 + 16父线程写入子线程 PID 的地址(ptid)
v23 + 8新线程 TLS 基址
v31 + 16子线程写自己 PID 的地址(ctid)

在 pthread 内部,线程函数会存储在线程控制块中:

*(_QWORD *)(v31 + 96) = a3;  // 将用户线程函数写入线程控制块

通过读取 v31 + 96的地址,我们可以获取实际执行的线程函数,hook 代码如下:


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) {

}
});

结果如下:

EPvxoc.png

可以看到成功输出了该 so 文件的线程函数,接下来,我们尝试着先 nop 掉这几个函数,看能否过检测,hook 代码如下:


function nopFunc(parg2) {
 Memory.protect(parg2, 4, 'rwx');  // 修改该地址的权限为可读可写
 var 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) {
         var pathptr = args[0];
         if (pathptr !== undefined && pathptr != null) {
             var path = ptr(pathptr).readCString();
             console.log("[android_dlopen_ext -> enter", path);
             if (path.indexOf(so_name) !== -1) {
                 this.match = true
            }
        }
    },
     onLeave: function (retval) {
         if (this.match) {
             console.log(so_name, "加载成功");
             var base = Module.findBaseAddress("libDexHelper.so")
             nopFunc(base.add(308204));
             nopFunc(base.add(362896));
             nopFunc(base.add(332536));
             nopFunc(base.add(366304));
             nopFunc(base.add(385348));
        }
         console.log("android_dlopen_ext -> leave")
    }
});
}
hook_dlopen("libDexHelper.so")

结果如下:

EPvAH3.png

发现我们 nop 掉线程后,并没有报错,那证明我们 nop 掉没有问题,但是卡在了最开始的 libmsaoaidsec.so 文件里,对于这个文件,我们同样直接 nop 掉里面的线程即可:


function hook_pth() {
 var pth_create = Module.findExportByName("libc.so", "pthread_create");
 console.log("[pth_create]", pth_create);
 Interceptor.attach(pth_create, {
     onEnter: function (args) {
         var module = Process.findModuleByAddress(args[2]);
         if (module != null) {
             console.log("开启线程-->", module.name, args[2].sub(module.base));
             if (module.name.indexOf("libmsaoaidsec.so") != -1) {
                 Interceptor.replace(module.base.add(0x175f8), new NativeCallback(function () {
                     console.log("替换成功")
                }, "void", ["void"]))
                 Interceptor.replace(module.base.add(0x16d30), new NativeCallback(function () {
                     console.log("替换成功")
                }, "void", ["void"]))
            }
        }

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

最终也是成功绕过了检测,结果如下:

EPvSL9.png

至此,该 app 的 frida 检测分析流程就结束了。

相关 hook 脚本,会分享到知识星球当中,需要的小伙伴自取,仅供学习交流。


文章来源: https://www.freebuf.com/articles/others-articles/447681.html
如有侵权请联系:admin#unsafe.sh