iOS逆向实战 -- 某激活程序网络请求分析

前言

本文内容仅供技术研究与学习交流之用,严禁用于任何商业用途。

文中所涉及的技术手段、工具及分析过程,均基于公开或开源信息探讨,旨在分享技术原理。

作者坚决反对且不鼓励任何利用此类技术从事违法违规操作的行为。

若读者滥用本文技术造成任何不良后果,均由使用者自行承担,与本文作者无关。

本人技术水平有限,本文更多是个人摸索过程中的总结与沉淀。如有疏漏或不当之处,欢迎各位大佬探讨指正。

激活流程分析

初始化阶段

先砸壳,随后 ida 打开程序,跟到 start 函数,start 函数有反调试检测,但是我们的主要目的是分析其网络请求加密及参数生成方式以及含义,所以暂时不住要介绍如何对抗这些检测。

start 函数的末尾进入 iOS 的应用生命周期

img

可以看出调用的函数kSAkHVfYbEfPcnXAGCCEjMTXDaxNzHvHdGjKMhxl,跟进分析一下

image-20260413165254630

得知其就是对于全局变量进行赋值,便于后续操作,赋值完成之后进入UIApplicationMain,把程序交给 UIKit,进入 App 正常生命周期

数据包发送阶段

通过分析VCMain的类的其他函数,最终跟到激活框的UI展示函数-[hdVWMVzzjaKwmOFRnWPjPZjgurropaFTOsSacoaX mGszNNdLYBCczbtbJYYnbFIGZAYBeDLZYdcaOrLr:]

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
v13 = objc_retainAutoreleasedReturnValue(
+[UIAlertController alertControllerWithTitle:message:preferredStyle:](// 创建
&OBJC_CLASS___UIAlertController,
"alertControllerWithTitle:message:preferredStyle:",
v5,
v7,
1));
v36[0] = _NSConcreteStackBlock;
v36[1] = 3254779904LL;
v36[2] = sub_100009568; // 后续调用输入变化处理函数
v36[3] = &unk_10022CD90;
v36[4] = self;
-[UIAlertController addTextFieldWithConfigurationHandler:](v13, "addTextFieldWithConfigurationHandler:", v36);// 添加输入框
v34[0] = _NSConcreteStackBlock;
v34[1] = 3254779904LL;
v34[2] = sub_1000095F8;
v34[3] = &unk_10022CDC0;
v34[4] = self;
v14 = objc_retain(v13);
v35 = v14;
v26 = v9;
v15 = v9;
v16 = v25;
v17 = objc_retainAutoreleasedReturnValue(
+[UIAlertAction actionWithTitle:style:handler:](
&OBJC_CLASS___UIAlertAction,
"actionWithTitle:style:handler:",
v15,
1,
v34));
v32[0] = _NSConcreteStackBlock;
v32[1] = 3254779904LL;
v32[2] = sub_100009694; // 直接调用点击激活按钮的函数
v32[3] = &unk_10022CDC0;
v32[4] = self;

先分析函数sub_100009568,他首先调用方法mlZnRMHhNGWhoNmjlNODKyEmNRuWRjfvlqwpPEHB

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
void __fastcall sub_100009568(__int64 a1, id a2)
{
id v3; // x21
NSNotificationCenter *v4; // x20

v3 = objc_retain(a2);
v4 = objc_retainAutoreleasedReturnValue(+[NSNotificationCenter defaultCenter](&OBJC_CLASS___NSNotificationCenter, "defaultCenter"));
-[NSNotificationCenter addObserver:selector:name:object:](
v4,
"addObserver:selector:name:object:",
*(a1 + 32),
"mlZnRMHhNGWhoNmjlNODKyEmNRuWRjfvlqwpPEHB:",// 调用输入变化处理的函数:去掉空格 长度为40
UITextFieldTextDidChangeNotification,
v3);
objc_release(v4);
objc_msgSend(v3, "setSecureTextEntry:", 0);
objc_msgSend(v3, "setKeyboardType:", 1);
objc_release(v3);
}

// 输入激活码
void __cdecl -[hdVWMVzzjaKwmOFRnWPjPZjgurropaFTOsSacoaX mlZnRMHhNGWhoNmjlNODKyEmNRuWRjfvlqwpPEHB:](
hdVWMVzzjaKwmOFRnWPjPZjgurropaFTOsSacoaX *self,
SEL a2,
id a3)
{
void *v4; // x19
NSString *v5; // x0
NSString *rechargeCardCode; // x8
NSString *v7; // x0
NSString *v8; // x0
NSString *v9; // x8
NSString *v10; // x0
void *v11; // x21

v4 = objc_retainAutoreleasedReturnValue(objc_msgSend(a3, "object"));
v5 = objc_retainAutoreleasedReturnValue(objc_msgSend(v4, "text"));
rechargeCardCode = self->rechargeCardCode;
self->rechargeCardCode = v5; // 从文本框获取输入
objc_release(rechargeCardCode);
v7 = self->rechargeCardCode;
if ( v7
&& (v8 = objc_retainAutoreleasedReturnValue(
-[NSString stringByReplacingOccurrencesOfString:withString:](
v7,
"stringByReplacingOccurrencesOfString:withString:",
CFSTR(" "),
&stru_10025B6E8)), // 去掉空格
v9 = self->rechargeCardCode,
self->rechargeCardCode = v8,
objc_release(v9),
(v10 = self->rechargeCardCode) != 0) )
{
-[UIAlertAction setEnabled:](self->chargeAlertAction, "setEnabled:", -[NSString length](v10, "length") == 40);// 校验长度是否为40
}
else
{
v11 = objc_retainAutoreleasedReturnValue(objc_msgSend(v4, "text"));
-[UIAlertAction setEnabled:](self->chargeAlertAction, "setEnabled:", objc_msgSend(v11, "length") == 40);
objc_release(v11);
}
objc_release(v4);
}

