首页
社区
课程
招聘
[原创] iOS App的商业级环境检测手段 - 基于某讯A<E Framework 详解
发表于: 8小时前 203

[原创] iOS App的商业级环境检测手段 - 基于某讯A<E Framework 详解

8小时前
203

iOS App的商业级环境检测手段 - 基于某讯Å<E Framework 详解

0. 引言

移动App与游戏安全领域常常聚焦在Android客户端。对于同时拥有Android和iOS双端互通的游戏、各大常用App,通常可以解iOS安装包.ipa来降低分析成本,原因是iOS对JIT的严格限制,以及各大厂商对AppStore加密的信任,致使iOS应用中的二进制Framework(也可以理解为apk中的lib/arch/*.so)往往更加规范、易于分析。iOS App运行在沙箱内,权限也相当低。那么在这种情况下,都有哪些手段抵抗用户对环境的修改、越狱、甚至调试注入?

1. 解包

本次研究的App是一个使用Unity引擎开发的游戏,在AppStore上免费发布。要分析一个iOS应用程序,要进行以下步骤:

1.1 获取解密后的ipa

任何在AppStore上免费发布的App都可以通过Decrypt IPA Store来获取解包后的程序,如果App是付费的,则需要你自行购买并使用Root后的设备进行解包,这里不再赘述。

1.2 文件结构

ipa本质上就是一个zip文件,直接使用Bandizip解压缩即可。解压缩后可见ipa结构如图所示:

Payload下默认的目录名称应当是*.app,但是由于OSX会自动将*.app的目录识别为软件包,因此我这里将其改成了*-app便于查看文件。对于Unity游戏来说,App根目录下的ProductName只是一个entry,所有代码都在UnityFramework中(也就是Android Unity游戏包中常见的libil2cpp.so)。但我们今天分析的重点是另外两个库anortanogs,分别是某讯反作弊套件中的包装层与核心检测层。

2. 分析

2.1 调用拓扑

先查看UnityFramework的依赖

mako@makos-laptop UnityFramework.framework % llvm-otool -L UnityFramework

UnityFramework:
    ...
    @rpath/anort.framework/anort (compatibility version 1.0.0, current version 1.0.0)
    ...

发现其只依赖anort包装层。再看anort的依赖

mako@makos-laptop anort.framework % llvm-otool -L anort         
anort:
    ...
    @rpath/anogs.framework/anogs (compatibility version 1.0.0, current version 1.0.0)
    ...

所以简单得出anogs为检测核心:

UnityFramework -> anort -> anogs

2.2 anogs核心分析

2.2.1 字符串加密模式

直接用IDA Pro打开anogs二进制,左侧可以看到其导出表没有被混淆,先看_AnoSDKGetReportData这个可疑函数:

可见sub_3CC48是关键函数,跟进去看一下:

发现一些函数仅通过一个整数arg获取返回值,随便点进去几个看看:

char *__fastcall sub_105278(int a1)
{
  _BYTE *v2; // x19
  _BYTE *v3; // x0
  char v4; // w17
  __int64 v5; // x9
  unsigned __int8 v6; // w11
  unsigned __int8 *v7; // x12
  char *v8; // x8
  char *v9; // x8
  char v10; // w16
  char v11; // t1
  __int64 v12; // x14
  char i; // w1
  char v14; // w15
  int v15; // w16
  __int64 j; // x17
  int v17; // w1
  char v18; // w17
  char *v19; // x2
  char v20; // t1

  v2 = sub_56F58();
  v3 = sub_56F6C();
  v4 = 0;
  v5 = a1 + 1LL;
  v6 = v3[v5];
  v7 = &v2[a1];
  v8 = &v3[a1];
  v11 = *v8;
  v9 = v8 + 2;
  v10 = v11;
  v12 = a1 + 2LL;
  for ( i = 1; ; i = 0 )
  {
    v14 = i;
    if ( (v4 & 1) != 0 || !v10 )
    {
      v15 = *v7;
      v6 = v2[v5] ^ v15;
      if ( v6 )
      {
        for ( j = 0; j != v6; v15 = ((v15 + j++) ^ 6) + 1 )
          v9[j] = v7[j + 2] ^ v15;
      }
      v3[v12 + v6] = 0;
      v3[v5] = v6;
      v10 = 1;
      v3[a1] = 1;
    }
    if ( v6 )
    {
      v17 = 0;
      v18 = -1;
      v19 = v9;
      do
      {
        v20 = *v19++;
        v18 ^= v20;
        ++v17;
      }
      while ( (unsigned __int8)v17 < (unsigned int)v6 );
    }
    else
    {
      v18 = -1;
    }
    if ( (*v7 ^ (unsigned __int8)v2[(int)(v12 + v6)]) == (unsigned __int8)~v18 )
      break;
    v4 = 1;
    if ( (v14 & 1) == 0 )
    {
      byte_189268 = 1;
      return v9;
    }
  }
  return v9;
}

另一个:

char *__fastcall sub_106130(int a1)
{
                ...
        for ( j = 0; j != v6; v15 = ((v15 + j++) ^ 0x13) + 2 )
                ...

每个函数除了此处常数不一样,其他位置都一样。表存储位置:

void *sub_56F58()
{
  return &unk_119A68;
}

所以该解密模式大致可以总结成:

表地址: 0x119A68
每条字符串:
    第 0 字节: key
    第 1 字节: len ^ key
    后续 len 字节: cipher[i] ^ key
    每解一个字节后:
        key = (((key + i) ^ xor_const) + add_const) & 0xff
    尾部有一个以 0xff 开头的校验字节

所有字符串解码函数都有 sub_56F58() 的调用,并且该函数只有获取表的功能,所以可以xref sub_56F58() 定位到所有字符串解码函数,再根据函数的 xref 获取常量,解出全部字符串。

2.2.2 聚合检测函数sub_89fc

查看字符串列表:

发现函数sub_89FC包含大量越狱管理器*.app/bin/su等路径字符串,转到该函数分析:

2.2.2.1 检测常见越狱路径存在

一开始就能看到一大坨:

*(_QWORD *)task_info_out = sub_10B7B4(9295);
  *(_QWORD *)&task_info_out[2] = sub_104DF4(9502);
  *(_QWORD *)&v43 = sub_106FF4(9532);
  *((_QWORD *)&v43 + 1) = sub_10945C(9564);
  *(_QWORD *)&v44 = sub_10AFB8(9588);
  *((_QWORD *)&v44 + 1) = sub_10649C(9622);
  *(_QWORD *)&v45 = sub_108350(9649);
  *((_QWORD *)&v45 + 1) = sub_10A32C(9677);
  *(_QWORD *)&v46 = sub_1055D8(9709);
  *((_QWORD *)&v46 + 1) = sub_106C90(10829);
  *(_QWORD *)&v47 = sub_10810C(10847);
  *((_QWORD *)&v47 + 1) = sub_10945C(10864);
  *(_QWORD *)&v48 = sub_10B324(10891);
  *((_QWORD *)&v48 + 1) = sub_105398(11007);
  *(_QWORD *)&v49 = sub_107114(11033);
  *((_QWORD *)&v49 + 1) = sub_107DA0(11044);
  v50 = sub_108A28(11055);
  v51 = sub_10AC4C(11085);
  v52 = sub_105A64(11113);
  v53 = sub_107C7C(11143);
  v54 = sub_109FC0(11174);
  if ( (sub_94FC(*(__int64 *)task_info_out) & 1) != 0 )
  {
    v4 = 0;
LABEL_6:
    if ( !a1 )
      return &dword_0 + 1;
    v7 = *(const char **)&task_info_out[2 * v4];
    goto LABEL_8;
  }
  v5 = 0;
  while ( v5 != 20 )
  {
    v4 = v5 + 1;
    v6 = sub_94FC(*(_QWORD *)&task_info_out[2 * v5 + 2]);
    v5 = v4;
    if ( v6 )
      goto LABEL_6;
  }

sub_94FC内进行操作:

bool __fastcall sub_94FC(const char *a1)
{
  _BOOL8 v2; // x20
  stat v4; // [xsp+0h] [xbp-1C0h] BYREF
  char v5; // [xsp+90h] [xbp-130h] BYREF
  char v6[263]; // [xsp+91h] [xbp-12Fh] BYREF

  if ( !(unsigned int)sub_9FD04(a1, 0) || !(unsigned int)sub_9FCB0(a1, &v4) )
    return 1;
  sub_95EC(&v5, a1);
  if ( (unsigned int)sub_9FD04(v6, 0) )
    v2 = (unsigned int)sub_9FCB0(v6, &v4) == 0;
  else
    v2 = 1;
  if ( v5 )
    unlink(v6);
  return v2;
}

两个条件分别是:

__int64 __fastcall sub_9FD04(const char *a1, int a2)
{
  __int64 (__fastcall *v3)(__int64); // x8

  if ( dword_175530 )
    return access(a1, a2);
  v3 = (__int64 (__fastcall *)(__int64))off_178948;
  if ( !off_178948 )
  {
    v3 = sub_AC4E8;
    off_178948 = sub_AC4E8;
  }
  return v3(33);
}

__int64 __fastcall sub_9FCB0(const char *a1, stat *a2)
{
  __int64 (__fastcall *v3)(__int64); // x8

  if ( dword_175530 )
    return stat(a1, a2);
  v3 = (__int64 (__fastcall *)(__int64))off_178948;
  if ( !off_178948 )
  {
    v3 = sub_AC4E8;
    off_178948 = sub_AC4E8;
  }
  return v3(338);
}

可以看到使用的是accessstat二者,任意一者检测到即为风险。检测名单如下:

  • /Applications/Cydia.app
  • /Applications/Blackra1n.app
  • /Applications/FakeCarrier.app
  • /Applications/Icy.app
  • /Applications/IntelliScreen.app
  • /Applications/MxTube.app
  • /Applications/RockApp.app
  • /Applications/SBSetttings.app
  • /Applications/WinterBoard.app
  • /Applications/Sileo.app
  • /bin/su
  • /usr/sbin/sshd
  • /Library/MobileSubstrate
  • /usr/lib/tweaks
  • /electra
  • /chimera
  • /usr/lib/libsubstrate.dylib
  • /usr/lib/jelbrekLib.dylib
  • /usr/lib/libjailbreak.dylib
  • /usr/lib/libsubstitute.dylib
  • /private/etc/apt/sileo.sources
2.2.2.2 符号链接检测

再往下看:

v9 = (const char *)sub_107A34(9741);
  *(_QWORD *)task_info_outCnt = v9;
  *(_QWORD *)&task_info_outCnt[2] = sub_108C6C(9757);
  *(_QWORD *)&v57 = sub_10AFB8(9788);
  *((_QWORD *)&v57 + 1) = sub_106130(9819);
  *(_QWORD *)&v58 = sub_107DA0(9844);
  *((_QWORD *)&v58 + 1) = sub_109A0C(9869);
  *(_QWORD *)&v59 = sub_107A34(10341);
  v48 = 0u;
  v49 = 0u;
  v46 = 0u;
  v47 = 0u;
  v44 = 0u;
  v45 = 0u;
  *(_OWORD *)task_info_out = 0u;
  v43 = 0u;
  if ( sub_104590(v9, (char *)task_info_out, 0x7Fu) > 0 )
  {
LABEL_13:
    if ( !a1 )
      return &dword_0 + 1;
    v8 = a1;
    v7 = v9;
LABEL_15:
    sub_1C36C(v8, (unsigned __int64)v7, a2);
    return &dword_0 + 1;
  }
  v10 = 2;
  while ( v10 != 14 )
  {
    v9 = *(const char **)&task_info_outCnt[v10];
    v48 = 0u;
    v49 = 0u;
    v46 = 0u;
    v47 = 0u;
    v44 = 0u;
    v45 = 0u;
    *(_OWORD *)task_info_out = 0u;
    v43 = 0u;
    v10 += 2;
    if ( sub_104590(v9, (char *)task_info_out, 0x7Fu) >= 1 )
      goto LABEL_13;
  }

核心检测函数sub_0104590

ssize_t __fastcall sub_104590(const char *a1, char *a2, size_t a3)
{
  if ( (dword_189240 & 0x40000) != 0 )
    return sub_AC4E8(58);
  else
    return readlink(a1, a2, a3);
}

可以看到使用readlink函数检测固定符号链接。检测列表为:

  • /Applications
  • /var/stash/Library/Ringtones
  • /var/stash/Library/Wallpaper
  • /var/stash/usr/include
  • /var/stash/usr/libexec
  • /var/stash/usr/share
  • /User
2.2.2.3 fstab可写挂载检测

继续往下看:

v12 = sub_106378(9321);
  v13 = sub_107238(9334);
  v55[0] = 0;
  v55[1] = 0;
  if ( (sub_94FC((__int64)v12) & 1) != 0 || (v12 = (char *)v13, (unsigned int)sub_94FC(v13)) )
  {
    if ( v12 )
    {
      v14 = sub_9ECD4(v12, "r");
      if ( v14 )
      {
        v15 = v14;
        v58 = 0u;
        v59 = 0u;
        *(_OWORD *)task_info_outCnt = 0u;
        v57 = 0u;
        *(_QWORD *)&v46 = 0;
        v44 = 0u;
        v45 = 0u;
        *(_OWORD *)task_info_out = 0u;
        v43 = 0u;
        do
        {
          if ( !sub_9FB8C((char *)task_info_outCnt) )
          {
            sub_9F018(v15);
            goto LABEL_33;
          }
        }
        while ( sscanf(
                  (const char *)task_info_outCnt,
                  "%s %s %s %s %d %d",
                  task_info_out,
                  &v43,
                  &v44,
                  &v45,
                  &v46,
                  (char *)&v46 + 4) != 6
             || DWORD1(v46) != 1 );
        sub_1C36C(v55, (unsigned __int64)&v45, 16);
        sub_9F018(v15);
        v17 = sub_1C238(v55, "rw", 2);
        v7 = v12;
        if ( v17 )
          goto LABEL_33;
        if ( !a1 )
          return &dword_0 + 1;
        goto LABEL_8;
      }
    }
  }
LABEL_33:

可以看到其打开/etc/fstab/private/etc/fstab两个文件,然后逐行sscanf("%s %s %s %s %d %d"),当一行能解析出6列,并且第4列是 rw,最后一个整数是 1,就认为系统存在可写挂载配置。

2.2.2.4 检测隐藏的su
v27 = sub_106C90(10829);
  if ( (sub_94FC(v27) & 1) == 0 )
  {
    v28 = sub_1077EC(10839);
    v29 = sub_9FEA0(v28);
    if ( v29 )
    {
      v30 = v29;
      while ( 1 )
      {
        v31 = sub_9FF74(v30);
        if ( !v31 )
          break;
        if ( !(unsigned int)sub_1C0F0(v31->d_name, "su") )
        {
          sub_A0044(v30);
          v7 = "shadowed";
          goto LABEL_8;
        }
      }
      sub_A0044(v30);
    }
  }

这里直接枚举/bin/,只要目录项里出现了su就检测到,防止路径访问被直接hook。

2.2.2.5 SSH检测
if ( (sub_92B4(a1, a2) & 1) != 0 )
    return &dword_0 + 1;

此处跳进SSH检测:

__int64 __fastcall sub_92B4(_BYTE *a1, __int64 a2)
{
  __int64 v4; // x0
  __int64 v5; // x21
  __int64 v6; // x0
  __int64 result; // x0
  char *v8; // x21
  unsigned __int64 v9; // x0

  sub_AD0CC();
  v5 = v4;
  v6 = sub_108904(9254);
  result = sub_AD3F8(v5, v6, 1, 1);
  if ( (_DWORD)result )
  {
    v8 = sub_10B56C(11393);
    if ( (sub_9350(0x16u, v8) & 1) != 0 || (result = sub_9350(0x2Cu, v8), (_DWORD)result) )
    {
      if ( a1 )
      {
        v9 = sub_1066E4(10424);
        sub_1C36C(a1, v9, a2);
      }
      return 1;
    }
  }
  return result;
}

此处可以看到使用sub_10B56C字符串,查询一下:

定位到SSH-,因此确定其检测的是SSH Banner
依旧是两个条件,分别往这个函数传入0x16u0x2Cu,代表端口2244

_BYTE *__fastcall sub_9350(unsigned __int16 a1, _BYTE *a2)
{
  _BYTE *v2; // x19
  unsigned int v4; // w0
  int v5; // w20
  int v6; // w21
  __int64 v7; // x22
  __int128 v9; // [xsp+0h] [xbp-180h] BYREF
  fd_set v10; // [xsp+10h] [xbp-170h] BYREF
  _BYTE v11[104]; // [xsp+98h] [xbp-E8h] BYREF
  _OWORD v12[4]; // [xsp+100h] [xbp-80h] BYREF

  v2 = a2;
  if ( a2 )
  {
    if ( *a2 )
    {
      sub_9D94(v11, 0, 0);
      if ( (unsigned int)sub_9E68(v11, "127.0.0.1", a1, 0) )
        goto LABEL_10;
      v4 = sub_A0BC(v11);
      v5 = v4;
      if ( v4 - 1 > 0x3FE
        || (memset(&v10, 0, sizeof(v10)),
            v6 = 1 << v4,
            v7 = v4 >> 5,
            v10.fds_bits[(unsigned int)v7] |= 1 << v4,
            v9 = xmmword_118C30,
            select(v4 + 1, &v10, 0, 0, (timeval *)&v9) < 1)
        || (v10.fds_bits[v7] & v6) == 0
        || (memset(v12, 0, sizeof(v12)), (unsigned int)sub_104748(v5, v12) == -1)
        || (sub_10B56C(11393), (sub_DEB44(v12, v2) & 1) == 0) )
      {
LABEL_10:
        v2 = 0;
      }
      else
      {
        v2 = (_BYTE *)(&dword_0 + 1);
      }
      sub_9E5C(v11);
    }
    else
    {
      return 0;
    }
  }
  return v2;
}
2.2.2.6 Substrate / Substitute 模块检测
*(_QWORD *)task_info_out = sub_105398(11207);
  *(_QWORD *)&task_info_out[2] = sub_106ED4(11231);
  *(_QWORD *)&v43 = sub_108C6C(11257);
  *((_QWORD *)&v43 + 1) = sub_10AB28(11284);
  *(_QWORD *)&v44 = sub_1054B8(11308);
  *((_QWORD *)&v44 + 1) = sub_107238(11334);
  v18 = sub_D85D8();
  if ( v18 )
  {
    v19 = v18;
    v20 = 0;
    while ( 2 )
    {
      v21 = sub_D8620((__int64)v19);
      if ( v21 )
      {
        v22 = v21;
        for ( i = 0; i != 12; i += 2 )
        {
          if ( (unsigned int)sub_DEBB4(v22, *(_QWORD *)&task_info_out[i]) )
          {
            if ( a1 )
            {
              v26 = basename(v22);
              sub_1C118(a1, v26, a2);
            }
            sub_D875C(v19);
            return &dword_0 + 1;
          }
        }
        if ( ++v20 != 100000 )
          continue;
      }
      break;
    }
    sub_D875C(v19);
  }

其中sub_D85D8sub_D8620分支比较长,这里不再贴代码了,遍历100000个image路径,然后检测匹配:

  • MobileSubstrate.dylib
  • substitute-loader.dylib
  • SubstrateBootstrap.dylib
  • SubstrateLoader.dylib
  • SubstrateInserter.dylib
  • libsubstrate.dylib
2.2.2.7 检测环境变量DYLD_INSERT_LIBRARIES
v24 = sub_108A28(11355);
  v25 = getenv(v24);
  if ( v25 )
  {
    if ( a1 )
      sub_1CCE8(a1, a2, "%s,%s", v24, v25);
    return &dword_0 + 1;
  }

这里的sub_108A28(11355)就是环境变量名DYLD_INSERT_LIBRARIES

2.2.2.8 检测task_for_pid
sub_AD0CC();
  v33 = v32;
  v34 = sub_106928(12226);
  if ( (unsigned int)sub_AD3F8(v33, v34, 1, 1) )
  {
    v44 = 0u;
    v45 = 0u;
    *(_OWORD *)task_info_out = 0u;
    v43 = 0u;
    task_info_outCnt[0] = 16;
    if ( !task_info(mach_task_self_, 0x13u, task_info_out, task_info_outCnt) && (__int64)v43 >= 1 )
    {
      if ( !a1 )
        return &dword_0 + 1;
      v41 = (char *)sub_106928(12226);
      goto LABEL_66;
    }
  }

这里检测的是:

task_info(mach_task_self_, TASK_EXTMOD_INFO=0x13, out, outCnt=16)

该函数可以查当前进程扩展信息里是否留下过 task_for_pid 相关痕迹。

2.2.2.9 posix_spawn检测
sub_AD0CC();
  v36 = v35;
  v37 = sub_1077EC(12239);
  result = (_DWORD *)sub_AD3F8(v36, (__int64)v37, 1, 1);
  if ( (_DWORD)result )
  {
    v38 = (const char *)sub_108904(12254);
    result = dlopen(v38, 1);
    if ( result )
    {
      v39 = result;
      v40 = (const char *)sub_10AA04(12283);
      result = dlsym(v39, v40);
      if ( result )
      {
        if ( *result != 1476395088 )
          return 0;
        if ( !a1 )
          return &dword_0 + 1;
        v41 = sub_1077EC(12239);
LABEL_66:
        v7 = v41;
LABEL_8:
        v8 = a1;
        goto LABEL_15;
      }
    }
  }
  return result;
}

这里的流程等价于

dlopen("/usr/lib/libSystem.B.dylib", 1)
dlsym(handle, "posix_spawn")
读取 posix_spawn 首 4 字节
比较 0x58000050

检测首条指令是否等于 0x58000050,不太清楚该绝对地址代表什么特征,如有大佬清楚可以补充解答一下。

2.2.3 Frida检测函数 sub_12B30

从字符串列表里可以看见一个显眼的frida-server,定位到该函数。

__int64 sub_12B30()
{
  __int64 i; // x19
  __int64 v2; // x20
  unsigned __int8 *v4; // x20
  void *v5; // x0
  __int64 v6; // x19
  int v7; // w22
  __int64 v8; // x0
  unsigned __int64 v9; // x21
  char *v10; // x0
  _OWORD v11[8]; // [xsp+0h] [xbp-D0h] BYREF
  const char *v12[4]; // [xsp+88h] [xbp-48h]

  v12[0] = (const char *)sub_10921C(27862);
  v12[1] = (const char *)sub_1055D8(27909);
  v12[2] = (const char *)sub_107238(27934);
  v12[3] = (const char *)sub_1096A0(27966);
  if ( sub_36E74(v12[0]) )
    return 1;
  for ( i = 1; i != 4; ++i )
  {
    if ( sub_36E74(v12[i]) )
      return 1;
  }
  v4 = (unsigned __int8 *)sub_106254(28020);
  v5 = sub_A83B0();
  if ( !v5 )
    return 0;
  v6 = (__int64)v5;
  v7 = 1000;
  while ( 1 )
  {
    v8 = sub_A8400(v6);
    if ( !v8 )
    {
LABEL_12:
      v2 = 0;
      goto LABEL_15;
    }
    v9 = v8 + 4;
    if ( !(unsigned int)sub_1C238((_BYTE *)(v8 + 4), v4, 512) )
      break;
    if ( !--v7 )
      goto LABEL_12;
  }
  memset(v11, 0, sizeof(v11));
  sub_1C36C(v11, v9, 128);
  v10 = sub_106378(27621);
  sub_98EF0(706, v10);
  v2 = 1;
LABEL_15:
  sub_A8464(v6);
  return v2;
}

开局先解四个字符串,分别是:

  • /Library/LaunchDaemons/re.frida.server.plist
  • /usr/sbin/frida-server
  • /var/jb/usr/sbin/frida-server
  • /var/jb/Library/LaunchDaemons/re.frida.server.plist

路径检测的方式依旧是accessstat,这里不再展开。如果路径检测没中,就进入sub_A83B0函数读取进程表:

void *sub_A83B0()
{
  void *v0; // x0
  void *v1; // x19

  v0 = malloc(0x218u);
  v1 = v0;
  if ( v0 )
  {
    bzero(v0, 0x218u);
    if ( (unsigned int)sub_A8280((__int64)v1) )
    {
      free(v1);
      return 0;
    }
  }
  return v1;
}

__int64 __fastcall sub_A8280(__int64 a1)
{
  __int64 result; // x0
  size_t v3; // x0
  void *v4; // x0
  size_t v5; // [xsp+8h] [xbp-38h] BYREF
  int v6[6]; // [xsp+10h] [xbp-30h] BYREF

  *(_OWORD *)v6 = xmmword_129040;
  v5 = 0;
  if ( sysctl(v6, 4u, 0, &v5, 0, 0) )
    return 0xFFFFFFFFLL;
  v3 = v5;
  if ( __ROR8__(0x2C3F35BA781948B1LL * v5, 3) >= 0x6522C3F35BA782uLL )
  {
    v3 = v5 + v5 / 0xA;
    v5 = v3;
  }
  *(_QWORD *)a1 = v3 / 0x288;
  *(_DWORD *)(a1 + 16) = 0;
  v4 = malloc(v3);
  *(_QWORD *)(a1 + 8) = v4;
  if ( !v4 )
    return 0xFFFFFFFFLL;
  result = sysctl(v6, 4u, v4, &v5, 0, 0);
  if ( (_DWORD)result )
  {
    free(*(void **)(a1 + 8));
    *(_QWORD *)(a1 + 8) = 0;
    return 0xFFFFFFFFLL;
  }
  return result;
}

这里用的是sysctl(KERN_PROC_ALL)读取进程表。随手进入sub_A8400

__int64 __fastcall sub_A8400(__int64 a1)
{
  unsigned __int64 v1; // x8
  __int64 v2; // x9
  __int64 v3; // x8
  __int64 v4; // x19

  v1 = *(int *)(a1 + 16);
  if ( *(_QWORD *)a1 <= v1 )
    return 0;
  v2 = *(_QWORD *)(a1 + 8);
  *(_DWORD *)(a1 + 16) = v1 + 1;
  v3 = v2 + 648 * v1;
  *(_DWORD *)(a1 + 20) = *(_DWORD *)(v3 + 40);
  v4 = a1 + 20;
  sub_1C3E4(a1 + 24, v3 + 243, 512);
  return v4;
}

返回{pid, name}临时结构,进程名来自kinfo_proc,偏移值+243。最多1000条,只检测frida-server命中。

2.2.4 Flex修改器检测 sub_1F600

依旧公式,字符串里找到了gp3_mod_flex定位到函数:

__int64 __fastcall sub_1F600(__int64 a1, unsigned __int8 *a2)
{
  unsigned __int8 *v4; // x0
  __int64 v6; // x0

  if ( !a2 )
    return 0;
  if ( !*a2 )
    return 0;
  v4 = (unsigned __int8 *)sub_10A7BC(25081);
  if ( (unsigned int)sub_1C0F0(a2, v4) )
    return 0;
  v6 = sub_79088();
  sub_7CE64(v6, a1 + 16);
  sub_1F660();
  return 1;
}

只检测传入运行时对象名是不是gp3_mod_flex

2.2.5 反调试聚合 sub_3BF44

__int64 __fastcall sub_3BF44(__int64 a1)
{
  __int64 v3; // x0
  __int64 v4; // x20
  char *v5; // x0
  __int64 v6; // x2
  __int64 v7; // x3
  __int64 v8; // x4
  __int64 v9; // x5
  __int64 v10; // x6
  __int64 v11; // x7
  __int64 v12; // x0
  const char *v13; // x0
  int v14; // [xsp+8h] [xbp-18h] BYREF

  v14 = 32;
  if ( !*(_BYTE *)(a1 + 12) )
  {
    sub_AD0CC();
    v4 = v3;
    v5 = sub_10B56C(36693);
    if ( (sub_AD3F8(v4, (__int64)v5, 0, 1) & 1) != 0 || (v12 = sub_6E524(), sub_F8B98(v12)) )
    {
      if ( !sub_3A044(0, 0, v6, v7, v8, v9, v10, v11) )
      {
        if ( (unsigned int)sub_3C010() )
        {
          v13 = (const char *)sub_108D90(35158);
          sub_99048(v13);
        }
        else if ( !sub_3C0BC() )
        {
          *(_BYTE *)(a1 + 12) = 1;
        }
      }
    }
  }
  return sub_130E8(&v14);
}

这里聚合了一些反调试功能,通过xref ptrace检测函数找到该调度点。

2.2.5.1 ptrace检查 sub_3C010

常规反调试特征检查

__int64 sub_3C010()
{
  bool v0; // w8
  size_t v2; // [xsp+8h] [xbp-2B8h] BYREF
  int v3[2]; // [xsp+10h] [xbp-2B0h] BYREF
  int v4; // [xsp+18h] [xbp-2A8h]
  pid_t v5; // [xsp+1Ch] [xbp-2A4h]
  _BYTE v6[32]; // [xsp+20h] [xbp-2A0h] BYREF
  int v7; // [xsp+40h] [xbp-280h]

  bzero(v6, 0x288u);
  v2 = 648;
  *(_QWORD *)v3 = 0xE00000001LL;
  v4 = 1;
  v5 = getpid();
  v0 = sysctl(v3, 4u, v6, &v2, 0, 0) != -1;
  return *(_DWORD *)&v0 & ((unsigned __int16)(v7 & 0x800) >> 11);
}

输出大小 0x288,也就是 648 字节的 kinfo_proc。读取 extern_proc.p_flag 后判断p_flag & 0x8000x800 就是 P_TRACED

2.2.5.2 设置PT_DENY_ATTACHsub_3C0BC

如果上面ptrace没命中,则进入:

int sub_3C0BC()
{
  return mac_syscall(SYS_ptrace, 31, 0, 0, 0);
}

这里的反汇编是:

__text:000000000003C0BC                 MOV             X0, #0x1F
__text:000000000003C0C0                 MOV             X1, #0
__text:000000000003C0C4                 MOV             X2, #0
__text:000000000003C0C8                 MOV             X3, #0
__text:000000000003C0CC                 MOV             X16, #0x1A
__text:000000000003C0D0                 SVC             0x80
__text:000000000003C0D4                 BR              X30

x0=31PT_DENY_ATTACHx16=26 是 macOS/iOS syscall 表里的 ptrace syscall。调用成功后,sub_3BF44 会把对象字段 a1+0x0c 置 1,避免重复设置。

2.2.6 Receipt检查 sub_6BD44
void sub_6BD44()
{
  unsigned __int8 *v0; // x0
  _BYTE v1[1024]; // [xsp+8h] [xbp-418h] BYREF

  bzero(v1, 0x400u);
  if ( !(unsigned int)sub_3A158(v1, 1024) )
  {
    v0 = (unsigned __int8 *)sub_107EC4(6045);
    if ( sub_DEBB4((__int64)v1, v0) )
      sub_970FC((__int64)v1);
  }
}

先收集Receipt信息:

__int64 __fastcall sub_3A158(_BYTE *a1, __int64 a2)
{
  void *v4; // x19
  NSBundle *v5; // x20
  NSURL *v6; // x24
  NSString *v7; // x23
  NSString *v8; // x20
  const char *v9; // x1
  __int64 v10; // x21

  v4 = objc_autoreleasePoolPush();
  v5 = objc_retainAutoreleasedReturnValue(+[NSBundle mainBundle](&OBJC_CLASS___NSBundle, "mainBundle"));
  v6 = objc_retainAutoreleasedReturnValue(-[NSBundle appStoreReceiptURL](v5, "appStoreReceiptURL"));
  v7 = objc_retainAutoreleasedReturnValue(-[NSURL absoluteString](v6, "absoluteString"));
  objc_release(v6);
  objc_release(v5);
  if ( v7 && (v8 = objc_retainAutorelease(v7), (v9 = -[NSString UTF8String](v8, "UTF8String")) != 0) )
  {
    sub_1C36C(a1, (unsigned __int64)v9, a2);
    v10 = 0;
    v7 = v8;
  }
  else
  {
    v10 = 0xFFFFFFFFLL;
  }
  objc_release(v7);
  objc_autoreleasePoolPop(v4);
  return v10;
}

收集到的信息匹配sandboxReceipt,沙盒receipt一般说明App被测试签名、测试安装,说明不是正版,大概率是Sideload。

2.2.7 CodeResources 与自定义防篡改 sub_34E94

先获取bundle path,然后拼接字符串/_CodeSignature/CodeResources

if ( !(unsigned int)sub_822F0((__int64)&v67, 0x100u) )
  {
    v5 = sub_108230(14348);
    sub_1C2E8(&v67, v5, 256);

然后打开,解析plist,找到files2hash2

    sub_FADD0(v123);
    v4 = 65539;
    if ( (sub_FAE4C(v123, &v67) & 1) != 0 )
    {
      v6 = sub_FB228(v123);
      v7 = (_QWORD *)sub_ED200(v6);
      v8 = v7;
      if ( v7 )
      {
        v9 = sub_E7208(*v7, "plist");
        if ( v9 )
        {
          v10 = sub_E7208(v9, "dict");
          if ( v10 )
          {
            for ( i = sub_E7208(v10, "key"); i; i = sub_E7294(i, "key") )
            {
              v12 = (unsigned __int8 *)sub_E7A6C(i);
              if ( v12 && !(unsigned int)sub_1C238("files2", v12, 7) )
              {
                v19 = (unsigned __int8 *)sub_E7A6C(j);
                if ( v19 && !(unsigned int)sub_1C238("hash2", v19, 6) )
                          {

遍历bundle内资源:

                v13 = sub_E7294(i, "dict");
                if ( v13 )
                {
                  v14 = sub_E7208(v13, "key");
                  if ( v14 )
                  {
                    while ( v14 )
                    {
                      v15 = sub_E7A6C(v14);
                      v16 = v15;
                      if ( v15 && (sub_35D04(v15) & 1) == 0 )
                      {
                        v17 = sub_E7294(v14, "dict");
                        if ( !v17 )
                          goto LABEL_35;
                        for ( j = sub_E7208(v17, "key"); j; j = sub_E7294(j, "key") )
                        {

计算哈希,置入表内:

                            v20 = sub_E7294(j, "data");
                            j = v20;
                            if ( !v20 )
                              goto LABEL_35;
                            v21 = sub_E7A6C(v20);
                            if ( v21 && (unsigned int)sub_D7174(v21, &v124, 33) == 32 )
                            {
                              v126 = 0;
                              v83 = 0;
                              v84 = 0;
                              sub_D890(&v83, v16);
                              v90 = 0u;
                              if ( v83 )
                                v22 = v83;
                              else
                                v22 = "";
                              sub_D890(&v90, v22);
                              LOBYTE(v93) = v126;
                              v91 = v124;
                              v92 = v125;
                              sub_35EEC(v65, &v90);
                              sub_7704(&v90);
                              sub_7704(&v83);
                            }
                          }
                        }
                      }
                      v14 = sub_E7294(v14, "key");
                    }
                    v4 = 0;
                  }
                }
                break;
              }
            }
          }
        }
LABEL_35:
        sub_ED2B8(v8);
      }
      else
      {
        v4 = 65538;
      }
    }
    else
    {
      v4 = 65537;
    }
    sub_FAE14(v123);

统计缺失、冗余、hash 不匹配(v1 = hash 不匹配,v2 = 冗余,v3 = 缺失):

              v43 = (_QWORD *)v64[0];
              v1 = 0;
              v2 = 0;
              if ( v64[0] )
              {
                do
                {
                  v44 = (const char **)(v43 + 2);
                  v45 = sub_363D4(v65, v43 + 2);
                  if ( v45 )
                  {
                    *(_BYTE *)(v45 + 72) = 1;
                    if ( (unsigned int)sub_1C0B4(v45 + 40, v43 + 5, 32) )
                    {
                      v46 = *v44 ? (char *)*v44 : "";
                      if ( (sub_35AC8((__int64)v46) & 1) == 0 )
                      {
                        v1 = (unsigned int)(v1 + 1);
                        if ( *v44 )
                          v47 = *v44;
                        else
                          v47 = "";
                        sub_35C70(v1, v47, 256);
                      }
                    }
                  }
                  else
                  {
                    if ( *v44 )
                      v48 = *v44;
                    else
                      v48 = "";
                    if ( (sub_35AC8((__int64)v48) & 1) == 0 )
                    {
                      v2 = (unsigned int)(v2 + 1);
                      if ( *v44 )
                        v49 = *v44;
                      else
                        v49 = "";
                      sub_35C70(v2, v49, 512);
                    }
                  }
                  v43 = (_QWORD *)*v43;
                }
                while ( v43 );
              }
              v50 = v65[0];
              while ( 1 )
              {
                v4 = v50;
                if ( !v50 )
                  break;
                v50 = *(_QWORD *)(v50 + 8);
                if ( !v50 )
                {
                  v3 = 0;
LABEL_88:
                  if ( *(_BYTE *)(v4 + 72) != 1 )
                  {
                    if ( *(_QWORD *)(v4 + 24) )
                      v51 = *(const char **)(v4 + 24);
                    else
                      v51 = "";
                    v3 = (unsigned int)v3 + ((unsigned int)sub_35AC8((__int64)v51) ^ 1);
                  }
                  v52 = *(_QWORD *)(v4 + 16);
                  while ( v52 )
                  {
                    v53 = v52;
                    v52 = *(_QWORD *)(v52 + 8);
                    if ( !v52 )
                    {
                      v4 = v53;
                      goto LABEL_88;
                    }
                  }
                  v54 = (__int64 *)v4;
                  while ( 1 )
                  {
                    v4 = *v54;
                    if ( !*v54 )
                      goto LABEL_57;
                    v55 = *(_QWORD *)(v4 + 8) == (_QWORD)v54;
                    v54 = (__int64 *)*v54;
                    if ( v55 )
                      goto LABEL_88;
                  }
                }
              }

上述检测完成后,如果没有问题,则进入自定义清单检测,分别有两个文件:

  1. /__a**info.dat
v23 = sub_10A698(14380);
    sub_1C2E8(v123, v23, 512);
    sub_FADD0(v66);
    if ( (sub_FAE4C(v66, v123) & 1) != 0 )
    {
      v24 = (char *)sub_FB228(v66);
      v25 = sub_FB230(v66);
      __dst = 0u;
      sub_D7630(&v124);
      sub_D7650((int)&v124, v24, v25);
      sub_D8170(&v124, &__dst);
  1. /__a**cfinfo.dat
if ( !(unsigned int)sub_822F0((__int64)&v90, 0x200u) )
      {
        v26 = sub_10B9F8(14397);
        sub_1C2E8(&v90, v26, 512);
        sub_FADD0(&v83);
        if ( (sub_FAE4C(&v83, &v90) & 1) == 0 )
          goto LABEL_47;

获取记录后首先校验Magic v30 == -1417418754

      v122 = xmmword_119200;
      if ( v25 >= 0x10 )
      {
        sub_FB3C0(&v67, v24, v25, 0, 0);
        v30 = sub_FB61C(&v67);
        v31 = sub_FB61C(&v67);
        v32 = sub_FB61C(&v67);
        if ( v30 == -1417418754 && (v33 = v25 - 12, (((_DWORD)v25 - 12) & 0xF) == 0) )
        {

然后对资源进行解密:

            sub_D8948(&v90);
            if ( (unsigned int)sub_D8964((int)&v90, (int)&v122, off_177EF0, 16, 0x10u) )
            {
              v4 = 32769;
            }
            else
            {
              sub_FB3C0(&v124, 0, 0, 1, 0);
              if ( (sub_FB910(&v124, v57) & 1) != 0 )
              {
                v58 = v124;
                bzero((void *)v124, v125);
                if ( (unsigned int)sub_D9BB4(&v90, v56, v58, v57, 2) )
                {
                  v4 = 32771;
                }

这两个文件是运行时产生的,解密是标准AES-128-CFB:

key = 71 9A 62 0E 24 EA 10 EA E0 EA DA 6A DD 3F 41 ED
iv  = 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

解密函数比较大,使用AI辅助分析一下即可:

sub_D8948(&v90)
  初始化 Rijndael/AES ctx:
  ctx[0] = off_168F00
  ctx+8 = 0,表示未初始化

sub_D8964(&v90, &v122, off_177EF0, 16, 0x10)
  Rijndael/AES key schedule
  key_len = 16
  block_len = 16
  rounds = 10
  key = v122,也就是栈上的 16 字节 key
  iv  = *off_177EF0 指向的前 16 字节

sub_FB3C0 / sub_FB910
  只是在申请输出缓冲区,不参与密码算法

sub_D9BB4(&v90, v56, v58, v57, 2)
  mode = 2
  AES-CFB 解密

解密后再与实际资源进行匹配:

                  v59 = sub_FB61C(&v124);
                  if ( v59 )
                  {
                    while ( (_QWORD)v125 - *((_QWORD *)&v124 + 1) >= 0x2Du )
                    {
                      __dst = 0u;
                      v89 = 0u;
                      v60 = sub_FB660(&v124);
                      sub_FB8B4((int)&v124, &__dst);
                      sub_FB61C(&v124);
                      v61 = sub_FB6A8(&v124);
                      if ( (sub_35D04(v61) & 1) == 0 )
                      {
                        v83 = 0;
                        v84 = 0;
                        v86 = __dst;
                        v87 = v89;
                        sub_D890(&v83, v61);
                        v85 = v60;
                        sub_362E4(v64, &v83);
                        sub_7704(&v83);
                      }
                      v4 = 0;
                      if ( !--v59 )
                        goto LABEL_110;
                    }
                    v4 = 16388;
                  }

2.2.8 系统状态检测

2.2.8.1 VPN检测
__int64 __fastcall sub_388CC(_BYTE *a1, int a2)
{
  __int64 v4; // x0
  __int64 v5; // x19
  __int64 v6; // x0
  __int64 v7; // x20
  __int64 v8; // x0
  void *v9; // x27
  UIDevice *v10; // x21
  NSString *v11; // x23
  double v12; // d0
  __int64 v13; // x20
  CFDictionaryRef v15; // x26
  NSString *v16; // x21
  id v17; // x24
  id v18; // x19
  id v19; // x0
  __int64 v20; // x23
  __int64 v21; // x8
  void *v22; // x20
  NSString *v23; // x27
  NSString *v24; // x21
  NSString *v25; // x26
  NSString *v26; // x19
  id v27; // x20
  __int64 v28; // x20
  const char *v29; // x0
  __int64 v30; // x0
  _BYTE *v31; // [xsp+10h] [xbp-160h]
  void *v32; // [xsp+18h] [xbp-158h]
  const __CFDictionary *v33; // [xsp+20h] [xbp-150h]
  NSString *v34; // [xsp+28h] [xbp-148h]
  int v35; // [xsp+30h] [xbp-140h]
  __int64 v36; // [xsp+38h] [xbp-138h]
  id obj; // [xsp+40h] [xbp-130h]
  __int64 v38; // [xsp+48h] [xbp-128h]
  __int128 v39; // [xsp+50h] [xbp-120h] BYREF
  __int128 v40; // [xsp+60h] [xbp-110h]
  __int128 v41; // [xsp+70h] [xbp-100h]
  __int128 v42; // [xsp+80h] [xbp-F0h]
  _BYTE v43[128]; // [xsp+90h] [xbp-E0h] BYREF

  if ( *(_BYTE *)(sub_6E524() + 1245) )
    return 0;
  v4 = sub_6E524();
  v5 = sub_F8898(v4);
  sub_AD0CC();
  v7 = v6;
  v8 = sub_10735C(17235);
  if ( !(unsigned int)sub_AD3F8(v7, v8, v5, 1) )
    return 0;
  v9 = objc_autoreleasePoolPush();
  v10 = objc_retainAutoreleasedReturnValue(+[UIDevice currentDevice](&OBJC_CLASS___UIDevice, "currentDevice"));
  v11 = objc_retainAutoreleasedReturnValue(-[UIDevice systemVersion](v10, "systemVersion"));
  objc_release(v10);
  -[NSString doubleValue](v11, "doubleValue");
  if ( v12 >= 9.0 )
  {
    v15 = CFNetworkCopySystemProxySettings();
    v35 = a2;
    v16 = objc_retainAutoreleasedReturnValue(+[NSString stringWithUTF8String:](&OBJC_CLASS___NSString, "stringWithUTF8String:", sub_10AD70(11986)));
    v17 = objc_retainAutoreleasedReturnValue(-[__CFDictionary objectForKeyedSubscript:](v15, "objectForKeyedSubscript:", v16));
    v18 = objc_retainAutoreleasedReturnValue(objc_msgSend(v17, "allKeys"));
    objc_release(v17);
    objc_release(v16);
    v41 = 0u;
    v42 = 0u;
    v39 = 0u;
    v40 = 0u;
    obj = objc_retain(v18);
    v19 = objc_msgSend(obj, "countByEnumeratingWithState:objects:count:", &v39, v43, 16);
    if ( v19 )
    {
      v38 = *(_QWORD *)v40;
      v33 = v15;
      v34 = v11;
      v31 = a1;
      v32 = v9;
      while ( 2 )
      {
        v20 = 0;
        if ( (unsigned __int64)v19 <= 1 )
          v21 = 1;
        else
          v21 = (__int64)v19;
        v36 = v21;
        do
        {
          if ( *(_QWORD *)v40 != v38 )
            objc_enumerationMutation(obj);
          v22 = *(void **)(*((_QWORD *)&v39 + 1) + 8 * v20);
          v23 = objc_retainAutoreleasedReturnValue(+[NSString stringWithUTF8String:](&OBJC_CLASS___NSString, "stringWithUTF8String:", sub_10BC40(11999)));
          if ( objc_msgSend(v22, "rangeOfString:", v23) != (id)0x7FFFFFFFFFFFFFFFLL )
            goto LABEL_25;
          v24 = objc_retainAutoreleasedReturnValue(+[NSString stringWithUTF8String:](&OBJC_CLASS___NSString, "stringWithUTF8String:", sub_105154(12005)));
          if ( objc_msgSend(v22, "rangeOfString:", v24) != (id)0x7FFFFFFFFFFFFFFFLL )
            goto LABEL_24;
          v25 = objc_retainAutoreleasedReturnValue(+[NSString stringWithUTF8String:](&OBJC_CLASS___NSString, "stringWithUTF8String:", sub_105820(12011)));
          if ( objc_msgSend(v22, "rangeOfString:", v25) != (id)0x7FFFFFFFFFFFFFFFLL )
          {
            objc_release(v25);
LABEL_24:
            objc_release(v24);
LABEL_25:
            objc_release(v23);
LABEL_26:
            v28 = 1;
            v15 = v33;
            v11 = v34;
            a1 = v31;
            v9 = v32;
            goto LABEL_27;
          }
          v26 = objc_retainAutoreleasedReturnValue(+[NSString stringWithUTF8String:](&OBJC_CLASS___NSString, "stringWithUTF8String:", sub_106130(12019)));
          v27 = objc_msgSend(v22, "rangeOfString:", v26);
          objc_release(v26);
          objc_release(v25);
          objc_release(v24);
          objc_release(v23);
          if ( v27 != (id)0x7FFFFFFFFFFFFFFFLL )
            goto LABEL_26;
          ++v20;
        }
        while ( v36 != v20 );
        v19 = objc_msgSend(obj, "countByEnumeratingWithState:objects:count:", &v39, v43, 16);
        v28 = 0;
        v15 = v33;
        v11 = v34;
        a1 = v31;
        v9 = v32;
        if ( v19 )
          continue;
        break;
      }
    }
    else
    {
      v28 = 0;
    }
LABEL_27:
    objc_release(obj);
    v29 = (const char *)sub_10A450(39978);
    sub_1CCE8(a1, v35, v29, v28);
    v30 = sub_6E524();
    sub_F98D8(v30, 1, v28);
    objc_release(obj);
    objc_release(v15);
    v13 = 1;
  }
  else
  {
    v13 = 0;
  }
  objc_release(v11);
  objc_autoreleasePoolPop(v9);
  return v13;
}

先判断: UIDevice.systemVersion.doubleValue >= 9.0,如果满足,则调用 CFNetworkCopySystemProxySettings,读取字典返回的__SCOPED__字段,遍历所有interface key,检测是否包含:

  • tap
  • tun
  • ipsec
  • ppp

若包含,说明开启了VPN。

2.2.8.2 屏幕录制检测

__int64 __fastcall sub_386C4(_BYTE *a1, int a2)
{
  __int64 result; // x0
  void *v5; // x20
  UIScreen *v6; // x0
  UIScreen *v7; // x19
  void *v8; // x21
  NSNotificationCenter *v9; // x22
  id v10; // x22
  NSOperationQueue *v11; // x23
  id location; // [xsp+8h] [xbp-38h] BYREF

  if ( *(_BYTE *)(sub_6E524() + 1244) )
    return 0;
  result = sub_1162E0(11, 0, 0);
  if ( (_DWORD)result )
  {
    v5 = objc_autoreleasePoolPush();
    v6 = objc_retainAutoreleasedReturnValue(+[UIScreen mainScreen](&OBJC_CLASS___UIScreen, "mainScreen"));
    v7 = v6;
    if ( !v6 )
    {
      objc_release(0);
      objc_autoreleasePoolPop(v5);
      return 0;
    }
    sub_1CCE8(a1, a2, "iScreenCaptured:%d", -[UIScreen isCaptured](v6, "isCaptured"));
    if ( (unsigned int)sub_1162E0(11, 0, 0) )
    {
      if ( (byte_1797F8 & 1) == 0 )
      {
        byte_1797F8 = 1;
        v8 = objc_autoreleasePoolPush();
        v9 = objc_retainAutoreleasedReturnValue(+[NSNotificationCenter defaultCenter](&OBJC_CLASS___NSNotificationCenter, "defaultCenter"));
        objc_initWeak(&location, v9);
        objc_release(v9);
        v10 = objc_loadWeakRetained(&location);
        v11 = objc_retainAutoreleasedReturnValue(+[NSOperationQueue mainQueue](&OBJC_CLASS___NSOperationQueue, "mainQueue"));
        objc_release(
          objc_retainAutoreleasedReturnValue(
            objc_msgSend(
              v10,
              "addObserverForName:object:queue:usingBlock:",
              UIScreenCapturedDidChangeNotification,
              0,
              v11,
              &stru_165360)));
        objc_release(v11);
        objc_release(v10);
        objc_destroyWeak(&location);
        objc_autoreleasePoolPop(v8);
      }
    }
    objc_release(v7);
    objc_autoreleasePoolPop(v5);
    return 1;
  }
  return result;
}

调用-[UIScreen isCaptured]获取屏幕当前被录制状态。

3. 附件

  1. Framework样本
  2. 字符串抽取工具

结语

个人能力有限,本文未涉及绕过检测的具体手段与Patch方案。后续可能会(如果不咕咕咕)制作CrackMe与Patch教程,同时欢迎大佬指正文章中不严谨的内容。


[招生]科锐逆向工程师培训(2026年7月3日实地,远程教学同时开班, 第56期)!

上传的附件:
收藏
免费 0
打赏
分享
最新回复 (1)
雪    币: 311
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
2
欢迎加入我的团队
5小时前
0
游客
登录 | 注册 方可回帖
返回