mmmhua的模拟登录和部分通信协议分析

本文不构成任何技术操作指导或建议,仅供技术交流与学习参考。如发现本文内容存在可能违反法律规定的情况,请立即联系本人删除相关内容。(已对图片中出现的包名和应用名进行处理)

本来想着实现一下这个漫画app的模拟登录,结果抓个包一看:
[Pastedimage20250712224444.png]
data字段被加密,我先猜的Base64,但实际上没那么简单。
[Pastedimage20250712225929.png]
进入jadx看看LoginActivity:

手机验证登录部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
case R.id.login_button /* 2131232486 */: 
if (!this.cbServiceSelect.isChecked()) {
ToastUtils.show("请勾选并同意《隐私政策》和《服务协议》");
return;
} else if (l(this.loginPhoneEdit.getText().toString().trim())) { // 获取输入的电话
if (this.loginCodeEdit.getText().toString().trim().length() != 0) { // 验证码不为空
showDialog();
ApiParams apiParams = new ApiParams();
apiParams.with(Constants.RequestAction.getUser()); // return "user/login";
apiParams.with("mobile", this.loginPhoneEdit.getText().toString().trim()); // 追加电话号码
apiParams.with(PluginConstants.KEY_ERROR_CODE, this.loginCodeEdit.getText().toString().trim()); // 追加验证码
ServiceProvider.postAsyn(this, this.q, apiParams, UserBean.class, this);
return;
}
showToast(getResources().getString(R.string._please_input_validate_code));
return;
} else {
return;
}

postAysn->post->.....->DataRequestWrapper->p->n->b->converTobase64()->Base64.encode()
这不仅是登录的调用栈,这个app的所有网络请求都是经过postAysn->p的过程。p是一个调度函数,根据功能的不同,实现不同的请求。

继续分析这个登录请求,在方法b中,有个方法j,拼成最终的data,然后经由方法b进行加密。

我在多次抓取请求的过程中,发现token的一些使用规则:

  • 存储在运行时文件token_sp.xml中
  • 由服务端生成,每次登陆时服务端会发送本次登录的会话token
  • 如果是在本设备上首次登录,可以使用默认token:”manmanDefaultToken”
  • 如果之前在本设备上退出登录,退出登录时服务端也会发送一个token,供下次登录使用