继续跟进可以得知其逻辑是文本框获取输入-->去掉空格-->校验长度,激活码长度为40

img

跟踪函数sub_100009694跟到BrgDgGVvDpYFXxSlDIQVyIEuftIBMafdLGDMUiKE发现其是一个点击激活按钮的函数

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
bool __cdecl -[hdVWMVzzjaKwmOFRnWPjPZjgurropaFTOsSacoaX BrgDgGVvDpYFXxSlDIQVyIEuftIBMafdLGDMUiKE](
hdVWMVzzjaKwmOFRnWPjPZjgurropaFTOsSacoaX *self,// 建一个结果变量 dispatch_async 到全局队列 执行 sub_100008FD4
// 然后再 dispatch_async 回主线程执行 sub_100009060
SEL a2)
{
NSObject *v3; // x20
_QWORD v5[6]; // [xsp+0h] [xbp-70h] BYREF
__int64 v6; // [xsp+30h] [xbp-40h] BYREF
__int64 *v7; // [xsp+38h] [xbp-38h]
signed __int64 v8; // [xsp+40h] [xbp-30h]
char v9; // [xsp+48h] [xbp-28h]

v6 = 0;
v7 = &v6;
v8 = ' \0\0\0';
v9 = 0;
v3 = objc_retainAutoreleasedReturnValue(dispatch_get_global_queue(-2, 0));
v5[0] = _NSConcreteStackBlock;
v5[1] = 3254779904LL;
v5[2] = sub_100008FD4;
v5[3] = &unk_10022CD60;
v5[4] = self;
v5[5] = &v6;
dispatch_async(v3, v5);
objc_release(v3);
LOBYTE(self) = *(v7 + 24);
_Block_object_dispose(&v6, 8);
return self;
}

// 被一个 OC 方法作为 block 使用
void __fastcall sub_100008FD4(__int64 a1)
{
id v2; // x0
_QWORD block[5]; // [xsp+8h] [xbp-38h] BYREF

*(*(*(a1 + 40) + 8LL) + 24LL) = sub_100007920(*(*(a1 + 32) + 16LL));// 从某个对象里取出一个字符串 把这个字符串直接传给 sub_100007920 返回值保存到 block capture 里
// sub_100007920(input_key)
v2 = objc_retainAutorelease(&_dispatch_main_q);
block[0] = _NSConcreteStackBlock;
block[1] = 3254779904LL;
block[2] = sub_100009060; // 随后调用sub_100009060 如果激活成功 弹出成功提示
block[3] = &unk_10022CD30;
block[4] = *(a1 + 40);
dispatch_async(&_dispatch_main_q, block);
}

// 参数c的生成 激活请求构造
bool __fastcall sub_100007920(void *a1)
{
strcpy(v19, "active"); // 写接口名:active
v1 = objc_retain(a1);
v3 = sub_10002C254(v1, v2); // 写入随机参数
v18 = 'c'; // 写入参数c
v4 = objc_retainAutorelease(v1);
v5 = objc_msgSend(v4, "UTF8String");
objc_release(v4);
v6 = sub_10000E23C(v5);
sub_10000E098(v3, &v18, v6); // 写入请求对象v3
v7 = sub_10002C4D0(v19, v3); // 给主请求函数C4D0统一发送
sub_10000D600(v3);
if ( !v7 )
return 0;
v17 = 's'; // s == 0 判断成功
v8 = *(sub_10000DFB0(v7, &v17) + 40);
v9 = v8 == 0;
if ( v8 )
{
v16 = 'm'; // m 作为失败提示字符串展示
v10 = objc_retainAutoreleasedReturnValue(
+[NSString stringWithUTF8String:](
&OBJC_CLASS___NSString,
"stringWithUTF8String:",
*(sub_10000DFB0(v7, &v16) + 32)));
v11 = objc_retainAutorelease(&_dispatch_main_q);
block[0] = _NSConcreteStackBlock;
block[1] = 3254779904LL;
block[2] = sub_100007A9C;
block[3] = &unk_10022CCA0;
v15 = v10;
v12 = objc_retain(v10);
dispatch_async(&_dispatch_main_q, block);
objc_release(v15);
objc_release(v12);
}
sub_10000D600(v7);
return v9;
}

