吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 1427|回复: 17
收起左侧

[Android 原创] 分析某库登录逻辑以及协议复现

  [复制链接]
yyyyyyzh 发表于 2025-3-15 16:56
工具准备:Pixel XL       // 该软件不支持模拟器                                  雷电APP(脱壳用,不会脱壳,哭)                                  frIDA


    本贴仅作技术交流,如有侵权请在联系我立即删帖



符合360加固的特征,用雷电脱个壳,扔jadx编译一下
image-20250304174645502.png


尝试一下抓包,抓到两个数据包
image-20250313194656732.png




只有下面这一个数据包和登录有关
[Asm] 纯文本查看 复制代码
01
02
03
04
05
06
07
08
09
10
11
12
POST /login_jsonp_active.do HTTP/1.1
common: {"uniqueCode":"dd67d0c0-01e5-492d-8b3b-c35c5fa6ba48","appId":"com.zcool.community","channel":"zcool","mobileType":"android","versionCode":4638}
BaseInfo: {"uniqueCode":"dd67d0c0-01e5-492d-8b3b-c35c5fa6ba48","appId":"com.zcool.community","channel":"zcool","mobileType":"android","versionCode":4638}
Content-Type: application/x-www-form-urlencoded
Content-Length: 624
Host: passport.zcool.com.cn
Connection: Keep-Alive
Accept-Encoding: gzip
Cookie: HWWAFSESID=a3b2973ef5454da521; HWWAFSESTIME=1741081260467
User-Agent: okhttp/3.12.0
 
app=android&key=dTdlMmtGQjZIQk45NEN3dTIzamdVb2xYMVp5dDZnYmgrMGRlK2xjbkpwZjJpWGczb1FhYWxUVUx1%250AaVJ1d01ZeG5mZUpXZzUvT0M0WQpuYnF1NG0wdjZEd0NaSldPMkZGQ1loRmZ5NG1PM1dTMmhGd3d0%250AbVNhaG9vMVRBbitzQWRrZVBubWZxNGt6ajIyUHJpdEVxUGhHM0tKCmMzWmExU2FXWDJ0VjA0S0NE%250AL3owNmNOUEUrMjRwcExDR0VqSVViU2RyVDU2a0RmS0dqOFhKYmNyYjljM0lqbE9IWm5Rajl5UmM1%250AdnMKTDlFc2xLaVJNRU5rUVJ6RnVaN1k0OVBPTklkeVpsYUJwaG5UTlpkTy9DcmJDcmhvTjRZUUM2%250AcjZiSU4xcTdieHlIOTBxNjYyT0RCNwpNZVhzQVcwbjI3eEtUTHVDWmxhQnBoblROWmRPL0NyYkNy%250AaG9Od0dTRVo1aGxrNlAzMHpYOFFIaTJRZEhjM2JBQy9ESC9iQXBidko1CnUybDluN3FOYkFaT1ZO%250AajNaYnlHSjJrVmdBPT0KP2tleUlkPTE%253D%250A


