吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 1866|回复: 25
收起左侧

[Android 原创] 关于libmsaoaidsec.so反Frida

  [复制链接]
BubbleC 发表于 2025-2-22 02:23

前言

最近在分析绕过frida检测的姿势,看到libmsaoaidsec.so 的使用还是挺多的,比如某站、某书和某艺。

既然有这么多样本,那就来分析一下检测逻辑,顺便感受一下不同版本之间该.so有哪些变化。
注意!

样本一是比较早的经典版本,想看较新的绕过思路可直接看样本二。
【环境】:Redmi(android 8)、v8a、Frida16

样本一

【某app v7.26.1版本】

先定位so,hook_dlopen可见加载libmsaoaidsec.so后,frida进程就被杀掉了,因此监测点在libmsaoaidsec.so中。

function hook_dlopen(){
  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("load"+path);
                         }
                       }
                     });

}
setImmediate(hook_dlopen)

接下来要定位函数

ida打开libmsaoaidsec.so找到JNI_Onload函数的偏移地址,尝试hook JNI_Onload函数,如果能成功hook将打印日志“call JNI_Onload”,没有就说明frida检测在hook JNI_Onload前。

运行后没有打印日志,检测在init_xxx。

需要hook .init_xxx的函数,但这里有一个问题,dlopen函数调用完成之后.init_xxx函数已经执行完成了,这个时候不容易使用frida进行hook,这个问题其实很麻烦的,因为想要hook linker的call_function并不容易,这里面涉及到linker的自举。

这里学到的思路是在.init_proc函数中找一个调用了外部函数的位置,时机越早越好。

导出表搜索只搜索到.init_proc

f5看到.init_proc被混淆了,很明显的虚假控制流。不过这里在最开始调用了一个外部函数sub_B1B4,这个时候也就是.init_proc函数刚刚调用的时候,在这个时机点是个注入的好时机。
1.png

int sub_B1B4()
{
  int v1[8]; // [sp+0h] [bp-20h] BYREF

  v1[3] = *(_DWORD *)off_1FC04;
  v1[1] = 0;
  v1[0] = 0;
  _system_property_get("ro.build.version.sdk", v1);
  return atoi((const char *)v1);
}

在获取了一个非常早的注入时机之后,就可以定位具体的frida检测点了。

这里对pthread_create函数进行hook,可以看到这里有两个线程地址和其他的不一样,说明这两个线程是libmsaoaidsec.so创建的。

2.png

这里需要计算一下偏移地址:

0xb54d4129 - 0xb54c3000 = 0x11129
0xb54d3975 - 0xb54c3000 = 0x10975

0x11129和0x10975是线程需要执行的函数的地址,也就是pthread_create函数的第三个参数。

而我们需要通过交叉引用找到pthread_create函数被调用的地址。

G键搜索11129找到sub_11128,然后通过x键交叉引用找到目标函数sub_113E0,同理,搜索10975找到sub_10974,然后交叉引用找到目标函数sub_109A8

然后直接把pthread_create函数nop掉即可。

样本二

v8.31.0

【另一款也使用libmsaoaidsec.so的app】

这里用上面定位frida检测的hook脚本直接定位到libmsaoaidsec.so

这里为了避免遇到字符串加密,而是将注意力放在分析frida检测逻辑上,就直接从内存中dump出libmsaoaidsec.so然后修复得到libmsaoaidesc_fixed.so

ida打开,由于前面的分析可以直接定位到.init_proc,反编译看到有很多while一看便知是虚假控制流,然后发现代码与样本一的代码有些细微的变化,比如此处就没有之前较明显的外部函数可以hook了。那我们就来分析下具体逻辑。

这里直接使用D810去除混淆,然后来分析代码逻辑看看做了哪些检测的操作。

