iOS逆向实战 -- 某激活程序网络请求分析
前言
本文内容仅供技术研究与学习交流之用,严禁用于任何商业用途。
文中所涉及的技术手段、工具及分析过程,均基于公开或开源信息探讨,旨在分享技术原理。
作者坚决反对且不鼓励任何利用此类技术从事违法违规操作的行为。
若读者滥用本文技术造成任何不良后果,均由使用者自行承担,与本文作者无关。
本人技术水平有限,本文更多是个人摸索过程中的总结与沉淀。如有疏漏或不当之处,欢迎各位大佬探讨指正。
激活流程分析
初始化阶段
先砸壳,随后 ida 打开程序,跟到 start 函数,start 函数有反调试检测,但是我们的主要目的是分析其网络请求加密及参数生成方式以及含义,所以暂时不住要介绍如何对抗这些检测。
start 函数的末尾进入 iOS 的应用生命周期

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

得知其就是对于全局变量进行赋值,便于后续操作,赋值完成之后进入UIApplicationMain,把程序交给 UIKit,进入 App 正常生命周期
数据包发送阶段
通过分析VCMain的类的其他函数,最终跟到激活框的UI展示函数-[hdVWMVzzjaKwmOFRnWPjPZjgurropaFTOsSacoaX mGszNNdLYBCczbtbJYYnbFIGZAYBeDLZYdcaOrLr:]
1 | v13 = objc_retainAutoreleasedReturnValue( |
先分析函数sub_100009568,他首先调用方法mlZnRMHhNGWhoNmjlNODKyEmNRuWRjfvlqwpPEHB
1 | void __fastcall sub_100009568(__int64 a1, id a2) |
继续跟进可以得知其逻辑是文本框获取输入-->去掉空格-->校验长度,激活码长度为40

跟踪函数sub_100009694跟到BrgDgGVvDpYFXxSlDIQVyIEuftIBMafdLGDMUiKE发现其是一个点击激活按钮的函数
1 | bool __cdecl -[hdVWMVzzjaKwmOFRnWPjPZjgurropaFTOsSacoaX BrgDgGVvDpYFXxSlDIQVyIEuftIBMafdLGDMUiKE]( |
继续分析,最终跟到主请求函数sub_10002C4D0,也就是我们要分析的主要函数
激活阶段参数生成方式及意义分析
初步分析
先通过 frida 去 hook 下其请求 json 跟响应 json
1 | 请求 JSON: |
主请求函数以及分析如下
1 | __int64 __fastcall sub_10002C4D0(__int64 a1, __int64 a2) |
具体分析
请求侧:
参数c分析:
通过交叉引用主请求函数sub_10002C4D0跟到函数sub_100007920

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

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



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

所以参数c的含义就是:输入的长度为40的激活码,与hook值一致。
参数d分析:
通过函数sub_10000B4F8,返回一个全局字符串qword_100272D38取d的原值
1 | v4 = objc_retainAutoreleasedReturnValue(sub_10000B4F8());// 返回一个全局字符串qword_100272D38 取d的原值 |
跟进函数sub_10000B4F8分析发现就是一个返回一个全局字符串,交叉引用跟到函数-[kSAkHVfYbEfPcnXAGCCEjMTXDaxNzHvHdGjKMhxl application:didFinishLaunchingWithOptions:],参数d的原始值来自+[PvQxldgWzfOsUeJZrxmyNaFCPmDycLCYbdkaSEFu lkpgSgOYxufbYXuPdLuCwLmwjVqMbLPZXffviehp]
而这个函数的逻辑就是 ios 设备指纹采集

随后通过函数sub_1000236BC(可逆算法)跟函数sub_100020960(base64)进行一个加密
所以参数d :Base64(加密的设备标识)
参数p,v分析
参数p,v的生成方式跟d一致,均是通过一个全局变量的值去进行赋值,这里不过多赘述,具体可看下图


参数p:直接设置成固定字符串
参数v:版本字段
参数ov分析
通过以下代码可以确定参数ov为iOS的版本号

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

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


跟hook出的结构一致
参数at分析
依旧查看函数调用关系,最终通过全局变量进行交叉引用可以跟到函数sub_100023F34

可以得知是一个服务端下发的参数 流程为:服务端下发 --> 客户端保存 --> 后续多个请求回传
响应侧:
参数s,m:
依旧分析函数sub_100007920
1 | v17 = 's'; // s == 0 判断成功 |
参数s:判断激活成功与否的标志
参数m:作为激活失败字符串进行提示
其他参数分析
部分参数通过hook并没有得到,但是通过函数sub_100023F34可以得知还有其他参数,于是从函数sub_100023F34开始分析。经过分析函数sub_100023F34可以得知是一个启动阶段后台配置拉取的函数 函数开始有一个检测

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



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

可以得知参数ak:setActive_key,参数as:setStatus
通过一样的方式查看ps、vs、hs、ts的值,最终跟到函数-[VCUCStepMain iypKLJmjdPkAaoFhiuQjyABYwmXwnYtKjjdypKYS]

可以发现是服务器下发的一些策略值或者阈值,超过或者未达到则拦截
加密/解密分析
初步分析
通过frida hook出密文以及明文
1 | 请求密文(Base64): XmdeOVps... |
主要加密:
请求方向
客户端把 JSON 明文:RSA 加密 –> Base64 编码 –> 发给服务器
服务器收到后:Base64 解码 –> 用 RSA 私钥解密 –> 得到 JSON 明文
具体加密流程(RSA):
- 通过函数
-[kSAkHVfYbEfPcnXAGCCEjMTXDaxNzHvHdGjKMhxl application:didFinishLaunchingWithOptions:](最开始的初始化阶段的函数)进行构造一段硬编码 PEM 公钥文本,通过sub_10002EA30把 PEM 文本包装成内存 BIO,再调用sub_1000DC9C4解析PUBLIC KEY,最后把结果保存到qword_100273A18
1 | strcpy( |
- 交叉引用
qword_100273A18跟到sub_10002EC94函数 也就是请求体加密的主函数 - 请求体加密主函数首先通过
sub_1000EBF70取 RSA 密钥模长字节数,也就是RSA_size(key),然后用RSA_size(key) - 11作为单块最大明文长度,接着把整段 JSON 按块切开,再通过sub_1000EBF9C逐块做 RSA 公钥加密,最后base64编码。
1 | id __fastcall sub_10002EC94(__int64 a1, const char *a2) |
响应方向:
服务器返回的数据是另一套:服务器把 JSON 明文 用 AES 加密 –> 再 Base64 编码发回客户端
客户端收到后:先 Base64 解码 –> 用 AES key/iv 解密 –> 得到 JSON 明文
具体加密流程(AES):
程序接收到数据后,首先先进行base64解码,得到二进制密文,随后通过参数d的值取出key跟iv,最后调用函数sub_10002E968进行解密
1 | v137 = objc_msgSend(objc_alloc(&OBJC_CLASS___NSData), "initWithBase64EncodedData:options:", v136, 1);// 对响应文本做 Base64 解码,得到二进制密文。 |
而函数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 | __int64 __fastcall sub_10002E968(__int64 a1, __int64 a2, __int64 a3, __int64 a4, __int64 a5) |
解密脚本:
1 | import base64 |
最终解密出的值跟hook出的值一致 解密成功