看那么长的数据先从 hashmap下手,hook hashmap,因为是登录逻辑,可以打印一下username或者password的字段堆栈
[JavaScript] 纯文本查看 复制代码
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Java.perform(function() {
    function showStacks() {
        console.log(
            Java.use("android.util.Log")
               .getStackTraceString(
                    Java.use("java.lang.Throwable").$new()
                )
        );
    }
 
     
    var HashMap = Java.use('java.util.HashMap');
    HashMap.put.implementation = function(a, b) {
     
        // 打印username的堆栈信息
        if (a.equals("username")) {
            showStacks();
            console.log("hashMap.put :", a, b);
        }
     
        console.log("put: ", a, b);
        return this.put(a, b);
    }
}


image-20250313200624941.png

image-20250313201530543.png

根据这个堆栈信息去寻找登录加密函数,把没用的东西去掉,看看这几个函数
[Asm] 纯文本查看 复制代码
1
2
3
4
5
6
7
8
java.lang.Throwable
    at com.zcool.community.data.api.PassportApi.signIn(PassportApi.java:138)
    at com.zcool.community.module.session.pwdsignin.PwdSigninViewProxy.signin(PwdSigninViewProxy.java:61)
    at com.zcool.community.module.session.pwdsignin.PwdSigninViewFragment$Content.onSubmitClick(PwdSigninViewFragment.java:189)
    at com.zcool.community.module.session.pwdsignin.PwdSigninViewFragment$Content.access$000(PwdSigninViewFragment.java:83)
    at com.zcool.community.module.session.pwdsignin.PwdSigninViewFragment$Content$1.onClick(PwdSigninViewFragment.java:136)
    at com.zcool.inkstone.util.ViewUtil.lambda$onClick$0(ViewUtil.java:46)
    at com.zcool.inkstone.util.-$$Lambda$ViewUtil$AQ3e0vrll-kI-Unlrb6ud-SUkjg.accept(Unknown Source:4)


搜一下 login_jsonp 这个地址先
image-20250313194913280.png


进去是个接口,定义了一个 signIn 方法
image-20250313202739009.png


再去寻找一下刚刚堆栈的第一个方法,恰好也是 sigin,使用的就是上图接口
image-20250313202831858.png


稍微分析了一下 str 是用户名 str2 是用户密码,这个 str3 是处理第三方登录的如qq,微信,测试用的账号密码登录,不管这个从 str2 跳出去找到 PASSWORD_KEY = “password”,石锤了是密码字段,可能为了防止搜明文搜出来吧 image-20250313203346783.png
然后就是调用方法 buildAndSetKeyParams 进行数据加密,加密完成时候,发起网络请求
[Java] 纯文本查看 复制代码
01
02
03
04
05
06
07
08
09
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
Map<String, String> createBaseParams = createBaseParams();
buildAndSetKeyParams(createBaseParams, createBaseKeyParams);
createBaseParams.put(NotificationCompat.CATEGORY_SERVICE, "https://www.zcool.com.cn");
createBaseParams.put("appLogin", "https://www.zcool.com.cn/tologin.do");
return this.mApiInterface.onKeyLoginWithToken(createBaseParams, createBaseHeaders()).map(new Function<NetSignInInfo, TrustedResponse<TrustedSignInInfo>>() { // from class: com.zcool.community.data.api.PassportApi.2
    /* JADX WARN: Type inference failed for: r4v1, types: [T, com.zcool.community.data.api.entity.trusted.TrustedSignInInfo] */
    [url=home.php?mod=space&uid=1892347]@Override[/url] // io.reactivex.functions.Function
    public TrustedResponse<TrustedSignInInfo> apply(@io.reactivex.annotations.NonNull NetSignInInfo netSignInInfo) throws Exception {
        com.zcool.community.data.api.entity.net.NetResponse netResponse = new com.zcool.community.data.api.entity.net.NetResponse();
        netResponse.data = netSignInInfo.toTrustedSignInInfo();
        if (((TrustedSignInInfo) netResponse.data).result) {
            netResponse.code = 0;
        } else {
            netResponse.code = -1;
        }
        Log.d("TAG", "response.code:" + netResponse.code);
        netResponse.msg = ((TrustedSignInInfo) netResponse.data).msg;
        return netResponse.toTrustedResponse(new Converter<TrustedSignInInfo, TrustedSignInInfo>() { // from class: com.zcool.community.data.api.PassportApi.2.1
            @Override // com.zcool.community.lang.Converter
            public TrustedSignInInfo convert(TrustedSignInInfo trustedSignInInfo) {
                return trustedSignInInfo;
            }
        });
    }
}).map(new Function<TrustedResponse<TrustedSignInInfo>, TrustedResponse<TrustedSignInInfo>>() { // from class: com.zcool.community.data.api.PassportApi.1
    @Override // io.reactivex.functions.Function
    public TrustedResponse<TrustedSignInInfo> apply(@io.reactivex.annotations.NonNull TrustedResponse<TrustedSignInInfo> trustedResponse) throws Exception {
        if (trustedResponse.code == 0 && trustedResponse.data.userId > 0) {
            if (!TextUtils.isEmpty(trustedResponse.data.SERVER_COOKIE_V1)) {
                CookiesHelper.addPassportServerCookieV1(trustedResponse.data.SERVER_COOKIE_V1);
            } else {
                Timber.e("sign in success, but cookie not found", new Object[0]);
                new IllegalAccessError("SERVER_COOKIE_V1 not found").printStackTrace();
            }
        }
        return trustedResponse;
    }
});

进入buildAndSetKeyParams 函数,这个是继承父类的方法,父类方法中只有一句
[Asm] 纯文本查看 复制代码
1
map.put("key",EncryptManager.getInstance().encrypt(map2));
在进入找到 EncryptManager.getInstance().encrypt(map2) ,根据最下面的图 EncryptManager.getInstance() 等效于 LazyInstance.access$100(); 等效于 LazyInstance.get() 返回结果为  private static final EncryptManager instance = new EncryptManager() 这个名为 instance 的 EncryptManager 类 image-20250313204911515.png
绕了一圈,创建了一个EncryptManager 类 调用这个类的 encrypt 方法,传入map2,map是空的,map2包含用户的各种信息。hook一下这个方法,查看出入值,返回值和抓包的值是否有一致的部分
[JavaScript] 纯文本查看 复制代码
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
Java.perform(function () {
    function showStacks() {
        console.log(
            Java.use("android.util.Log")
                .getStackTraceString(
                    Java.use("java.lang.Throwable").$new()
                )
        );
    }
 
    var EncryptManager = Java.use("com.zcool.community.data.api.encrypt.EncryptManager");
    EncryptManager["encrypt"].implementation = function (map) {
        console.log(`EncryptManager.encrypt is called: map=${map}`);
        let result = this["encrypt"](map);
        console.log(`EncryptManager.encrypt result=${result}`);
        return result;
    };
});
image-20250313210119315.png
处理一下着两份数据,不能说大差不差,只能说完全一样,只有间隔符不一样返回的数值的间隔符是 %0A ,抓包的是 %250A,而发包是要使用url编码,%25在url编码中就是%这些东西将密文分成了 76 字符一组。
[Asm] 纯文本查看 复制代码
01
02
03
04
05
06
07
08
09
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
result=
dTdlMmtGQjZIQk45NEN3dTIzamdVb2xYMVp5dDZnYmgrMGRlK2xjbkpwZjJpWGczb1FhYWxUVUx1
%0A
aVJ1d01ZeG5mZUpXZzUvT0M0WQpuYnF1NG0wdjZEd0NaSldPMkZGQ1loRmZ5NG1PM1dTMmhGd3d0
%0A
bVNhaG9vMVRBbitzQWRrZVBubWZxNGt6ajIyUHJpdEVxUGhHM0tKCmMzWmExU2FXWDJ0VjA0S0NE
%0A
L3owNmNOUEUrMjRwcExDR0VqSVViU2RyVDU2a0RmS0dqOFhKYmNyYjljM0lqbE9IWm5Rajl5UmM1
%0A
dnMKTDlFc2xLaVJNRU5rUVJ6RnVaN1k0OVBPTklkeVpsYUJwaG5UTlpkTy9DcmJDcmhvTjRZUUM2
%0A
cjZiSU4xcTdieHlIOTBxNjYyT0RCNwpNZVhzQVcwbjI3eEtUTHVDWmxhQnBoblROWmRPL0NyYkNy
%0A
aG9Od0dTRVo1aGxrNlAzMHpYOFFIaTJRZEhjM2JBQy9ESC9iQXBidko1CnUybDluN3FOYkFaT1ZO
%0A
ajNaYnlHSjJrVmdBPT0KP2tleUlkPTE
%3D
%0A
 
app=android&key=
dTdlMmtGQjZIQk45NEN3dTIzamdVb2xYMVp5dDZnYmgrMGRlK2xjbkpwZjJpWGczb1FhYWxUVUx1
%250A
aVJ1d01ZeG5mZUpXZzUvT0M0WQpuYnF1NG0wdjZEd0NaSldPMkZGQ1loRmZ5NG1PM1dTMmhGd3d0
%250A
bVNhaG9vMVRBbitzQWRrZVBubWZxNGt6ajIyUHJpdEVxUGhHM0tKCmMzWmExU2FXWDJ0VjA0S0NE
%250A
L3owNmNOUEUrMjRwcExDR0VqSVViU2RyVDU2a0RmS0dqOFhKYmNyYjljM0lqbE9IWm5Rajl5UmM1
%250A
dnMKTDlFc2xLaVJNRU5rUVJ6RnVaN1k0OVBPTklkeVpsYUJwaG5UTlpkTy9DcmJDcmhvTjRZUUM2
%250A
cjZiSU4xcTdieHlIOTBxNjYyT0RCNwpNZVhzQVcwbjI3eEtUTHVDWmxhQnBoblROWmRPL0NyYkNy
%250A
aG9Od0dTRVo1aGxrNlAzMHpYOFFIaTJRZEhjM2JBQy9ESC9iQXBidko1CnUybDluN3FOYkFaT1ZO
%250A
ajNaYnlHSjJrVmdBPT0KP2tleUlkPTE
%253D
%250A

encrypt就是一个关键方法了
[Java] 纯文本查看 复制代码
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
private EncryptManager() {
    KEYS.put("1", "F#C@5IOBULR9L415C~ZX*97C");
    KEYS.put("5", "DB&T78AQF&W7T#@~LGP9YC~T");
    KEYS.put(Constants.VIA_REPORT_TYPE_SHARE_TO_QQ, "3D4BT10H4#DUQLXHJ*WLLN&B");
}
 
public String encrypt(Map map) {
    StringBuffer stringBuffer = new StringBuffer();
    // json序列化,将map数据转换成json字符串
    String json = new Gson().toJson(map);
    // 使用DESede加密json
    // DESede又叫3DES,密钥24位,encrypt函数上方就是明文的密钥
    stringBuffer.append(DESedeCoder.encode(json, KEYS.get("1")));
    // 拼接keyID参数
    stringBuffer.append("?keyId=");
    stringBuffer.append("1");
    String stringBuffer2 = stringBuffer.toString();
    try {
        // URL编码
        // encryptBASE64(stringBuffer2.getBytes("UTF-8")),将加密结果从byte类型转换成可传输的ASCII字符串
        String encode = URLEncoder.encode(encryptBASE64(stringBuffer2.getBytes("UTF-8")), "UTF-8");
        Timber.v("encrypt %s->%s->%s", json, stringBuffer2, encode);
        return encode;
    } catch (UnsupportedEncodingException e) {
        e.printStackTrace();
        return null;
    }
}

既然进行了url编码,那打印出来的 %0A 就需要还原成换行符了,%3D 解码成 = 经过3DES加密之后的结果就是
[Asm] 纯文本查看 复制代码
1
2
3
4
5
6
7
8
dTdlMmtGQjZIQk45NEN3dTIzamdVb2xYMVp5dDZnYmgrMGRlK2xjbkpwZjJpWGczb1FhYWxUVUx1
aVJ1d01ZeG5mZUpXZzUvT0M0WQpuYnF1NG0wdjZEd0NaSldPMkZGQ1loRmZ5NG1PM1dTMmhGd3d0
bVNhaG9vMVRBbitzQWRrZVBubWZxNGt6ajIyUHJpdEVxUGhHM0tKCmMzWmExU2FXWDJ0VjA0S0NE
L3owNmNOUEUrMjRwcExDR0VqSVViU2RyVDU2a0RmS0dqOFhKYmNyYjljM0lqbE9IWm5Rajl5UmM1
dnMKTDlFc2xLaVJNRU5rUVJ6RnVaN1k0OVBPTklkeVpsYUJwaG5UTlpkTy9DcmJDcmhvTjRZUUM2
cjZiSU4xcTdieHlIOTBxNjYyT0RCNwpNZVhzQVcwbjI3eEtUTHVDWmxhQnBoblROWmRPL0NyYkNy
aG9Od0dTRVo1aGxrNlAzMHpYOFFIaTJRZEhjM2JBQy9ESC9iQXBidko1CnUybDluN3FOYkFaT1ZO
ajNaYnlHSjJrVmdBPT0KP2tleUlkPTE=

ECB的填充模式,没有iv值,先将传入的字符串转换成byte的形式,再将传入的keyID=1作为密钥,看起来这个是一个标准的DESede/ECB加密 image-20250314084208467.png
将原数据base64解码,去除掉拼接的 ?keyId=1, 得到密文,尝试DESede解码
[Asm] 纯文本查看 复制代码
1
2
3
4
5
6
7
u7e2kFB6HBN94Cwu23jgUolX1Zyt6gbh+0de+lcnJpf2iXg3oQaalTULuiRuwMYxnfeJWg5/OC4Y
nbqu4m0v6DwCZJWO2FFCYhFfy4mO3WS2hFwwtmSahoo1TAn+sAdkePnmfq4kzj22PritEqPhG3KJ
c3Za1SaWX2tV04KCD/z06cNPE+24ppLCGEjIUbSdrT56kDfKGj8XJbcrb9c3IjlOHZnQj9yRc5vs
L9EslKiRMENkQRzFuZ7Y49PONIdyZlaBphnTNZdO/CrbCrhoN4YQC6r6bIN1q7bxyH90q662ODB7
MeXsAW0n27xKTLuCZlaBphnTNZdO/CrbCrhoNwGSEZ5hlk6P30zX8QHi2QdHc3bAC/DH/bApbvJ5
u2l9n7qNbAZOVNj3ZbyGJ2kVgA==
?keyId=1

image-20250314090130128.png
解密脚本
[JavaScript] 纯文本查看 复制代码
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
const CryptoJS = require('crypto-js');
key = "F#C@5IOBULR9L415C~ZX*97C"
data = "u7e2kFB6HBN94Cwu23jgUolX1Zyt6gbh+0de+lcnJpf2iXg3oQaalTULuiRuwMYxnfeJWg5/OC4Ynbqu4m0v6DwCZJWO2FFCYhFfy4mO3WS2hFwwtmSahoo1TAn+sAdkePnmfq4kzj22PritEqPhG3KJc3Za1SaWX2tV04KCD/z06cNPE+24ppLCGEjIUbSdrT56kDfKGj8XJbcrb9c3IjlOHZnQj9yRc5vsL9EslKiRMENkQRzFuZ7Y49PONIdyZlaBphnTNZdO/CrbCrhoN4YQC6r6bIN1q7bxyH90q662ODB7MeXsAW0n27xKTLuCZlaBphnTNZdO/CrbCrhoNwGSEZ5hlk6P30zX8QHi2QdHc3bAC/DH/bApbvJ5u2l9n7qNbAZOVNj3ZbyGJ2kVgA=="
 
 
function decodeDESede(data, key) {
 
    var _key =  CryptoJS.enc.Utf8.parse(key);
    var decrypted = CryptoJS.TripleDES.decrypt(data, _key, {
        mode: CryptoJS.mode.ECB,
        padding: CryptoJS.pad.Pkcs7
    }).toString(CryptoJS.enc.Utf8);
 
    return decrypted;
}
 
console.log(decodeDESede(data, key))

image-20250314095718537.png
解密后的数据
[Asm] 纯文本查看 复制代码
1
2
3
4
5
6
7
解密后的数据:
{"password":"12345678",
"common":{"uniqueCode":"dd67d0c0-01e5-492d-8b3b-c35c5fa6ba48","appId":"com.zcool.community","channel":"zcool","mobileType":"android","versionCode":4638},
"appLogin":"https://www.zcool.com.cn/tologin.do",
"service":"https://www.zcool.com.cn",
"appId":"1006",
"username":"13112345678"}

再回头看put数据的方法,这里put了五项,但是map数值不是new的,说明 common字段以及提前构建好了
[Java] 纯文本查看 复制代码
01
02
03
04
05
06
07
08
09
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
public Single<TrustedResponse<TrustedSignInInfo>> signIn(String str, String str2, String str3, int i) {
        Map createBaseKeyParams = createBaseKeyParams();
        createBaseKeyParams.put("appId", BaseApi.APP_ID);
        createBaseKeyParams.put(NotificationCompat.CATEGORY_SERVICE, "https://www.zcool.com.cn");
        createBaseKeyParams.put("appLogin", "https://www.zcool.com.cn/tologin.do");
        createBaseKeyParams.put("username", str);
        createBaseKeyParams.put(WifiEnterpriseConfig.PASSWORD_KEY, str2);
        if (!TextUtils.isEmpty(str3)) {
            createBaseKeyParams.put("thirdId", str3);
            createBaseKeyParams.put("siteId", Integer.valueOf(i));
        }
        Map<String, String> createBaseParams = createBaseParams();
        buildAndSetKeyParams(createBaseParams, createBaseKeyParams);
        return this.mApiInterface.signIn(createBaseParams, createBaseHeaders()).map(new Function<NetSignInInfo, TrustedResponse<TrustedSignInInfo>>() { // from class: com.zcool.community.data.api.PassportApi.4
            /* JADX WARN: Type inference failed for: r2v1, types: [T, com.zcool.community.data.api.entity.trusted.TrustedSignInInfo] */
            @Override // io.reactivex.functions.Function
            public TrustedResponse<TrustedSignInInfo> apply(@io.reactivex.annotations.NonNull NetSignInInfo netSignInInfo) throws Exception {
                com.zcool.community.data.api.entity.net.NetResponse netResponse = new com.zcool.community.data.api.entity.net.NetResponse();
                netResponse.data = netSignInInfo.toTrustedSignInInfo();
                if (((TrustedSignInInfo) netResponse.data).result) {
                    netResponse.code = 0;
                } else {
                    netResponse.code = -1;
                }
                netResponse.msg = ((TrustedSignInInfo) netResponse.data).msg;
                return netResponse.toTrustedResponse(new Converter<TrustedSignInInfo, TrustedSignInInfo>() { // from class: com.zcool.community.data.api.PassportApi.4.1
                    @Override // com.zcool.community.lang.Converter
                    public TrustedSignInInfo convert(TrustedSignInInfo trustedSignInInfo) {
                        return trustedSignInInfo;
                    }
                });
            }
        }).map(new Function<TrustedResponse<TrustedSignInInfo>, TrustedResponse<TrustedSignInInfo>>() { // from class: com.zcool.community.data.api.PassportApi.3
            @Override // io.reactivex.functions.Function
            public TrustedResponse<TrustedSignInInfo> apply(@io.reactivex.annotations.NonNull TrustedResponse<TrustedSignInInfo> trustedResponse) throws Exception {
                if (trustedResponse.code == 0 && trustedResponse.data.userId > 0) {
                    if (!TextUtils.isEmpty(trustedResponse.data.SERVER_COOKIE_V1)) {
                        CookiesHelper.addPassportServerCookieV1(trustedResponse.data.SERVER_COOKIE_V1);
                    } else {
                        Timber.e("sign in success, but cookie not found", new Object[0]);
                        new IllegalAccessError("SERVER_COOKIE_V1 not found").printStackTrace();
                    }
                }
                return trustedResponse;
            }
        });
    }

看common的构建,除了uniqueCode都是固定的,uniqueCode跟自己手机有关,这个字段不用管了
image-20250314105840308.png
这样一来数据包传输的东西只有password和username是不固定的,其他的基本固定,可以直接拿数值构建,再来看请求头的构造
[Asm] 纯文本查看 复制代码
1
2
3
4
5
return this.mApiInterface.signIn(createBaseParams, createBaseHeaders()).map(new Function<NetSignInInfo, TrustedResponse<TrustedSignInInfo>>() {
 
}
                                                                             
// createBaseParams 是加密后的参数,createBaseHeaders()函数用于构造请求头

先获取令牌,由于是首次登录不存在令牌,在请求头添加 common 和 BaseInfo,内容相同 image-20250314130122238.png PixPin_2025-03-15_16-50-07.png
请求头的其他部分都是 OkHttp 默认添加的,也不存在时间戳等时间,开始简单仿造一个请求请求返回的数据经过gzip压缩,之前的请求头上以及表示可以接收gzip压缩了,可以写个判断,这里直接使用了,没判断
[JavaScript] 纯文本查看 复制代码
01
02
03
04
05
06
07
08
09
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
const https = require('https');
const zlib = require('zlib');
 
// 请求URL
const url = 'https://passport.zcool.com.cn/login_jsonp_active.do';
 
// 请求头
const headers = {
    'Content-Type': 'application/x-www-form-urlencoded',
    'Host': 'passport.zcool.com.cn',
    'Connection': 'Keep-Alive',
    'Accept-Encoding': 'gzip',
    'Cookie': 'HWWAFSESID=a3b2973ef5454da521; HWWAFSESTIME=1741081260467',
    'User-Agent': 'okhttp/3.12.0',
    'common': {"uniqueCode":"dd67d0c0-01e5-492d-8b3b-c35c5fa6ba48","appId":"com.zcool.community","channel":"zcool","mobileType":"android","versionCode":4638},
    'BaseInfo': {"uniqueCode":"dd67d0c0-01e5-492d-8b3b-c35c5fa6ba48","appId":"com.zcool.community","channel":"zcool","mobileType":"android","versionCode":4638}
};
 
// 请求体
const body = `app=android&key=dTdlMmtGQjZIQk45NEN3dTIzamdVb2xYMVp5dDZnYmgrMGRlK2xjbkpwZjJpWGczb1FhYWxUVUx1%250AaVJ1d01ZeG5mZUpXZzUvT0M0WQpuYnF1NG0wdjZEd0NaSldPMkZGQ1loRmZ5NG1PM1dTMmhGd3d0%250AbVNhaG9vMVRBbitzQWRrZVBubWZxNGt6ajIyUHJpdEVxUGhHM0tKCmMzWmExU2FXWDJ0VjA0S0NE%250AL3owNmNOUEUrMjRwcExDR0VqSVViU2RyVDU2a0RmS0dqOFhKYmNyYjljM0lqbE9IWm5Rajl5UmM1%250AdnMKTDlFc2xLaVJNRU5rUVJ6RnVaN1k0OVBPTklkeVpsYUJwaG5UTlpkTy9DcmJDcmhvTjRZUUM2%250AcjZiSU4xcTdieHlIOTBxNjYyT0RCNwpNZVhzQVcwbjI3eEtUTHVDWmxhQnBoblROWmRPL0NyYkNy%250AaG9Od0dTRVo1aGxrNlAzMHpYOFFIaTJRZEhjM2JBQy9ESC9iQXBidko1CnUybDluN3FOYkFaT1ZO%250AajNaYnlHSjJrVmdBPT0KP2tleUlkPTE%253D%250A`;
 
// 构造请求选项
const options = {
    hostname: 'passport.zcool.com.cn',
    port: 443,
    path: '/login_jsonp_active.do',
    method: 'POST',
    headers: headers
};
 
// 发送POST请求
const req = https.request(options, (res) => {
    let responseData = '';
 
    const gunzip = zlib.createGunzip();
    res.pipe(gunzip);
    gunzip.on('data', (chunk) => {
        responseData += chunk;
    });
 
    gunzip.on('end', () => {
        console.log('响应数据:', responseData);
    });
 
    // res.setEncoding('utf8');
 
    // res.on('data', (chunk) => {
    //     responseData += chunk;
    // });
 
    // res.on('end', () => {
    //     console.log('响应数据:', responseData);
    // });
});
 
req.on('error', (error) => {
    console.error('请求错误:', error);
});
 
req.write(body);
req.end();

和手机上结果相同 image-20250314160152587.png image-20250314160228991.png
返回的数据是没有进行加密的,明文传输回来,就不需要写DESede解密方法了,写一个加密方法,完善这个请求
[JavaScript] 纯文本查看 复制代码
01
02
03
04
05
06
07
08
09
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
var http = require('https');
const zlib = require('zlib');
const CryptoJS = require('crypto-js');
 
let username = "13112345678"
let password = "12345678"
 
let date = `{"password": ${password},"common":{"uniqueCode":"dd67d0c0-01e5-492d-8b3b-c35c5fa6ba48","appId":"com.zcool.community","channel":"zcool","mobileType":"android","versionCode":4638},"appLogin":"https://www.zcool.com.cn/tologin.do","service":"https://www.zcool.com.cn","appId":"1006","username": ${username}}`
 
// DESede解密
function decodeDESede(data, key) {
    var _key =  CryptoJS.enc.Utf8.parse(key);
 
    var decrypted = CryptoJS.TripleDES.decrypt(data, _key, {
        mode: CryptoJS.mode.ECB,
        padding: CryptoJS.pad.Pkcs7
    }).toString(CryptoJS.enc.Utf8);
 
    return decrypted;
}
 
// DESede加密
function encodeDESede(data, key) {
    var _key =  CryptoJS.enc.Utf8.parse(key);
 
    var decrypted = CryptoJS.TripleDES.encrypt(data, _key, {
        mode: CryptoJS.mode.ECB,
        padding: CryptoJS.pad.Pkcs7
    }).toString();
 
    return decrypted;
}
 
// 按照安卓代码处理body,这里的加密结果没用换行符,测试了一下也没啥问题,理论上没用换行符也不要url编码了。
function handleBody(body) {
    var result = body + '\n' + '?keyId=1';
    result = encodeURIComponent(btoa(result));
    return `app=android&key=${result}`;
}
 
console.log(handleBody(encodeDESede(date, key)))
 
 
// 构造请求
function post(result) {
    const headers = {
        'Content-Type': 'application/x-www-form-urlencoded',
        'Host': 'passport.zcool.com.cn',
        'Connection': 'Keep-Alive',
        'Accept-Encoding': 'gzip',
        'User-Agent': 'okhttp/3.12.1',
        'common': {"uniqueCode":"dd67d0c0-01e5-492d-8b3b-c35c5fa6ba48","appId":"com.zcool.community","channel":"zcool","mobileType":"android","versionCode":4638},
        'BaseInfo': {"uniqueCode":"dd67d0c0-01e5-492d-8b3b-c35c5fa6ba48","appId":"com.zcool.community","channel":"zcool","mobileType":"android","versionCode":4638}
    }
 
    var options = {
        hostname: 'passport.zcool.com.cn',
        port: 443,
        path: '/login_jsonp_active.do',
        method: 'POST',
        headers: headers
    }
 
    var req = http.request(options, function (res) {
        // console.log('STATUS:'+ res.statusCode);
        // console.log('HEADERS:'+ JSON.stringify(res.headers));
 
        var body = '';
 
        const gunzip = zlib.createGunzip();
        res.pipe(gunzip);
 
        gunzip.on('data', function (chunk) {
            body += chunk;
        });
 
        gunzip.on('end', function () {
            console.log('响应数据:', body)
        });
    });
 
    req.on('error', function (e) {
        console.log('problem with request:'+ e.message);
    })
 
    req.write(result);
    req.end();
}
 
post(handleBody(encodeDESede(date, key)))

这次没怎么hook(汗),hook代码
[JavaScript] 纯文本查看 复制代码
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// hook代码
Java.perform(function () {
    function showStacks() {
        console.log(
            Java.use("android.util.Log")
                .getStackTraceString(
                    Java.use("java.lang.Throwable").$new()
                )
        );
    }
 
    var HashMap = Java.use('java.util.HashMap');
    HashMap.put.implementation = function(a, b) {
     
        // 打印username的堆栈信息
        if (a.equals("username")) {
            showStacks();
            console.log("hashMap.put :", a, b);
        }
     
        console.log("put: ", a, b);
        return this.put(a, b);
    }
 
    var EncryptManager = Java.use("com.zcool.community.data.api.encrypt.EncryptManager");
    EncryptManager["encrypt"].implementation = function (map) {
        console.log(`EncryptManager.encrypt is called: map=${map}`);
        let result = this["encrypt"](map);
        console.log(`EncryptManager.encrypt result=${result}`);
        return result;
    };
});

这个软件的登录挺简单的,标准的DESede加密,以及方法和密钥在明文里,没有so层的逆向,请求也比较简单,没有时间戳部分。

免费评分

参与人数 2威望 +2 吾爱币 +101 热心值 +2 收起 理由
qinmou + 1 + 1 我很赞同!
正己 + 2 + 100 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

Treks 发表于 2025-3-16 14:01
感谢分享
gongluche 发表于 2025-3-16 16:32
podehui 发表于 2025-3-16 16:54
lizhi31101 发表于 2025-3-16 17:15
感谢分享,收藏学习
kingstk 发表于 2025-3-16 18:58
学习学习
freesq 发表于 2025-3-16 20:12
学习了,感谢大神的辅课
vilenyuu 发表于 2025-3-16 20:37
向大神学习  步骤很清晰
buta 发表于 2025-3-17 09:46
大神威武,学习了
cqman2 发表于 2025-3-17 11:41
感谢分享~!
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2025-4-3 22:53

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表