继续分析,最终跟到主请求函数sub_10002C4D0,也就是我们要分析的主要函数

激活阶段参数生成方式及意义分析

初步分析

先通过 frida 去 hook 下其请求 json 跟响应 json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
请求 JSON:
{
"jqPSOv2JRS": "fhSKRRGqUe",
"c": "1111111111222222222233333333334444444444",
"d": "K2BoLWhdQVxqLjstbyU+KHEpQyt1I3FVSCFHUEMmdCJII0QgRR5NIU4=",
"p": "...",
"v": "...",
"at": "00000000000000000000000000000000",
"ov": "16.0.3",
"A83cKhSaNZ": "opYBZy44UaoSlMR29"
}
响应 JSON:
{
"phpvxy": "79jrabwh",
"s": 1,
"m": "激活参数有误",
"npsde": "r8ro5biah"
}

主请求函数以及分析如下

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
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
__int64 __fastcall sub_10002C4D0(__int64 a1, __int64 a2)
{
v161 = '//:ptth';
strcpy(v180, "https://");
strcpy(v179, "/******/*********/*****/");
v4 = objc_retainAutoreleasedReturnValue(sub_10000B4F8());// 返回一个全局字符串qword_100272D38 取d的原值
v5 = objc_msgSend(v4, "lengthOfBytesUsingEncoding:", 4);
v6 = objc_retainAutorelease(v4);
v7 = objc_msgSend(v6, "UTF8String");
v8 = &v148 - ((v5 + 17) & 0xFFFFFFFFFFFFFFF0LL);
v8[v5 + 1] = 0;
sub_1000236BC(v7, v8); // 把 getNormalizedIFID() 返回的设备标识写入临时缓冲区并做加密:首字节写控制字节,后续每个字符按 round(sqrt(i*step)) 交替加减偏移。后续会作为参数 d 进行base64编码,参与响应解密时的密钥/IV 派生。
v9 = strlen(v8);
v149 = v8;
v151 = objc_retainAutoreleasedReturnValue(+[NSData dataWithBytes:length:](&OBJC_CLASS___NSData, "dataWithBytes:length:", v8, v9));
v10 = objc_retainAutoreleasedReturnValue(sub_100020960(v151));// 把加密后的设备标识 NSData 做标准 Base64 编码,随后写入请求对象字段 "d"。因此 d = Base64(加密后的设备标识)
v160 = 'd';
v11 = objc_retainAutorelease(v10);
v12 = sub_10000E23C(objc_msgSend(v11, "UTF8String"));
sub_10000E098(a2, &v160, v12); // 写入请求对象d
v13 = objc_retainAutoreleasedReturnValue(sub_10000B4DC());// 返回一个全局字符串qword_100272D30 取p的原值
objc_release(v13);
if ( v13 )
{
strcpy(v177, "p"); // 如果全局应用名存在,则写入请求字段 "p"
v14 = objc_retainAutorelease(objc_retainAutoreleasedReturnValue(sub_10000B4DC()));
v15 = sub_10000E23C(objc_msgSend(v14, "UTF8String"));
sub_10000E098(a2, v177, v15);
objc_release(v14);
}
v16 = objc_retainAutoreleasedReturnValue(sub_10000BA38());// 返回一个全局字符串qword_100272DB0 取v的原值
objc_release(v16);
if ( v16 )
{
strcpy(v177, "v"); // 如果全局版本号存在,则写入请求字段 "v"。
v17 = objc_retainAutorelease(objc_retainAutoreleasedReturnValue(sub_10000BA38()));
v18 = sub_10000E23C(objc_msgSend(v17, "UTF8String"));
sub_10000E098(a2, v177, v18);
objc_release(v17);
}
v150 = v11;
v152 = &v148;
v153 = v6;
v19 = objc_retainAutoreleasedReturnValue(sub_10000B514());// 返回一个全局字符串qword_100272D40 取at的原值
objc_release(v19);
if ( v19 )
{
strcpy(v177, "at"); // 如果附加全局字段存在,则写入请求字段 "at";
v20 = objc_retainAutorelease(objc_retainAutoreleasedReturnValue(sub_10000B514()));
v21 = sub_10000E23C(objc_msgSend(v20, "UTF8String"));
sub_10000E098(a2, v177, v21);
objc_release(v20);
}
strcpy(v159, "ov"); // 写入参数ov
v22 = objc_retainAutoreleasedReturnValue(+[UIDevice currentDevice](&OBJC_CLASS___UIDevice, "currentDevice"));// 获取 UIDevice currentDevice
v23 = objc_retainAutorelease(objc_retainAutoreleasedReturnValue(-[UIDevice systemVersion](v22, "systemVersion")));// 获取 systemVersion
v24 = sub_10000E23C(-[NSString UTF8String](v23, "UTF8String"));
sub_10000E098(a2, v159, v24);
objc_release(v23);
objc_release(v22);
v25 = arc4random() % 5;
v26 = arc4random() % 0xA;
LODWORD(v23) = v26 + 8;
v27 = malloc(v25 + 9); // 随机key
v28 = malloc(v26 + 9);
sub_10002C318(v27, v25 | 8); // 生成一组随机键名/随机值,长度为 8~17 插入请求对象
sub_10002C318(v28, v23);
v29 = sub_10000E23C(v28);
sub_10000E098(a2, v27, v29);
free(v27);
free(v28);
v30 = sub_10000DA4C(a2); // 把内部对象树序列化成 JSON 字符串(sub_10000DA4C)。
v31 = objc_retainAutoreleasedReturnValue(sub_10002EC94(0, v30));// sub_10002C4D0 在这里调用 sub_10002EC94 对序列化后的请求 JSON 做 RSA 分块加密 + Base64,结果作为后续 HTTP POST body。
memset(v178, 0, sizeof(v178));
v32 = 1;
v157 = malloc(1u);
v158 = 0;
v33 = sub_100184164(); // 初始化 libcurl easy handle,并分配响应缓冲区(v75/v76 由 write callback 持续扩容保存服务端返回)。
bzero(v177, 0x3FFu);
v34 = sub_10017E8C8(4);
snprintf(v177, 0x400u, "libcurl/%s", *(v34 + 8));
v177[1023] = 0;
sub_1001A01E0(v33, 10018, v35, v36, v37, v38, v39, v40, v177);// 设置 CURLOPT_USERAGENT = "libcurl/<version>"。
v156 = ':tcepxE';
v41 = sub_10018FCE4(0, &v156);
strcpy(v176, "Content-Type: application/json");
v42 = sub_10018FCE4(v41, v176); // 构造并追加 HTTP 头
sub_1001A01E0(v33, 13, v43, v44, v45, v46, v47, v48, 5);// 设置 CURLOPT_TIMEOUT = 5 秒。
sub_1001A01E0(v33, 78, v49, v50, v51, v52, v53, v54, 3);// 设置 CURLOPT_CONNECTTIMEOUT = 3 秒。
sub_1001A01E0(v33, 64, v55, v56, v57, v58, v59, v60, 0);// 设置 CURLOPT_SSL_VERIFYPEER = 0,关闭证书校验。
sub_1001A01E0(v33, 81, v61, v62, v63, v64, v65, v66, 0);// 设置 CURLOPT_SSL_VERIFYHOST = 0,关闭主机名校验。
v155 = v42;
sub_1001A01E0(v33, 10023, v67, v68, v69, v70, v71, v72, v42);
sub_1001A01E0(v33, 47, v73, v74, v75, v76, v77, v78, 1);
v79 = objc_retainAutorelease(v31);
v80 = objc_msgSend(v79, "bytes");
sub_1001A01E0(v33, 10015, v81, v82, v83, v84, v85, v86, v80);// 设置 CURLOPT_POSTFIELDS = 请求密文(Base64)缓冲区。
v154 = v79;
v87 = objc_msgSend(v79, "length");
sub_1001A01E0(v33, 60, v88, v89, v90, v91, v92, v93, v87);// 设置 CURLOPT_POSTFIELDSIZE = 请求密文长度。
sub_1001A01E0(v33, 41, v94, v95, v96, v97, v98, v99, 1);
sub_1001A01E0(v33, 10010, v100, v101, v102, v103, v104, v105, v178);
sub_1001A01E0(v33, 20011, v106, v107, v108, v109, v110, v111, sub_10002C1D8);// 设置 CURLOPT_WRITEFUNCTION = sub_10002C1D8,用于把 HTTP 响应内容累计写入 v75/v76。
sub_1001A01E0(v33, 10001, v112, v113, v114, v115, v116, v117, &v157);// 设置 CURLOPT_WRITEDATA = &v74 相关上下文,供 write callback 保存响应数据。
while ( 1 )
{
v118 = v32;
v119 = objc_retainAutoreleasedReturnValue(sub_10002CFE8());// 选择当前要访问的 host(sub_10002CFE8)。
v120 = sub_10002D644(v119);
v121 = objc_retainAutorelease(v119);
v122 = objc_msgSend(v121, "UTF8String");
v175 = 0;
v173 = 0u;
v174 = 0u;
v171 = 0u;
v172 = 0u;
v169 = 0u;
v170 = 0u;
v167 = 0u;
v168 = 0u;
v165 = 0u;
v166 = 0u;
v123 = v120 ? &v161 : v180;
v163 = 0u; // 拼接最终 URL
v164 = 0u;
__strcpy_chk(&v163, v123, 200);
__strcat_chk(&v163, v122, 200);
__strcat_chk(&v163, v179, 200);
__strcat_chk(&v163, a1, 200);
sub_1001A01E0(v33, 10002, v124, v125, v126, v127, v128, v129, &v163);
free(v157);
v157 = malloc(1u);
v158 = 0;
if ( !sub_1001841B4(v33) )
break; // 执行 curl_easy_perform。成功则跳出循环;失败则调用 sub_10002CFA8/sub_10002CCF0 轮换 host/索引后重试一次。
v130 = sub_10002CFA8();
sub_10002CCF0(v130);
objc_release(v121);
v32 = 0;
if ( (v118 & 1) == 0 )
{
v131 = 0;
v132 = v153;
v134 = v150;
v133 = v151;
goto LABEL_20;
}
}
objc_release(v121);
v135 = objc_alloc(&OBJC_CLASS___NSData);
v136 = objc_msgSend(v135, "initWithBytes:length:", v157, v158);// 把 write callback 收到的原始响应字节包装成 NSData。
v137 = objc_msgSend(objc_alloc(&OBJC_CLASS___NSData), "initWithBase64EncodedData:options:", v136, 1);// 对响应文本做 Base64 解码,得到二进制密文。
v138 = 0;
LOBYTE(v165) = 0;
v162[16] = 0;
v139 = *(v149 + 17); //从前面生成的参数d中得到解密初始值:v81/v82 取前 32 字节作为 AES-256 key,v80 取最后 16 字节倒序作为 IV。
v163 = *(v149 + 1);
v164 = v139;
v140 = (v149 + 33);
do
{
v141 = *v140--;
*&v162[v138] = vrev64_s8(v141);
v138 += 8;
}
while ( v138 != 16 );
v142 = objc_retainAutorelease(v137);
v143 = objc_msgSend(v142, "bytes");
v144 = objc_msgSend(v142, "length");
v145 = &v148 - ((v144 + 15) & 0xFFFFFFFFFFFFFFF0LL);
bzero(v145, v144);
v146 = sub_10002E968(v143, objc_msgSend(v142, "length"), &v163, v162, v145);// // 调用 sub_10002E968 解密响应:算法已可确认是 AES-256-CBC + PKCS#7 padding。输入为 Base64 解码后的密文,输出为明文 JSON。
v133 = v151;
v134 = v150;
if ( v146 < 1 )
{
v131 = 0;
}
else
{
v145[v146] = 0;
v131 = sub_10000D92C(v145); // 若解密成功且长度 > 0,则把明文字符串作为 JSON 解析成内部对象并返回;否则返回 0。
}
v132 = v153;
objc_release(v142);
objc_release(v136);
LABEL_20:
free(v157);
sub_10018FDFC(v155);
sub_100184390(v33);
objc_release(v154);
objc_release(v134);
objc_release(v133);
objc_release(v132);
return v131;
}