由于抓包得到的内容不可读,需要hook一些方法和函数来验证:
这是脚本:

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
Java.perform(function () {

let DataRequestWrapper = Java.use("com.itcode.reader.datarequest.neworkWrapper.DataRequestWrapper");
DataRequestWrapper.p.implementation = function (builder) {
console.log("==== DataRequestWrapper.p() 被调用 ====");
// console.log("builder param map: " + builder.f);
// console.log("builder type: " + builder.h);
// console.log("builder tag: " + builder.k);
let result = this.p.call(this, builder);
// console.log("p() 返回的 Request: " + result);
// console.log("Request URL: " + result.url());
// console.log("Request Headers: " + result.headers());

let body=result.body();
if (body != null) {
let Buffer = Java.use("okio.Buffer");
let buffer = Buffer.$new();
body.writeTo(buffer);
console.log("Request Body: " + buffer.readUtf8());
}
return result;
};
// DataRequestWrapper.n.implementation = function (str, bArr, obj) {
// console.log(`DataRequestWrapper.n is called: bArr=${bArr},\n, obj=${obj}`);
// let result = this["n"](str, bArr, obj);
// console.log(`DataRequestWrapper.n result=${result}`);
// return result;
// };

let JSONTools = Java.use("com.itcode.reader.datarequest.tool.JSONTools");
JSONTools["parseMapToJson"].implementation = function (map) {
let result = this["parseMapToJson"](map);
console.log(`JSONTools.parseMapToJson result=${result}`);
return result;
}
DataRequestWrapper["convertToBase64"].implementation = function (iArr) {
console.log(`DataRequestWrapper.convertToBase64 is called: iArr=${iArr}`);
let result = this["convertToBase64"](iArr);
console.log(` 结果字符串原始=${result}`);
const resultJs = Array.from(result); // 确保结果是JS数组
const resultStr = resultJs.map(code => String.fromCharCode(code)).join('');
console.log(` 结果字符串处理后=${resultStr}`);
return result;
};
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Java.perform(function() {
//验证函数n就是通信协议的最后一个函数
let DataRequestWrapper = Java.use("com.itcode.reader.datarequest.neworkWrapper.DataRequestWrapper");
DataRequestWrapper["n"].implementation = function (str, bArr, obj) {
console.log(`DataRequestWrapper.n is called: str=${str}, bArr=${bArr}, obj=${obj}`);
let result = this["n"](str, bArr, obj);
console.log(`DataRequestWrapper.n result=${result}`);
return result;
};
//查看验证函数j的逻辑,每次登陆产生的token不一样,首次使用app可用默认token:“manmanDefaultToken”
let JSONTools = Java.use("com.itcode.reader.datarequest.tool.JSONTools");
JSONTools["parseMapToJson"].implementation = function (map) {
let result = this["parseMapToJson"](map);
console.log(`JSONTools.parseMapToJson result=${result}`);
return result;
};
})

最后,实现模拟登录:

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
import json  
import time
import base64
import requests

class Login():
def __init__(self,encode_pattern,mobile):
self.url="https://xxxxxxxxxxxxxxxxxx"
self.headers = {
'Content-Type': 'raw',
'Accept-Encoding': 'gzip',
'User-Agent': 'okhttp/3.14.9',
'Host': 'api.tmanga.com'
}
self.token=''
self.encode_pattern=encode_pattern
self.mobile=mobile

def get_appversion_info(self):
return "5.2.51^ONEPLUS A6010^android^android^9^oppo^373899aa3f886e93^98:09:CF:0C:D3:6D^869386043396293^202411010^^^^"

def get_token(self):
#每次登录都有一个属于本次登陆的一个token,如果之前在本设备上退出了,设备会保留退出时服务器发送的token,如果没有这个token,获取验证码时的请求就会使用默认token

return "manmanDefaultToken"

def get_precom(self):
return 1

def fluter_and_add(self,param):
param.pop("wkParams",None)
#由于对于同一个设备的同一个用户而言,info恒定,这里就用抓到的信息,后续需要可以补充get_appversion_inf()
param["p_recom"]=self.get_precom()
param["info"]=self.get_appversion_info()
param["token"]=self.get_token()
return json.dumps(param,ensure_ascii=False)

def log_encode(self,map,default):
log_json=self.fluter_and_add(map)
print(log_json)
if default==True:
times=int(time.time())
if times<1000000000:
times=1000000000
sb=str(log_json)+'3'+str(times)
else:
times=int(time.time())+1
if times<1000000000:
times=1000000000
sb=str(log_json)+str(times)

bytes_data = sb.encode('utf-8')
length = len(bytes_data)

c_arr = []
for byte in bytes_data:
c_arr.append(chr(byte & 255))

i_arr = [0] * length
i3 = 0
i4 = 1

for i5 in range(length):
i6 = i5 % 5
if i6 == 0:
if i3 % 2 == 0:
i4 = 1
else:
i4 = -1
i3 += 1

c = c_arr[i5]
if ord(c) >= 127:
i_arr[i5] = ord(c) - ord('}')
else:
i_arr[i5] = ord(c) + ord('n') + ((i6 + 1) * i4)

#coverTobase64函数部分
b_arr = bytearray((x & 0xFF) for x in i_arr)
encoded_bytes = base64.b64encode(b_arr)

return encoded_bytes

def phone_login(self):
params={
"mobile":self.mobile,
"code":'',
"api":"user/login"
}
params_code={
"mobile":self.mobile,
"api":"verify-code/send"
}

code_data=self.log_encode(params_code,self.encode_pattern)
print(code_data)
response0 = requests.post(self.url, data=code_data, headers=self.headers)
print(response0.text)


params["code"]=input("输入验证码: \n")
log_data=self.log_encode(params,self.encode_pattern)
print(log_data)
response = requests.post(self.url, data=log_data, headers=self.headers)
print(response.text)
self.token=response.json().get("data").get("token")
print(self.token)

if __name__=="__main__":

phone_login=Login(mobile=xxxxxxxxxxx,encode_pattern=True)
phone_login.phone_login()

成功实现登录:
[Pastedimage20250713001057.png]