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的依赖

1
2
3
4
5
6
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的依赖

1
2
3
4
5
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为检测核心:

1
UnityFramework -> anort -> anogs

2.2 anogs核心分析

2.2.1 字符串加密模式

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
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;
}

另一个:

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

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

1
2
3
4
void *sub_56F58()
{
return &unk_119A68;
}

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

1
2
3
4
5
6
7
8
表地址: 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 检测常见越狱路径存在

一开始就能看到一大坨:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
*(_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内进行操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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;
}

两个条件分别是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
__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 符号链接检测

再往下看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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

1
2
3
4
5
6
7
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可写挂载检测

继续往下看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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检测
1
2
if ( (sub_92B4(a1, a2) & 1) != 0 )
return &dword_0 + 1;

此处跳进SSH检测:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
__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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
_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 模块检测
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
*(_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
1
2
3
4
5
6
7
8
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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;
}
}

这里检测的是:

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

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

2.2.2.9 posix_spawn检测
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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;
}

这里的流程等价于

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

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

2.2.3 Frida检测函数 sub_12B30

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
__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函数读取进程表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
__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定位到函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
__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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
__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

常规反调试特征检查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
__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没命中,则进入:

1
2
3
4
int sub_3C0BC()
{
return mac_syscall(SYS_ptrace, 31, 0, 0, 0);
}

这里的反汇编是:

1
2
3
4
5
6
7
__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
1
2
3
4
5
6
7
8
9
10
11
12
13
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信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
__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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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内资源:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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") )
{

计算哈希,置入表内:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
                            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 = 缺失):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
              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
1
2
3
4
5
6
7
8
9
10
11
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
1
2
3
4
5
6
7
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

1
2
3
4
5
6
7
8
9
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) )
{

然后对资源进行解密:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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:

1
2
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辅助分析一下即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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 解密

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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检测
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
__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 屏幕录制检测

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
__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教程,同时欢迎大佬指正文章中不严谨的内容。