分析某听书APP登录协议
注意
- 此文仅限于技术交流,请不要做违法的事情。对于那些居心叵测的人根据此文造成违法的事情与本人无关。此文章不得转载!!!
- 关键图片信息已打码,关键代码信息已用XXOO替代。如果APP方需要删除,请发邮件
xxoo@hotmail.com
,谢谢。 - 这并不是基础教程,需要一定iOS逆向经验的人才能看懂。
前序
工具
- SSL Kill Switch 2 抓取https包
- Reveal 查看APP视图结构
- class-dump 导出头文件
- IDA 反汇编Mach-O文件
- Charles APP抓包
- Frida Hook框架
逆向VIP过程
- 这年头看见VIP就手痒,如图点击带有VIP标识的栏目会弹出如图所示,然后逆向就开始了
- 使用Reveal查看发现列表控制器是
LMBookSectionListVC
,点击的Cell是LMSectionlistCell
,根据经验就知道当执行didSelectRowAtIndexPath
时触发的弹框。 - 使用IDA反汇编Mach-O文件,然后找到
LMBookSectionListVC tableView:didSelectRowAtIndexPath:
,Fn + F5
一下可以看到伪代码,大体如下1
2
3
4
5
6
7
8
9
10
11-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
NSArray *bookSections = [[self ownerVM] bookSections];
LMSection *section = [bookSections safeObjectAtIndex: indexPath.row];
if ([self isPlayerPlayingWithSection: section]) { // 如果正在播放,就继续不播放
return
}
// 如果没有播放就判断能不能播放
if ([section hasBuy] == 1 || [section downloadStatus] == 4 || ([section strategy] & 0x10)) { // 满足其中一个就播放
[self playWithSection: section];
}
} - 所以hook代码如下。插件安装完成后没有了上面的弹框,直接跳转到播放页面,但是给了一个播放错误的提示
收费章节未购买
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15%hook LMSection
- (long long)hasBuy {
return 1;
}
- (long long)strategy {
return 16;
}
- (long long)downloadStatus {
return 4;
}
%end - 通过Charles抓包。猜测是通过token判断是不是VIP,如果不是,list列表为空,就没有播放链接,这下就歇菜了。
- 进入播放页面请求:https://XXOO.info/yyting/gateway/entityPath.action(此处省略很多)
- 进入播放页面响应:
1
2
3
4
5{
"list": null,
"msg": "收费章节未购买!",
"status": 2
}
- VIP没有搞掉不甘心,总想搞事情,就逆向登录过程吧。
分析登录协议
请求参数
- 到登录界面,输入用户名和密码,点击登录。通过Charles抓包如图
- 上图可知请求分为三部分:
- BaseURL:
https://XXOO.info/XXOO/ClientLogon.action
- Query String:
imei
和token
- Form:
account
,pwd
,nwt
,q
,sc
- BaseURL:
- Query String拼接到BaseURL后面,Form就是Body内容
寻找传参函数
- 到登录界面,使用Reveal发现登录按钮属于
LMLoginViewController
- 终端执行
frida-trace -U -m "-[LMLoginViewController *]" 某听书APP
命令,点击登录按钮,发现执行clickLoginButton:
方法1
2
3
4/* TID 0x403 */
...省略很多
1146 ms -[LMLoginViewController clickLoginButton:<LMOrangeButton: 0x115d31fe0; baseClass = UIButton; frame = (38 277; 299 44); clipsToBounds = YES; opaque = NO; layer = <CALayer: 0x1c442dac0>>]
...省略很多 - 所以接下来在IDA中查看
-[LMLoginViewController clickLoginButton:]
的实现,核心代码概括如下:1
2
3-[LMLoginViewController clickLoginButton:] {
[[LMDataManager shareDataManger] manualLoginWithAccount:pwd:success:failure:]
} - 接着查看
-[LMDataManager manualLoginWithAccount:pwd:success:failure:]
实现,核心代码概括如下,接下来要分析这三段代码。1
2
3
4
5- (void)manualLoginWithAccount:(NSString *)account pwd:(NSString *)pwd success:(id)successBlock failure:(id)failureBlock {
LMHTTPParam *params = [LMHTTPParam buildManualLoginParamDicWithAccount:account pwd:pwd];
LMLoginApi *request = [[LMLoginApi alloc] initWithRequestParams: params];
[request startRequestWithSuccessCompletion:successBlock failureCompletion:failureBlock];
} - IDA分析
+[LMHTTPParam buildManualLoginParamDicWithAccount:account pwd:pwd]
核心代码概括如下:1
2
3
4
5
6
7
8
9
10+ [LMHTTPParam buildManualLoginParamDicWithAccount:account pwd:pwd] {
NSString *md5Pwd = MD5DigestToNSString(pwd);
NSString *base64Pwd = +[LMGTMBase64 base64WithHexStr: md5Pwd] {
NSData *data = [%c(LMGTMBase64) encodeData:md5Pwd isChunked: NO];
NSString *base64Pwd = [[NSString alloc] initWithData:data encoding: NSUTF8StringEncoding];
return base64Pwd;
}
NSDictionary *params = @{@"account": account, @"pwd": base64Pwd};
return params
} - IDA分析
-[[LMLoginApi alloc] initWithRequestParams: params]
没有做什么特别操作 - IDA分析
-[request startRequestWithSuccessCompletion:successBlock failureCompletion:failureBlock]
,核心代码如下1
2
3
4
5
6
7
8
9
10- (void)startRequestWithSuccessCompletion:successBlock failureCompletion:failureBlock {
- [request startWithCompletionBlockWithSuccess:successBlock failure:failureBlock] {
- [YTKBaseRequest setCompletionBlockWithSuccess:failure:];
- [YTKBaseRequest start] {
- [[YTKNetworkAgent sharedAgent] addRequest: request] {
-[request buildCustomUrlRequest]
}
}
}
} - IDA分析
-[request buildCustomUrlRequest]
,代码很多很长,有四段代码需要注意。1
2
3
4v52 = objc_msgSend(v90, "multipartFormRequestWithMethod:URLString:parameters:constructingBodyWithBlock:error:",CFSTR("POST"),v50,v30,v32,&v95);
v73 = objc_msgSend(v90, "requestWithMethod:URLString:parameters:error:", CFSTR("POST"), v50, v30, &v94);
v86 = objc_msgSend(v90, "requestWithMethod:URLString:parameters:error:", CFSTR("GET"), v85, v30, &v93);
v70 = objc_msgSend(v90, "requestWithMethod:URLString:parameters:error:", CFSTR("HEAD"), v68, v30, &v92); - 我们进行的是POST请求,所以
v73 = objc_msgSend(v90, "requestWithMethod:URLString:parameters:error:", CFSTR("POST"), v50, v30, &v94);
我们需要分析 - 从上可知v50包含我们需要的Query String,v30包含我们需要的Form,也就是body。所以接下来就要对这两个参数进行追踪。
解析Query String
- 跟踪v50如下,发现v50来源于v46,v46是
+[NSURL URLWithString:relativeToURL:]
,那么我们其实要寻找v391
2
3
4
5
6
7v46 = objc_msgSend(&OBJC_CLASS___NSURL, "URLWithString:relativeToURL:", v39, v44);
v47 = (void *)objc_retainAutoreleasedReturnValue(v46);
objc_release(v45);
objc_release(v42);
v48 = objc_msgSend(v47, "absoluteString");
v49 = &v2->super;
v50 = objc_retainAutoreleasedReturnValue(v48); - 跟踪v39如下,IDA伪代码如下
1
2
3
4
5
6
7
8
9v33 = ((id (__cdecl *)(LMRequestAgent_meta *, SEL))objc_msgSend)(
(LMRequestAgent_meta *)&OBJC_CLASS___LMRequestAgent,
"shareRequestAgent");
v34 = (void *)objc_retainAutoreleasedReturnValue(v33);
v35 = ((id (__cdecl *)(YTKBaseRequest *, SEL))objc_msgSend)(&v2->super, "requestUrl");
v36 = objc_retainAutoreleasedReturnValue(v35);
v37 = v36;
v38 = objc_msgSend(v34, "addParamForPostRequestUrl:", v36);
v39 = objc_retainAutoreleasedReturnValue(v38); - 简化一下v39代码, 并且进入
addParamForPostRequestUrl
查看实现,可以发现取出token和imei进行拼接,拼接结果就是我们需要的Query String
,我们离目标越来越近。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19NSString *requestUrl = -[YTKBaseRequest requestUrl];
v39 = [[LMRequestAgent shareRequestAgent] addParamForPostRequestUrl: requestUrl] {
v6 = -[LMRequestAgent requestToken](v3, "requestToken");
v7 = (void *)objc_retainAutoreleasedReturnValue(v6);
v8 = -[LMRequestAgent requestImei](v3, "requestImei");
v9 = (void *)objc_retainAutoreleasedReturnValue(v8);
v10 = objc_msgSend(&OBJC_CLASS___NSMutableString, "string");
v11 = (void *)objc_retainAutoreleasedReturnValue(v10);
if ( v9 && objc_msgSend(v9, "length") )
objc_msgSend(v11, "appendFormat:", CFSTR("?imei=%@"), v9);
if ( v7 && objc_msgSend(v7, "length") )
{
if ( objc_msgSend(v11, "length") )
objc_msgSend(v11, "appendFormat:", CFSTR("&token=%@"), v7);
else
objc_msgSend(v11, "appendFormat:", CFSTR("token=%@"), v7);
}
objc_msgSend(v11, "insertString:atIndex:", v5, 0LL);
} -[LMRequestAgent requestToken]
实现如下:1
2
3-[LMRequestAgent requestToken] {
return [[NSUserDefaults standardUserDefaults] objectForKey: @"token"];
}-[LMRequestAgent requestImei]
实现如下1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21// 先从钥匙串里面取如果取到就直接返回。没有取到就生成一个,把新生成的存入钥匙串,并且返回。
-[LMRequestAgent requestImei] {
NSString *udid = +[LMUDIDTools UDID] {
v2 = +[LMUDIDTools getUDIDFromKeyChain]
v3 = +[LMUDIDTools _UDID_iOS7]
if ( !v2
|| (unsigned __int64)objc_msgSend(v2, "isEqualToString:", CFSTR("00000000-0000-0000-0000-000000000000")) & 1
|| !((unsigned __int64)objc_msgSend(v2, "isEqualToString:", v3) & 1) ) {
v2 = +[LMUDIDTools _UDID_iOS7](&OBJC_CLASS___LMUDIDTools, "_UDID_iOS7");
+[LMUDIDTools settUDIDToKeyChain:](&OBJC_CLASS___LMUDIDTools, "settUDIDToKeyChain:", v2);
}
return v2;
}
// 对UDID进行Base64,LMGTMBase64使用了开源库`GTMBase64`
return [udid BASE64Encode] {
NSData *uuidData = [uuid dataUsingEncoding: NSUTF8StringEncoding];
NSData *uuidBase64Data = [LMGTMBase64 encodeData: uuidData];
NSString *imei = [[NSString alloc] initWithData:uuidBase64Data encoding: NSUTF8StringEncoding];
return imei
};
}- 我们可以自己写一个
-[LMRequestAgent requestImei]
简单的实现1
2
3
4
5
6
7NSString *requestImei() {
NSString *uuid = [[NSUUID UUID] UUIDString];
NSData *uuidData = [uuid dataUsingEncoding: NSUTF8StringEncoding];
NSData *uuidBase64Data = [GTMBase64 encodeData: uuidData];
NSString *imei = [[NSString alloc] initWithData:uuidBase64Data encoding: NSUTF8StringEncoding];
return imei;
} - 到这里
Query String
的两个参数token
和imei
都解析完成
解析Form(Body)里面的参数
- 还记得最上面的
v73 = objc_msgSend(v90, "requestWithMethod:URLString:parameters:error:", CFSTR("POST"), v50, v30, &v94);
吧,v30就是body里面的参数。
我们需要追踪v30的来源。1
2
3
4
5
6
7
8
9v24 = ((id (__cdecl *)(LMRequestAgent_meta *, SEL))objc_msgSend)(
(LMRequestAgent_meta *)&OBJC_CLASS___LMRequestAgent,
"shareRequestAgent");
v25 = (void *)objc_retainAutoreleasedReturnValue(v24);
v26 = ((id (__cdecl *)(LMBaseRequest *, SEL))objc_msgSend)(v2, "requestArgument");
v27 = objc_retainAutoreleasedReturnValue(v26);
v28 = v27;
v29 = objc_msgSend(v25, "processRequest:withArgument:", v2, v27);
v30 = objc_retainAutoreleasedReturnValue(v29); - 简化一下代码就是
1
[[LMRequestAgent shareRequestAgent] processRequest:request withArgument: [request requestArgument]];
[LMRequestAgent processRequest:withArgument:]
实现代码很长,前面分析过Form里面需要的参数是account
,pwd
,nwt
,q
,sc
。- account和pwd我们已经解析出来了,下面要解析
nwt
,q
,sc
这三个。 - 多次点击登录按钮发现
nwt
的值一直为1
。 q
每点击一次就+1
,猜测q
指的是查询次数。sc
每点击一次值都不一样,它应该是一些可变参数的拼接
- account和pwd我们已经解析出来了,下面要解析
- 所以接下来的操作都是解析
sc
,大致浏览一遍,发现,根据上下文可以v8是NSMutableDictionary1
objc_msgSend(v8, "setObject:forKey:", v60, CFSTR("sc"));
- 跟踪一下
frida-trace -U -m "-[NSMutableDictionary setObject:forKey:]" 某听书APP
,点击登录按钮会出现很多的调用,需要我们过滤一下key。1
2
3
4onEnter: function (log, args, state) {
if (ObjC.Object(args[3]) != 'sc') return
log('-[__NSDictionaryM setObject:' + ObjC.Object(args[2]) + ' forKey:' + ObjC.Object(args[3]) + ']');
}, - 再次点击登录按钮发现确实执行了,这个值跟Chales里面看到的值是一样的。
1
2
3Started tracing 13 functions. Press Ctrl+C to stop.
/* TID 0x403 */
1203 ms -[__NSDictionaryM setObject:e7b06599148c4dab5daf45b526e69a4f forKey:sc] - 这个时候就需要追踪v60的来源。发现v60是
[v57 MD5DigestToNSString]
,也就是对v57进行md5得到v60。而v57又来自于v56。v56是拼接的字符串,拼接的是v55
和OoiwGXXOO!5kU#@ku0BWm
两个字符串。1
2
3
4
5
6
7
8
9
10
11v56 = objc_msgSend(
&OBJC_CLASS___NSString,
"stringWithFormat:",
CFSTR("%@%@"),
v55,
CFSTR("OoiwGXXOO!5kU#@ku0BWm"),
v83);
v57 = (void *)objc_retainAutoreleasedReturnValue(v56);
v58 = v57;
v59 = objc_msgSend(v57, "MD5DigestToNSString");
v60 = (void *)objc_retainAutoreleasedReturnValue(v59); - 接下来看看v55来自于v54,v54也是字符串的拼接。
1
2v54 = objc_msgSend(&OBJC_CLASS___NSString, "createURLStringWithPath:andParams:", v52, v8);
v55 = (void *)objc_retainAutoreleasedReturnValue(v54); - 跟踪一下
frida-trace -U -m "+[NSString createURLStringWithPath:andParams:]" 某听书APP
,执行后找到对应的js文件,更改如下:1
2
3
4
5
6onEnter: function (log, args, state) {
log('+[NSString createURLStringWithPath:' + ObjC.Object(args[2])+ ' andParams:' + ObjC.Object(args[3]) + ']');
},
onLeave: function (log, retval, state) {
log('函数返回值' + ObjC.Object(retval));
} - 终端再次执行上面的跟踪命令,点击登录按钮输出如下。
1
2
3
4
5
6
7
8
9+[NSString createURLStringWithPath:/XXOO/ClientLogon.action andParams:{
account = Tyyy;
imei = M0Q1MDZGMzAtRDREOS00OUMyLUE2MUUtRUM1RUYyMjJENDc1;
nwt = 1;
pwd = "0QkGw9rBFy1PYL1B8iSudQ==";
q = 411;
token = "wQYGmjeAR1zP1SZKF6GjFg**_u6nbPC95d-IUR7IALtZAUA**";
}]
1538 ms 函数返回值/XXOO/ClientLogon.action?account=Tyyy&imei=M0Q1MDZGMzAtRDREOS00OUMyLUE2MUUtRUM1RUYyMjJENDc1&nwt=1&pwd=0QkGw9rBFy1PYL1B8iSudQ==&q=411& token=wQYGmjeAR1zP1SZKF6GjFg**_u6nbPC95d-IUR7IALtZAUA** - 这下就跟明确了,所需要的值都已经求出了,只要拼接就可以得到相应的字符串
说在最后
- 最终代码如下
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
55NSString *getImei() {
NSString *uuid = [[NSUUID UUID] UUIDString];
NSData *uuidData = [uuid dataUsingEncoding: NSUTF8StringEncoding];
NSData *uuidBase64Data = [GTMBase64 encodeData: uuidData];
NSString *imei = [[NSString alloc] initWithData:uuidBase64Data encoding: NSUTF8StringEncoding];
return imei;
}
NSString *MD5DigestToNSString(NSString *str) {
unsigned char v10[16];
const char *v3 = [str UTF8String];
size_t v5 = strlen(v3);
CC_MD5(v3, v5, v10);
NSMutableString *v7 = ([[NSMutableString alloc] initWithCapacity: 32LL]);
long long v8 = 0LL;
do [v7 appendFormat:@"%02x", (unsigned char)v10[v8++]];
while ( v8 != 16 );
return v7;
}
NSString *baseURL = @"https://XXOO.info";
NSString *relativeURL = @"/XXOO/ClientLogon.action?";
// 获取Imei
NSString *imei = getImei();
// 获取token
NSString *token = [[NSUserDefaults standardUserDefaults] objectForKey: @"token"];
// 查询字符串
NSString *queryStr = [NSString stringWithFormat:@"imei=%@&token=%@",imei, token];
NSString *account = @"YourUserName";
NSString *pwdText = @"YourPassword";
NSString *pwdMd5 = MD5DigestToNSString(pwdText);
NSData *pwdData = [%c(LMGTMBase64) encodeData: pwdMd5 isChunked: NO];
NSString *pwdBase64 = [[NSString alloc] initWithData:pwdData encoding: NSUTF8StringEncoding];
// 查询次数
NSString *q = @"1";
NSString *nwt = @"1";
// 可以看成是加盐字符串
NSString *salt = @"OoiwGXXOO!5kU#@ku0BWm";
// 生成sc
NSString *scStr = [NSString stringWithFormat:@"%@account=%@&imei=%@&nwt=%@&pwd=%@&q=%@&token=%@%@",
relativeURL, account,imei,nwt,pwdBase64,q,token,salt];
NSString *scMd5 = MD5DigestToNSString(scStr);
// 执行请求
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init];
request.URL = [NSURL URLWithString: [NSString stringWithFormat:@"%@%@%@",baseURL,relativeURL,queryStr]];
request.HTTPMethod = @"POST";
NSString *params = [NSString stringWithFormat:@"account=%@&nwt=%@&pwd=%@&q=%@&sc=%@",
account,nwt,pwdBase64,q,scMd5];
NSData *data = [params dataUsingEncoding: NSUTF8StringEncoding];
request.HTTPBody = data;
NSURLSessionDataTask *dataTask = [[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
id obj =[NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
NSLog(@"iOSRE:数据为%@",obj);
}];
[dataTask resume]; - 以上代码有两个地方还没有解析。
[%c(LMGTMBase64) encodeData: pwdMd5 isChunked: NO];
这个不是开源库GTMBase64
自带的方法,应该是自己封装的,伪代码显示还挺复杂的,以后有空在搞[[NSUserDefaults standardUserDefaults] objectForKey: @"token"]
,这个应该是从服务端拉取存入本地,有空在搞