本文仅用于逆向研究交流,禁止非法用途,如遇侵权联系删除!!!
目录
建议先阅读我之前的小程序篇,了解整体思路。
某幸咖啡小程序端 sign、uid、q 参数逆向详解(MD5+AES)
- 目标版本:v5.4.20
- 系统版本:iOS 13
- 测试设备:iPhone 8
抓包方案
该应用启用了 SSL Pinning,直接抓包无法获取数据。
- 安卓端:推荐使用 LSPosed + JustTrustMe
- iOS 端:
- iOS 13 及以下:SSL Kill Switch 2
- iOS 15:SSL Kill Switch 3
- 抓包工具:Reqable
建议先阅读我之前的小程序篇,了解整体思路。
SSL Kill Switch 2安装
打开 Cydia


然后在设置里面打开就可以了

接口分析
优化后的表达:
接口调用路径:
- 打开APP,选择任意一家店铺
- 点击页面顶部的"其他店铺"选项
- 进入店铺地图页面
当看到地图和底部列表界面时,即可抓取数据包
关键字搜索: YXBpLmxrY29mZmVlLmNvbS9yZXNvdXJjZS9tL3Nob3Avc2hvcExpcw==
检查数据包详情并分析加密方式

sign 纯数字
cid 固定220101 小程序是23010 版本不一样
t 时间戳
q 加密字符串
返回值 加密字符串
frida trace hook
让我们先分析URL参数加密部分
这些参数看起来不像MD5加密,暂时不需要hook CC_MD5
先hook URL接口触发点
关于frida-trace命令:
- 使用端口连接时添加-H参数
- 使用USB连接时添加-U参数
frida-trace -UF -m "-[NSMutableURLRequest setHTTPBody:]"
frida-trace -H 127.0.0.58:6666 -F -m "+[NSURL URLWithString:]"
触发命令之后会发现成功了但是用很多无用的接口,需要筛选掉并且打印栈

我们需要对生成的hook文件进行修改
如果你是在cmd 执行的命令文件大概率是在
C:\Users\用户名\__handlers__
命令执行之后也会显示文件的位置

打开文件进行编辑
源文件如下

修改后 对url进行筛选只有含sign的才会输出并且打印栈
/*
* Auto-generated by Frida. Please modify to match the signature of +[NSURL URLWithString:].
* This stub is currently auto-generated from manpages when available.
*
* For full API reference, see: https://frida.re/docs/javascript-api/
*/
defineHandler({
onEnter(log, args, state) {
var url = new ObjC.Object(args[2]);
// log(url.toString().includes('sign'))
if (url.toString().includes('sign')){
log(url)
log( Thread.backtrace(this.context, Backtracer.ACCURATE)
.map(DebugSymbol.fromAddress).join('\n') + '\n');
}
else {
// log(`+[NSURL URLWithString:${url}]`);
}
},
onLeave(log, retval, state) {
}
});
保存之后再次启动命令 触发接口进行hook
hook成功并且打印了栈

我们来分析一下栈
# 1. 用户操作触发(顶层触发点)
-[LCPickUpListView moveLocationHandler:] // 移动地图位置(用户操作)
↓
-[LCShopAddressViewModel requestNearByShopList:] // 视图模型发起附近店铺列表请求
↓
-[LuckinStoreManger requestPickerShopNearByList:complete:] // 店铺管理类转发请求
↓
-[LNSessionManager addRequest:] // 网络管理器接收请求(之前分析的 LNSessionManager)
↓
-[LNSessionManager __taskWithRequest:] // 创建网络任务
↓
-[LNSessionManager __HTTPTaskWithRequest:] // 创建 HTTP 任务
↓
-[AFHTTPRequestSerializer requestWithMethod:URLString:parameters:error:] // AFN 原生序列化入口
↓
# 2. 关键加密步骤(核心!)
-[LNAESRequestSerializer requestBySerializingRequest:withParameters:error:] // AES 加密序列化
↓
-[AFHTTPRequestSerializer requestBySerializingRequest:withParameters:error:] // AFN 原生序列化(被重写扩展)
所以说我们接下来应该对app进行反编译
进行ida分析怎么生成的参数
cracker脱壳
cracker教程网上有很多这里不做介绍
脱壳后找到脱壳完成的ipa文件所在位置‘
比如我的就在这个路径下
/var/mobile/Documents/CrackerXI
将ipa文件传回电脑
scp root@172.16.2.58:/var/mobile/Documents/CrackerXI/LuckyClient_5.4.20_CrackerXI.ipa C:\Users\15504\Desktop\files
执行之后输入ipone root默认密码 alpine
拿到ipa文件之后需要把ipa 后缀改成zip 解压
就可以获得主文件

IDA分析
将文件拖入到IDA进行反编译
需要等待一会,因为安卓的so文件都很小,但是ios的主文件都是100mb左右所以需要等
在栈分析中我们以及知道
LNAESRequestSerializer requestBySerializingRequest 函数是核心
我们直接通过ida左侧的函数栏搜索

按下F5看下
可以看到sign cid等字符串 这应该就是我们要找的位置了

分析q
因为q大概率就是query的意思我们先分析q的生成
此处首次出现了q 取了明文参数

重点在这三步