具体分析

请求侧:

参数c分析:

通过交叉引用主请求函数sub_10002C4D0跟到函数sub_100007920

img

继续查看函数调用关系最终跟到函数-[hdVWMVzzjaKwmOFRnWPjPZjgurropaFTOsSacoaX mGszNNdLYBCczbtbJYYnbFIGZAYBeDLZYdcaOrLr:],而其主要逻辑是激活框的UI页面展示

img

通过函数sub_100009694跟到函数-[hdVWMVzzjaKwmOFRnWPjPZjgurropaFTOsSacoaX BrgDgGVvDpYFXxSlDIQVyIEuftIBMafdLGDMUiKE]进而跟到sub_100007920

img

img

img

而跟进函数sub_100009568 --> mlZnRMHhNGWhoNmjlNODKyEmNRuWRjfvlqwpPEHB的逻辑可以得知激活码长度为40

img

所以参数c的含义就是:输入的长度为40的激活码,与hook值一致。

参数d分析:

通过函数sub_10000B4F8,返回一个全局字符串qword_100272D38取d的原值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
v4 = objc_retainAutoreleasedReturnValue(sub_10000B4F8());// 返回一个全局字符串qword_100272D38 取d的原值 
v5 = objc_msgSend(v4, "lengthOfBytesUsingEncoding:", 4);
v6 = objc_retainAutorelease(v4);
v7 = objc_msgSend(v6, "UTF8String");
v8 = &v148 - ((v5 + 17) & 0xFFFFFFFFFFFFFFF0LL);
v8[v5 + 1] = 0;
sub_1000236BC(v7, v8); // 把 getNormalizedIFID() 返回的设备标识写入临时缓冲区并做加密:首字节写控制字节,后续每个字符按 round(sqrt(i*step)) 交替加减偏移。后续会作为参数 d 进行base64编码,后续参与响应解密时的密钥/IV 派生。
v9 = strlen(v8);
v149 = v8;
v151 = objc_retainAutoreleasedReturnValue(+[NSData dataWithBytes:length:](&OBJC_CLASS___NSData, "dataWithBytes:length:", v8, v9));
v10 = objc_retainAutoreleasedReturnValue(sub_100020960(v151));// 把加密后的设备标识 NSData 做标准 Base64 编码,随后写入请求对象字段 "d"。因此 d = Base64(加密后的设备标识)
v160 = 'd';
v11 = objc_retainAutorelease(v10);
v12 = sub_10000E23C(objc_msgSend(v11, "UTF8String"));
sub_10000E098(a2, &v160, v12); // 写入请求对象d

