本文不构成任何技术操作指导或建议,仅供技术交流与学习参考。如发现本文内容存在可能违反法律规定的情况,请立即联系本人删除相关内容。(已对图片中出现的包名和应用名进行处理)
本来想着实现一下这个漫画 app 的模拟登录,结果抓个包一看:

data 字段被加密,我先猜的 Base64,但实际上没那么简单。

进入 jadx 看看 LoginActivity:
手机验证登录部分:
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 一些方法和函数来验证:
这是脚本:
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;
};
});
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;
};
})
最后,实现模拟登录:
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()
成功实现登录:
