-
-
[原创] iOS App的商业级环境检测手段 - 基于某讯A<E Framework 详解
-
发表于: 9小时前 234
-
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)。但我们今天分析的重点是另外两个库anort与anogs,分别是某讯反作弊套件中的包装层与核心检测层。
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);
}
可以看到使用的是access与stat二者,任意一者检测到即为风险。检测名单如下:
/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
依旧是两个条件,分别往这个函数传入0x16u与0x2Cu,代表端口22和44:
_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_D85D8和sub_D8620分支比较长,这里不再贴代码了,遍历100000个image路径,然后检测匹配:
MobileSubstrate.dylibsubstitute-loader.dylibSubstrateBootstrap.dylibSubstrateLoader.dylibSubstrateInserter.dyliblibsubstrate.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
路径检测的方式依旧是access与stat,这里不再展开。如果路径检测没中,就进入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 & 0x800,0x800 就是 P_TRACED。
2.2.5.2 设置PT_DENY_ATTACH:sub_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=31 是 PT_DENY_ATTACH,x16=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,找到files2与hash2:
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;
}
}
}
上述检测完成后,如果没有问题,则进入自定义清单检测,分别有两个文件:
/__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);
/__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. 附件
- Framework样本
- 字符串抽取工具
结语
个人能力有限,本文未涉及绕过检测的具体手段与Patch方案。后续可能会(如果不咕咕咕)制作CrackMe与Patch教程,同时欢迎大佬指正文章中不严谨的内容。