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的依赖
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); }
|
可以看到使用的是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 符号链接检测
再往下看:
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
依旧是两个条件,分别往这个函数传入0x16u与0x2Cu,代表端口22和44:
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_D85D8和sub_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
路径检测的方式依旧是access与stat,这里不再展开。如果路径检测没中,就进入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 & 0x800,0x800 就是 P_TRACED。
2.2.5.2 设置PT_DENY_ATTACH:sub_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=31 是 PT_DENY_ATTACH,x16=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,找到files2与hash2:
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; } } }
|
上述检测完成后,如果没有问题,则进入自定义清单检测,分别有两个文件:
/__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);
|
/__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,检测是否包含:
若包含,说明开启了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. 附件
- Framework样本 点击下载
- 字符串抽取工具 点击下载
结语
个人能力有限,本文未涉及绕过检测的具体手段与Patch方案。后续可能会(如果不咕咕咕)制作CrackMe与Patch教程,同时欢迎大佬指正文章中不严谨的内容。