v78 = objc_msgSend(objc_alloc((Class)&OBJC_CLASS___NSString), "initWithData:encoding:", v28, 4LL); // UTF-8编码
v79 = objc_retainAutoreleasedReturnValue(-[LNAESRequestSerializer luckinSerializerString](self, "luckinSerializerString")); // 获取密钥
v32 = objc_retainAutoreleasedReturnValue(objc_msgSend(v78, "_6_rAiqot5yzxotm1oznGKY8bcKtixEvzQkE:", v79)); // 加密/混淆处理
- 将 JSON 数据(
v28(明文的json))按UTF-8 编码转换为字符串(v78) - 调用当前类的
luckinSerializerString方法获取加密密钥(v79) - 对 UTF-8 字符串调用私有方法
_6_rAiqot5yzxotm1oznGKY8bcKtixEvzQkE:进行加密 / 混淆处理,传入密钥
所以说我们现在需要获取到密钥 就可以试着测试是不是标准的AES加密 如果是就直接能反推出q的明文
我们采用hook的方式 先拿到密钥
命令 模糊匹配函数关键字
frida-trace -H 172.16.2.58:6666 -F -m "-[LNAESRequestSerializer luckinSerializerString]"
触发接口进行hook 可以看到触发成功,我们现在需要对hook文件重写看到明文

重写hook 文件 输出明文key
/*
* Auto-generated by Frida. Please modify to match the signature of -[LNAESRequestSerializer luckinSerializerString].
* This stub is currently auto-generated from manpages when available.
*
* For full API reference, see: https://frida.re/docs/javascript-api/
*/
defineHandler({
onEnter(log, args, state) {
log(`-[LNAESRequestSerializer luckinSerializerString]`);
},
onLeave(log, retval, state) {
const key = new ObjC.Object(retval);
log(key)
}
});
成功拿到key 多次触发key 均一致

到这里我们已经拿到key了可以先验证下是不是aes加密,看看用这个key能不能解密出来
小程序的aes加密是先删除-+等字符串
加密模式是下面的
const decryptOptions = {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
};
e.replace(/-/g, '+').replace(/_/g, '/');
将密钥替换可以看到 能拿到明文了

那么返回数据加密也很有可能是aes我们复制一下试一试也成功了

目前还剩下sign
sign的特征为纯数字再回头看下sign的生成
回到函数LNAESRequestSerializer requestBySerializingRequest
sign第一步逻辑是将q 上面我们得到的,uid 随机就行在ck里面,cid 固定值,t时间戳
流程如下
1 . 初始化
v8 = objc_retainAutoreleasedReturnValue(+[NSMutableDictionary dictionary](&OBJC_CLASS___NSMutableDictionary, "dictionary"));
// 2. 存入uid:
v10 = objc_retainAutoreleasedReturnValue(objc_msgSend(v7, "objectForKeyedSubscript:", CFSTR("uid")));
if ( !v10 )
{
objc_msgSend(v8, "setObject:forKeyedSubscript:", &stru_104C95F80, CFSTR("uid"));
goto LABEL_11;
}
v11 = objc_retainAutoreleasedReturnValue(objc_msgSend(v7, "objectForKeyedSubscript:", CFSTR("uid")));
objc_msgSend(v8, "setObject:forKeyedSubscript:", v11, CFSTR("uid")); // 存入uid
// 3. 存入cid:
v15 = objc_retainAutoreleasedReturnValue(objc_msgSend(v7, "objectForKeyedSubscript:", CFSTR("cid")));
objc_msgSend(v8, "setObject:forKeyedSubscript:", v15, CFSTR("cid")); // 存入cid
objc_release(v15);
// 4. 存入t:生成毫秒级时间戳转字符串
v16 = objc_retainAutoreleasedReturnValue(+[NSDate date](&OBJC_CLASS___NSDate, "date"));
-[NSDate timeIntervalSince1970](v16, "timeIntervalSince1970");
v18 = (__int64)(v17 * 1000.0);
objc_release(v16);
v19 = objc_retainAutoreleasedReturnValue(+[NSString stringWithFormat:](&OBJC_CLASS___NSString, "stringWithFormat:", CFSTR("%ld"), v18));
objc_msgSend(v8, "setObject:forKeyedSubscript:", v19, CFSTR("t")); // 存入t
// 5. 存入q:处理后的q字符串
objc_msgSend(v8, "setObject:forKeyedSubscript:", v32, CFSTR("q")); // v32是处理后的q,存入v8
之后将获得的长字符串拼接上密钥
密钥为上文hook成功获得的
最好我们会获得一个长字符串结构为
因为顺序是字母表排序的c q t u 所以传入顺序是 u c t q但是实际是c q t u
cid=220101;q=这里是q;t=时间戳;uid=2d3f0f44-0594-465e-bdee-43309339eb7a1764740654537密钥
因为我们之前搞过小程序版本的就知道sign是用了一种特殊的md5表现形式 函数如下
def string_to_md5_int_list(input_str):
"""
将输入字符串的MD5哈希值转换为包含4个整数的列表
参数:
input_str: 要计算MD5的字符串
返回:
包含4个整数的列表,对应MD5哈希的四个32位块
"""
# 计算字符串的MD5哈希值(128位)
md5_hash = hashlib.md5(input_str.encode('utf-8')).digest()
print(md5_hash)
int_tuple = struct.unpack('>iiii', md5_hash)
# 转换为列表并返回
return list(int_tuple)
验证一下 完全一致


总结
我之前做过小程序版本,因此省略了q和sign的反汇编加密函数分析部分。
有兴趣的朋友可以自行研究,直接hook md5和CCCrypt是无效的,因为这些方法经过了额外处理。若想深入逆向,还需进一步分析加密逻辑。
我之前做过小程序版本,因此省略了q和sign的反汇编加密函数分析部分。
有兴趣的朋友可以自行研究,直接hook md5和CCCrypt是无效的,因为这些方法经过了额外处理。若想深入逆向,还需进一步分析加密逻辑。
1万+

被折叠的 条评论
为什么被折叠?