void init_proc()
{
  const char *v0; // x23
  __int64 v1; // x0
  __int64 v2; // x0
  unsigned __int64 StatusReg; // [xsp+0h] [xbp-870h] BYREF
  __int64 *v4; // [xsp+8h] [xbp-868h]
  int v6; // [xsp+18h] [xbp-858h]
  int v7; // [xsp+1Ch] [xbp-854h]
  __int64 *v8; // [xsp+20h] [xbp-850h]
  char *v10; // [xsp+30h] [xbp-840h]
  FILE *v11; // [xsp+38h] [xbp-838h]
  char v12[2000]; // [xsp+40h] [xbp-830h] BYREF
  __int64 v13; // [xsp+810h] [xbp-60h]

  StatusReg = _ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2));// 获取寄存器状态
  v13 = *(_QWORD *)(StatusReg + 40);
  v4 = (__int64 *)(&StatusReg - 250);
  *(_DWORD *)off_47FB8 = sub_123F0();           // 获取SDK版本
  sub_12550();                                  // 判断art/devm
  sub_12440();                                  // 获取版本信息
  if ( *(_DWORD *)off_47FB8 > 23 )
    *(_BYTE *)off_47ED8 = 1;
  if ( (sub_25A48() & 1) == 0 )
  {
    v10 = v12;
    memset(v10, 0, 0x7D0u);
    getpid();
    _cxa_finalize(v12);
    v11 = fopen(v12, "r");
    if ( v11 )
    {
      v8 = v4;
      memset(v4, 0, 0x7D0u);
      v0 = (const char *)v4;
      strdup((const char *)v11);
      fclose(v11);
      if ( !strchr(v0, 58) )
        sub_1BEC4();                            // anti-frida
    }
    v1 = sub_13728();
    sub_23AD4(v1);
    v6 = sub_C830();
    if ( v6 != 1 || (v7 = sub_95C8()) != 0 )
      sub_9150();
  }
  if ( *(_QWORD *)(StatusReg + 40) != v13 )
  {
    v2 = _strlcpy_chk();                        // check标志位
    sub_148A0(v2);
  }
}

可以定位到关键函数sub_1BEC4,来分析一下:

__int64 sub_1BEC4()
{
  unsigned __int64 StatusReg; // x19
  __int64 v1; // x0
  __int64 v3; // x0
  __int64 v4; // [xsp+8h] [xbp-48h]

  StatusReg = _ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2));
  v4 = *(_QWORD *)(StatusReg + 40);
  HIDWORD(qword_49010) = getpid();
  v1 = sub_1B924();                             // check2
  if ( *(_QWORD *)(StatusReg + 40) == v4 )
    return 0LL;
  v3 = _strlcpy_chk(v1);
  return sub_1BFAC(v3);                         // check1 检测/proc/pid/tast下线程:gum-js-loop和gmain
}

这里介绍下task和fd目录下的具体检测特征。

检测/proc/pid/tast下线程
/proc/pid/task目录下可以查看不同线程的子目录,获取每个线程运行时信息,这些信息包括线程的状态、线程的寄存器内容、线程占用的CPU时间、线程的堆栈信息等。

开启frida调试后这个task目录下会多出几个线程,检测点在查看这些多出来的线程是否和frida调试相关。        

gmain:Frida 使用 Glib 库,其中的主事件循环被称为 GMainLoop。在 Frida 中,gmain 表示 GMainLoop 的线程。

gdbus:GDBus 是 Glib 提供的一个用于 D-Bus 通信的库。在 Frida 中,gdbus 表示 GDBus 相关的线程。

gum-js-loop:Gum 是 Frida 的运行时引擎,用于执行注入的 JavaScript 代码。gum-js-loop 表示 Gum 引擎执行 JavaScript 代码的线程。

pool-frida:Frida 中的某些功能可能会使用线程池来处理任务,pool-frida 表示 Frida 中的线程池。

/proc/pid/fd

该目录的作用在于提供了一种方便的方式来查看进程的文件描述符信息,通过查看文件描述符信息,可以了解进程打开了哪些文件、网络连接等。

linjector是一种用于 Android 设备的开源工具,它允许用户在运行时向 Android 应用程序注入动态链接库(DLL)文件。通过注入 DLL 文件,用户可以修改应用程序的行为、调试应用程序、监视函数调用等,这在逆向工程、安全研究和动态分析中是非常有用的。

chek2:sub_1B924->sub_1CEF8->sub_1C544

chek1比较简单一眼就看到,下面详细介绍check2。

sub_1B924中一眼看到定位到_strlcpy_chk()

result = (__int64)dlopen(v19, 2);
  v21 = (char *)result;
  if ( result )
  {
    v24 = atoi(v21);
    v25 = (void (__fastcall *)(__int64 *, _QWORD, void *, _QWORD))v24;
    if ( (unsigned int)sub_CAA8() == 248 && (sub_12D9C(0LL) & 1) == 0 )
      sub_1CEF8(v25);
    if ( (unsigned int)sub_CAE8() == 249 )
      v25(&qword_49650, 0LL, &loc_1B8D4, 0LL);
    else
      sub_1B380(v21, v25);
    v23 = sub_CA28();
    if ( v23 == 167 )
      v25(&qword_49658, 0LL, &loc_19E0C, 0LL);
    result = dlclose(v21);
  }
  if ( *(_QWORD *)(StatusReg + 40) != v28 )
  {
    _strlcpy_chk(result);
    return sub_1BEC4();
  }

