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

本来想着实现一下这个漫画 app 的模拟登录,结果抓个包一看:

Pastedimage20250712224444.png


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

Pastedimage20250712225929.png


进入 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()

成功实现登录:

Pastedimage20250713001057.png