跟进函数sub_10000B4F8分析发现就是一个返回一个全局字符串,交叉引用跟到函数-[kSAkHVfYbEfPcnXAGCCEjMTXDaxNzHvHdGjKMhxl application:didFinishLaunchingWithOptions:],参数d的原始值来自+[PvQxldgWzfOsUeJZrxmyNaFCPmDycLCYbdkaSEFu lkpgSgOYxufbYXuPdLuCwLmwjVqMbLPZXffviehp]

而这个函数的逻辑就是 ios 设备指纹采集

img

随后通过函数sub_1000236BC(可逆算法)跟函数sub_100020960(base64)进行一个加密

所以参数d :Base64(加密的设备标识)

参数p,v分析

参数p,v的生成方式跟d一致,均是通过一个全局变量的值去进行赋值,这里不过多赘述,具体可看下图

img

img

参数p:直接设置成固定字符串

参数v:版本字段

参数ov分析

通过以下代码可以确定参数ov为iOS的版本号

img

两个混淆参数分析

通过分析主请求函数sub_10002C4D0的流程可以得知,中间会插入随机的key跟value

img

而通过交叉引用到sub_10002C4D0的上层函数可以得知在写入参数c之前也生成了一个随机参数

img

img

跟hook出的结构一致

参数at分析

依旧查看函数调用关系,最终通过全局变量进行交叉引用可以跟到函数sub_100023F34

img

可以得知是一个服务端下发的参数 流程为:服务端下发 --> 客户端保存 --> 后续多个请求回传

响应侧:

参数s,m:

依旧分析函数sub_100007920

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
v17 = 's';                                    // s == 0 判断成功
v8 = *(sub_10000DFB0(v7, &v17) + 40);
v9 = v8 == 0;
if ( v8 )
{
v16 = 'm'; // m 作为失败提示字符串展示
v10 = objc_retainAutoreleasedReturnValue(
+[NSString stringWithUTF8String:](
&OBJC_CLASS___NSString,
"stringWithUTF8String:",
*(sub_10000DFB0(v7, &v16) + 32)));
v11 = objc_retainAutorelease(&_dispatch_main_q);
block[0] = _NSConcreteStackBlock;
block[1] = 3254779904LL;
block[2] = sub_100007A9C;
block[3] = &unk_10022CCA0;
v15 = v10;
v12 = objc_retain(v10);
dispatch_async(&_dispatch_main_q, block);
objc_release(v15);
objc_release(v12);

参数s:判断激活成功与否的标志

参数m:作为激活失败字符串进行提示

其他参数分析

部分参数通过hook并没有得到,但是通过函数sub_100023F34可以得知还有其他参数,于是从函数sub_100023F34开始分析。经过分析函数sub_100023F34可以得知是一个启动阶段后台配置拉取的函数 函数开始有一个检测

img

随后函数大致分为三个相同含义的分组,皆是设置ip、拼接请求体、构造参数,具体如下

img

img

img

通过交叉引用全局变量可以跟到函数-[VCUCStepMain WVvPVKMdAKpXVBQjOrSobbqYNIKJChqzGFTrEiYO]

img

可以得知参数ak:setActive_key,参数as:setStatus

通过一样的方式查看ps、vs、hs、ts的值,最终跟到函数-[VCUCStepMain iypKLJmjdPkAaoFhiuQjyABYwmXwnYtKjjdypKYS]

img

可以发现是服务器下发的一些策略值或者阈值,超过或者未达到则拦截

加密/解密分析

初步分析

通过frida hook出密文以及明文

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
请求密文(Base64): XmdeOVps...
请求 JSON:
{
"jqPSOv2JRS": "fhSKRRGqUe",
"c": "1111111111222222222233333333334444444444",
"d": "K2BoLWhd...",
"p": "...",
"v": "...",
"at": "00000000000000000000000000000000",
"ov": "16.0.9",
"A83cKhSaNZ": "opYBZy44UaoSlMR29"
}

响应密文(Base64): YsDNR6rRAHnSxTioj1TDg/hjtWdT8nZAtpfXmnvAxwD...
响应 JSON:
{
"phpvxy": "79jrabwh",
"s": 1,
"m": "激活参数有误",
"npsde": "r8ro5biah"
}

主要加密:

请求方向

客户端把 JSON 明文RSA 加密 –> Base64 编码 –> 发给服务器

服务器收到后:Base64 解码 –> 用 RSA 私钥解密 –> 得到 JSON 明文

具体加密流程(RSA):

  1. 通过函数-[kSAkHVfYbEfPcnXAGCCEjMTXDaxNzHvHdGjKMhxl application:didFinishLaunchingWithOptions:](最开始的初始化阶段的函数)进行构造一段硬编码 PEM 公钥文本,通过sub_10002EA30把 PEM 文本包装成内存 BIO,再调用sub_1000DC9C4解析 PUBLIC KEY,最后把结果保存到qword_100273A18
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
strcpy(
__s,
"-----BEGIN PUBLIC KEY-----\n"
"MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDThwbpcEtsJX3KkLwFJBVajDhG\n"
"ND0MoFbEV6BTFyW7Eu...\n"
"-----END PUBLIC KEY-----\n"); // 硬编码 RSA 公钥
sub_10002EA30(0, __s); // 应用启动阶段载入 RSA 公钥:a1=0 时将 PEM 公钥解析后保存到 qword_100273A18,后续给 sub_10002EC94 对请求 JSON 做 RSA 分块加密。

// 把传入的 PEM 文本先包装成内存 BIO,再解析为 key
__int64 __fastcall sub_10002EA30(__int64 a1, char *__s)
{
__int64 v4; // x0
__int64 v5; // x19

strlen(__s);
v4 = sub_100063108(__s); // 包装成内存 BIO
v5 = v4;
if ( a1 ) // 调用 sub_1000DC9C4 解析 PUBLIC KEY,最后把结果保存到 qword_100273A18
qword_100273A20 = sub_1000DC95C(v4, 0, 0, 0);
else
qword_100273A18 = sub_1000DC9C4(v4, 0, 0, 0);
return sub_100061AF0(v5);
}
  1. 交叉引用qword_100273A18跟到sub_10002EC94函数 也就是请求体加密的主函数
  2. 请求体加密主函数首先通过sub_1000EBF70取 RSA 密钥模长字节数,也就是RSA_size(key),然后用RSA_size(key) - 11作为单块最大明文长度,接着把整段 JSON 按块切开,再通过sub_1000EBF9C逐块做 RSA 公钥加密,最后base64编码。
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
id __fastcall sub_10002EC94(__int64 a1, const char *a2)
{
v3 = objc_msgSend(objc_alloc(&OBJC_CLASS___NSData), "initWithBytes:length:", a2, strlen(a2));
v4 = objc_msgSend(v3, "length");
v5 = &qword_100273A18;
if ( a1 )
v5 = &qword_100273A20;
v6 = (sub_1000EBF70(*v5) - 11); // 计算 RSA 单块最大明文长度
v7 = objc_retainAutoreleasedReturnValue(+[NSMutableData data](&OBJC_CLASS___NSMutableData, "data"));
v8 = ceilf(v4 / v6);
if ( v8 > 0.0 )
{
v9 = 0.0;
v10 = 1;
do
{
v11 = (v9 * v6); // 循环分块处理请求明文,每轮取一个子片段进入 RSA 加密。
if ( (v4 - v11) <= v6 )
v12 = v4 - v11;
else
v12 = v6;
v13 = objc_retainAutoreleasedReturnValue(objc_msgSend(v3, "subdataWithRange:", v11, v12));
v14 = malloc(v6);
bzero(v14, v6);
v15 = objc_msgSend(v13, "length");
v16 = objc_retainAutorelease(v13);
v17 = objc_msgSend(v16, "bytes");
if ( a1 )
{
v18 = sub_1000EBFA8(v15, v17, v14, qword_100273A20);
if ( !v18 )
goto LABEL_14;
}
else
{
v18 = sub_1000EBF9C(v15, v17, v14, qword_100273A18);// 请求常规路径的 RSA 公钥加密调用:sub_1000EBF9C(..., qword_100273A18)。成功后将每块密文 append 到 NSMutableData。
if ( !v18 )
{
LABEL_14:
free(v14);
objc_release(v16);
v19 = 0;
goto LABEL_15;
}
}
objc_msgSend(v7, "appendBytes:length:", v14, v18);
free(v14);
objc_release(v16);
v9 = v10++;
}
while ( v8 > v9 );
}
v19 = objc_retainAutoreleasedReturnValue(sub_100020A24(v7));// 把拼接后的 RSA 二进制密文整体经过 Base64 编码,得到通过 libcurl POST 发送的请求体
LABEL_15:
objc_release(v7);
objc_release(v3);
return objc_autoreleaseReturnValue(v19);
}

响应方向:

服务器返回的数据是另一套:服务器把 JSON 明文AES 加密 –> 再 Base64 编码发回客户端

客户端收到后:先 Base64 解码 –> 用 AES key/iv 解密 –> 得到 JSON 明文

具体加密流程(AES):

程序接收到数据后,首先先进行base64解码,得到二进制密文,随后通过参数d的值取出key跟iv,最后调用函数sub_10002E968进行解密

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
v137 = objc_msgSend(objc_alloc(&OBJC_CLASS___NSData), "initWithBase64EncodedData:options:", v136, 1);// 对响应文本做 Base64 解码,得到二进制密文。
v138 = 0;
LOBYTE(v165) = 0;
v162[16] = 0;
v139 = *(v149 + 17); // 从前面生成的参数d中得到解密初始值:v81/v82 取前 32 字节作为 AES-256 key,v80 取最后 16 字节倒序作为 IV。
v163 = *(v149 + 1);
v164 = v139;
v140 = (v149 + 33);
do
{
v141 = *v140--;
*&v162[v138] = vrev64_s8(v141);
v138 += 8;
}
while ( v138 != 16 );
v142 = objc_retainAutorelease(v137);
v143 = objc_msgSend(v142, "bytes");
v144 = objc_msgSend(v142, "length");
v145 = &v148 - ((v144 + 15) & 0xFFFFFFFFFFFFFFF0LL);
bzero(v145, v144);
v146 = sub_10002E968(v143, objc_msgSend(v142, "length"), &v163, v162, v145);// 调用 sub_10002E968 解密

而函数sub_10002E968后续调用sub_1000B78EC,且函数sub_1000B77CC的返回值0x1002370C0

对应数据为20 00 00 00 10 00 00 00 → key_len=0x20(32), iv_len=0x10(16),而且最后有padding 校验逻辑

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
__int64 __fastcall sub_10002E968(__int64 a1, __int64 a2, __int64 a3, __int64 a4, __int64 a5)
{
__int64 v10; // x0
__int64 v11; // x19
void *v12; // x0
int v13; // w21
__int64 v14; // x20
int v16; // [xsp+Ch] [xbp-34h] BYREF

v10 = sub_1000C1B60();
if ( !v10
|| (v11 = v10, v12 = sub_1000B77CC(), sub_1000C26D8(v11, v12, 0, a3, a4) != 1)
|| sub_1000C20A8(v11, a5, &v16, a1, a2) != 1
|| (v13 = v16, sub_1000C2478(v11, a5 + v16, &v16) != 1) )
{
abort();
}
v14 = (v16 + v13);
sub_1000C1B74(v11);
return v14;
}

__int64 __fastcall sub_1000B78EC(__int64 a1, __int64 a2, __int64 a3, int a4)
{
__int64 v7; // x19
__int64 v8; // x0
int v9; // w23
__int64 v10; // x1
int v11; // w0
__int64 (__fastcall *v12)(); // x8
__int64 (__fastcall *v13)(); // x9

v7 = sub_1000C36E0();
v8 = sub_1000C3304(a1);
v9 = sub_1000C32FC(v8) & 0xF0007;
v10 = 8 * sub_1000C3720(a1);
if ( a4 || (v9 - 1) > 1 )
{
v11 = sub_10004DFD0(a2, v10, v7);
v12 = sub_10004E55C;
}
else
{
v11 = sub_10004E31C(a2, v10, v7);
v12 = sub_10004E8F8;
}
v13 = sub_10004DFB4;
if ( v9 != 2 )
v13 = 0;
*(v7 + 248) = v12;
*(v7 + 256) = v13;
if ( (v11 & 0x80000000) == 0 )
return 1;
sub_1000B43A8(6, 133, 143, "crypto/evp/e_aes.c", 2662);
return 0;
}

解密脚本:

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
import base64
import math
from typing import Tuple

from Crypto.Cipher import AES

D_BASE64 = "K2BoLWhdQVx..."
CIPHERTEXT_BASE64 = "zF5xkrEC+tdPw4Unwz2pZhNNp99aUQeZtICNt4OTa..."
def decode_d_field(b64: str) -> Tuple[int, int, bytes, str]:
raw = base64.b64decode(b64)
first_byte = raw[0]
step = first_byte & 0x0F
out = bytearray()

for i in range(len(raw) - 1):
index = i + 1
delta = round(math.sqrt(index * step))

sign = -1 if (index & 1) else 1
if (step & 1) == 0:
sign = -sign

orig = (raw[index] - sign * delta) & 0xFF
out.append(orig)

return first_byte, step, raw, out.decode("utf-8")

def derive_key_iv(obfuscated_bytes: bytes) -> Tuple[bytes, bytes]:
key = obfuscated_bytes[1:33]
iv = obfuscated_bytes[25:41][::-1]
return key, iv

def pkcs7_unpad(data: bytes) -> bytes:
pad = data[-1]
return data[:-pad]

def decrypt_response(cipher_b64: str, key: bytes, iv: bytes) -> str:
cipher_bytes = base64.b64decode(cipher_b64)
plain_padded = AES.new(key, AES.MODE_CBC, iv).decrypt(cipher_bytes)
plain = pkcs7_unpad(plain_padded)
return plain.decode("utf-8")

def main():
first_byte, step, obfuscated, device_id = decode_d_field(D_BASE64)
key, iv = derive_key_iv(obfuscated)

print("设备标识:", device_id)
print("===============================================================")
print("AES Key:", key.hex())
print("AES IV:", iv.hex())
print("===============================================================")
print("响应明文:", decrypt_response(CIPHERTEXT_BASE64, key, iv))

if __name__ == "__main__":
main()

最终解密出的值跟hook出的值一致 解密成功

img