交叉引用定位到关键变量v25,进而定位到sub_1CEF8。

sub_1CEF8一开头就看到加载了“libart.so”,然后一堆异或和取余操作,应该是在加解密(这里就不还原算法了),根据经验合理猜测这是通过libc.so的pthread_create创建线程实例来anti-frida。

继续往下分析发现关键代码块为LABEL__35:

LABEL_35:
  if ( (sub_25B30(v15) & 1) != 0 )
    sub_234E0(0LL);
  result = a1(&v42, 0LL, sub_1C544, v15);
  if ( *(_QWORD *)(StatusReg + 40) != v44 )
  {
    v41 = _strlcpy_chk(result);
    return std::_Rb_tree<std::string,std::string,std::_Identity<std::string>,std::less<std::string>,std::allocator<std::string>>::_M_erase(v41);
  }

看到老朋友_strlcpy_chk同理,定位到sub_1C544,一路分析定位到LABEL_81,最终的检测写在这个while循环里:

                                      while ( 1 )
                                      {
LABEL_81:
                                        v103 = sub_1BFAC();// check1,gum-js-loop和gmain
                                        v104 = sub_1C158(v103);// chek3,检测linjector
                                        sub_1C26C(v104);// check4,检测/data/local/tmp和frida-agent
                                        sub_26334(a1);// check5,mmap
                                        sleep(4u);
                                      }
                                    }
                                  }

分析结束,下面是hook
这里插入关于linker的讨论
我们已经知道dlopen(android_dlopen_ext)将动态库加载到内存中,加载库后,系统会处理库中的依赖关系,并在库的地址空间中映射所有符号。

然后执行.init_array.init_proc 段。

ELF 文件中 .init_proc 段的地址存储在 .init_array 表中。

在加载库后,系统会调用 call_constructors 来执行 .init_array 中的所有初始化函数,.init_proc 中的代码一般由开发者定义,它可能会调用一些重要的逻辑函数,比如初始化全局变量,设置反调试、密钥初始化等。

由此可知, 如果库中有重要的保护逻辑或初始化算法,它们通常会出现在 .init_proc 或由 .init_proc 调用的函数中。  

这里有个关键的地方在于, call_constructors的执行过程,即:linker64中dlopen通过do_dlopen->call_constructors->call_array

整个流程借用正己大佬给的流程图,更清晰:
3.png
所以我们从call_constructors入手。sub_1BEC4 是一个位于 libmsaoaidsec.so 中的关键函数,但在动态库完成加载前,它的地址并未映射到内存中。而call_constructors 是库完成加载后,首次执行初始化逻辑的地方,此时目标库的基地址已经确定,所有符号地址也已经可以解析。

使用objdump查看android_dlopen_extcall_constructors相对linker64的偏移地址,然后hook:

function hook_android_dlopen_ext() {
    var linker64_base_addr = Module.getBaseAddress("linker64")
    var android_dlopen_ext_func_off = 0x8f74
    var android_dlopen_ext_func_addr = linker64_base_addr.add(android_dlopen_ext_func_off)
    Interceptor.attach(android_dlopen_ext_func_addr, {
        onEnter: function (args) {
            if (args[0].readCString() != null && args[0].readCString().indexOf("libmsaoaidsec.so") >= 0) {
                hook_call_constructors()
            }
        },
        onLeave: function (ret) {

        }
    })
}

function hook_call_constructors() {
    var linker64_base_addr = Module.getBaseAddress("linker64")
    var call_constructors_func_off = 0x20b78
    var call_constructors_func_addr = linker64_base_addr.add(call_constructors_func_off)
    var listener = Interceptor.attach(call_constructors_func_addr, {
        onEnter: function (args) {
            var module = Process.findModuleByName("libmsaoaidsec.so")
            if (module != null) {
                Interceptor.replace(module.base.add(0x1BEC4), new NativeCallback(function () {
                    console.log("replace sub_1BEC4")
                }, "void", []))
                listener.detach()
            }
        },
    })
}

hook_android_dlopen_ext()

4.png
成功绕过!


v8.63.0

关于样本二的最新版(24年11月)我也分析了下,关键函数没变,具体检测的函数调用有点变化,不过一样可以bypass<(^-^)>

__int64 sub_1BEC4()
{
  unsigned __int64 StatusReg; // x19
  __int64 v1; // x0
  __int64 v3; // x0
  __int64 v4; // [xsp+8h] [xbp-48h]

  StatusReg = _ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2));
  v4 = *(_QWORD *)(StatusReg + 40);
  HIDWORD(qword_49010) = getpid();
  v1 = sub_1B924();
  if ( *(_QWORD *)(StatusReg + 40) == v4 )
    return 0LL;
  v3 = _strlcpy_chk(v1);
  return sub_1BFAC(v3);
}

5.png
成功!

【参考】:

[原创]绕过bilibili frida反调试-Android安全-看雪-安全社区|安全招聘|kanxue.com

某书Frida检测绕过记录_libmsaoaidsec.so-CSDN博客

后记

第一次在吾爱发文有些小忐忑,笨人此前备受完美主义折磨,羞愧于自己没有写出有价值的文章,认为不值得分享。
最近笨人的想法有些转变,决定大胆迈出第一步。欢迎大家留言交流!

免费评分

参与人数 7威望 +2 吾爱币 +108 热心值 +5 收起 理由
snowfox + 1 谢谢@Thanks!
ogli324 + 1 + 1 谢谢@Thanks!
正己 + 2 + 100 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
mingminglann + 1 我很赞同!
快乐的小跳蛙 + 2 + 1 热心回复!
myweb1996 + 3 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
芽衣 + 1 我很赞同!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

 楼主| BubbleC 发表于 2025-3-13 19:57
XGeorge 发表于 2025-3-12 22:33
你好,我有一点疑惑的是,为什么是init_xxx,它是什么特殊函数吗:"没有就说明frida检测在hook JNI_Onload前。 ...

你好,xxx是个指代,elf文件中 .init段会调用初始化函数.init_array,有些文件中会存在时机更早的.init_proc进行初始化操作。
那个脚本是用来确认检测时机是否在jni_onload中,如果不在就会在jni_onload之前也就是.init_proc或者array中。
 楼主| BubbleC 发表于 2025-2-25 19:26
快乐的小跳蛙 发表于 2025-2-25 13:38
请问下,我在使用frida hook单例模式类构造内调用的native方法时,使用你文中提到的在call_constructors中h ...

你是指call_constructors没被成功hook吗?需要注意的是这个代码中有两处偏移地址(①和②)需要根据你的实际环境做修改:


要获得①和②的地址需要你先找到自己手机上linker64的位置,然后
[Shell] 纯文本查看 复制代码
objdump -D [linker64] | grep dlopen

[Shell] 纯文本查看 复制代码
objdump -D [linker64] | grep call_constructors
快乐的小跳蛙 发表于 2025-2-25 13:38
请问下,我在使用frida hook单例模式类构造内调用的native方法时,使用你文中提到的在call_constructors中hook目标函数偏移,确认函数调用过,onEnter的日志没有输出是什么原因,如果需要hook代码和apk信息回复一下,谢谢
快乐的小跳蛙 发表于 2025-2-25 19:54
BubbleC 发表于 2025-2-25 19:26
你是指call_constructors没被成功hook吗?需要注意的是这个代码中有两处偏移地址(①和②)需要根据你的 ...

这些我都是对照我自己的手机更改过偏移的,我打包了一份用到apk,so,js文件。麻烦闲暇之余看下hook_gen函数的 console.log("enter hook_gen");能不能输出,在您的设备上。
 楼主| BubbleC 发表于 2025-2-25 20:56
快乐的小跳蛙 发表于 2025-2-25 19:54
这些我都是对照我自己的手机更改过偏移的,我打包了一份用到apk,so,js文件。麻烦闲暇之余看下hook_gen ...

好的我看一下
快乐的小跳蛙 发表于 2025-2-25 21:28
smzll 发表于 2025-2-26 12:13
楼主,最新版的样本二,在打印完replace sub_1BEC4之后,手机就一直卡死在加载页面了进不去,能麻烦看下嘛
smzll 发表于 2025-2-26 12:30
本帖最后由 smzll 于 2025-2-26 13:37 编辑
smzll 发表于 2025-2-26 12:13
楼主,最新版的样本二,在打印完replace sub_1BEC4之后,手机就一直卡死在加载页面了进不去,能麻烦看下嘛

我的是8.67版本,新版本也是一样的卡死进不去
 楼主| BubbleC 发表于 2025-2-26 19:33
smzll 发表于 2025-2-26 12:30
我的是8.67版本,新版本也是一样的卡死进不去

你好,样本二v8.67.0我试了下,是可以绕过的
 楼主| BubbleC 发表于 2025-2-26 19:35
快乐的小跳蛙 发表于 2025-2-25 21:28
https://wwxe.lanzoub.com/iIwJh2otxz5e 压缩包在这里

代码还没来得及看,不过使用本贴的代码是可以成功绕过的
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2025-3-25 03:40

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表