安卓逆向 分析某库登录逻辑以及协议复现

admin 2025年5月15日09:49:45评论0 views字数 19508阅读65分1秒阅读模式

工具准备:Pixel XL       // 该软件不支持模拟器                                  雷电APP(脱壳用,不会脱壳,哭)  frIDA

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

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

安卓逆向  分析某库登录逻辑以及协议复现

尝试一下抓包,抓到两个数据包

安卓逆向  分析某库登录逻辑以及协议复现

只有下面这一个数据包和登录有关

POST /login_jsonp_active.do HTTP/1.1common: {"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-urlencodedContent-Length: 624Host: passport.zcool.com.cnConnection: Keep-AliveAccept-Encoding: gzipCookie: HWWAFSESID=a3b2973ef5454da521; HWWAFSESTIME=1741081260467User-Agent: okhttp/3.12.0app=android&key=dTdlMmtGQjZIQk45NEN3dTIzamdVb2xYMVp5dDZnYmgrMGRlK2xjbkpwZjJpWGczb1FhYWxUVUx1%250AaVJ1d01ZeG5mZUpXZzUvT0M0WQpuYnF1NG0wdjZEd0NaSldPMkZGQ1loRmZ5NG1PM1dTMmhGd3d0%250AbVNhaG9vMVRBbitzQWRrZVBubWZxNGt6ajIyUHJpdEVxUGhHM0tKCmMzWmExU2FXWDJ0VjA0S0NE%250AL3owNmNOUEUrMjRwcExDR0VqSVViU2RyVDU2a0RmS0dqOFhKYmNyYjljM0lqbE9IWm5Rajl5UmM1%250AdnMKTDlFc2xLaVJNRU5rUVJ6RnVaN1k0OVBPTklkeVpsYUJwaG5UTlpkTy9DcmJDcmhvTjRZUUM2%250AcjZiSU4xcTdieHlIOTBxNjYyT0RCNwpNZVhzQVcwbjI3eEtUTHVDWmxhQnBoblROWmRPL0NyYkNy%250AaG9Od0dTRVo1aGxrNlAzMHpYOFFIaTJRZEhjM2JBQy9ESC9iQXBidko1CnUybDluN3FOYkFaT1ZO%250AajNaYnlHSjJrVmdBPT0KP2tleUlkPTE%253D%250A

看那么长的数据先从 hashmap下手,hook hashmap,因为是登录逻辑,可以打印一下username或者password的字段堆栈

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);    }}
安卓逆向  分析某库登录逻辑以及协议复现
安卓逆向  分析某库登录逻辑以及协议复现

根据这个堆栈信息去寻找登录加密函数,把没用的东西去掉,看看这几个函数

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 这个地址先

安卓逆向  分析某库登录逻辑以及协议复现

进去是个接口,定义了一个 signIn 方法

安卓逆向  分析某库登录逻辑以及协议复现

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

安卓逆向  分析某库登录逻辑以及协议复现

稍微分析了一下 str 是用户名 str2 是用户密码,这个 str3 是处理第三方登录的如qq,微信,测试用的账号密码登录,不管这个从 str2 跳出去找到 PASSWORD_KEY = “password”,石锤了是密码字段,可能为了防止搜明文搜出来吧

安卓逆向  分析某库登录逻辑以及协议复现

然后就是调用方法 buildAndSetKeyParams 进行数据加密,加密完成时候,发起网络请求

Map<StringString> 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<NetSignInInfoTrustedResponse<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] */    
@Override[/url] // io.reactivex.functions.Function
    public TrustedResponse<TrustedSignInInfoapply(@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<TrustedSignInInfoTrustedSignInInfo>() { // 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<TrustedSignInInfoapply(@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 函数,这个是继承父类的方法,父类方法中只有一句

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 类

安卓逆向  分析某库登录逻辑以及协议复现

绕了一圈,创建了一个EncryptManager 类 调用这个类的 encrypt 方法,传入map2,map是空的,map2包含用户的各种信息。hook一下这个方法,查看出入值,返回值和抓包的值是否有一致的部分

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;    };});
安卓逆向  分析某库登录逻辑以及协议复现

处理一下着两份数据,不能说大差不差,只能说完全一样,只有间隔符不一样返回的数值的间隔符是 %0A ,抓包的是 %250A,而发包是要使用url编码,%25在url编码中就是%这些东西将密文分成了 76 字符一组。

result=dTdlMmtGQjZIQk45NEN3dTIzamdVb2xYMVp5dDZnYmgrMGRlK2xjbkpwZjJpWGczb1FhYWxUVUx1%0AaVJ1d01ZeG5mZUpXZzUvT0M0WQpuYnF1NG0wdjZEd0NaSldPMkZGQ1loRmZ5NG1PM1dTMmhGd3d0%0AbVNhaG9vMVRBbitzQWRrZVBubWZxNGt6ajIyUHJpdEVxUGhHM0tKCmMzWmExU2FXWDJ0VjA0S0NE%0AL3owNmNOUEUrMjRwcExDR0VqSVViU2RyVDU2a0RmS0dqOFhKYmNyYjljM0lqbE9IWm5Rajl5UmM1%0AdnMKTDlFc2xLaVJNRU5rUVJ6RnVaN1k0OVBPTklkeVpsYUJwaG5UTlpkTy9DcmJDcmhvTjRZUUM2%0AcjZiSU4xcTdieHlIOTBxNjYyT0RCNwpNZVhzQVcwbjI3eEtUTHVDWmxhQnBoblROWmRPL0NyYkNy%0AaG9Od0dTRVo1aGxrNlAzMHpYOFFIaTJRZEhjM2JBQy9ESC9iQXBidko1CnUybDluN3FOYkFaT1ZO%0AajNaYnlHSjJrVmdBPT0KP2tleUlkPTE%3D%0Aapp=android&key=dTdlMmtGQjZIQk45NEN3dTIzamdVb2xYMVp5dDZnYmgrMGRlK2xjbkpwZjJpWGczb1FhYWxUVUx1%250AaVJ1d01ZeG5mZUpXZzUvT0M0WQpuYnF1NG0wdjZEd0NaSldPMkZGQ1loRmZ5NG1PM1dTMmhGd3d0%250AbVNhaG9vMVRBbitzQWRrZVBubWZxNGt6ajIyUHJpdEVxUGhHM0tKCmMzWmExU2FXWDJ0VjA0S0NE%250AL3owNmNOUEUrMjRwcExDR0VqSVViU2RyVDU2a0RmS0dqOFhKYmNyYjljM0lqbE9IWm5Rajl5UmM1%250AdnMKTDlFc2xLaVJNRU5rUVJ6RnVaN1k0OVBPTklkeVpsYUJwaG5UTlpkTy9DcmJDcmhvTjRZUUM2%250AcjZiSU4xcTdieHlIOTBxNjYyT0RCNwpNZVhzQVcwbjI3eEtUTHVDWmxhQnBoblROWmRPL0NyYkNy%250AaG9Od0dTRVo1aGxrNlAzMHpYOFFIaTJRZEhjM2JBQy9ESC9iQXBidko1CnUybDluN3FOYkFaT1ZO%250AajNaYnlHSjJrVmdBPT0KP2tleUlkPTE%253D%250A

encrypt就是一个关键方法了

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加密之后的结果就是

dTdlMmtGQjZIQk45NEN3dTIzamdVb2xYMVp5dDZnYmgrMGRlK2xjbkpwZjJpWGczb1FhYWxUVUx1aVJ1d01ZeG5mZUpXZzUvT0M0WQpuYnF1NG0wdjZEd0NaSldPMkZGQ1loRmZ5NG1PM1dTMmhGd3d0bVNhaG9vMVRBbitzQWRrZVBubWZxNGt6ajIyUHJpdEVxUGhHM0tKCmMzWmExU2FXWDJ0VjA0S0NEL3owNmNOUEUrMjRwcExDR0VqSVViU2RyVDU2a0RmS0dqOFhKYmNyYjljM0lqbE9IWm5Rajl5UmM1dnMKTDlFc2xLaVJNRU5rUVJ6RnVaN1k0OVBPTklkeVpsYUJwaG5UTlpkTy9DcmJDcmhvTjRZUUM2cjZiSU4xcTdieHlIOTBxNjYyT0RCNwpNZVhzQVcwbjI3eEtUTHVDWmxhQnBoblROWmRPL0NyYkNyaG9Od0dTRVo1aGxrNlAzMHpYOFFIaTJRZEhjM2JBQy9ESC9iQXBidko1CnUybDluN3FOYkFaT1ZOajNaYnlHSjJrVmdBPT0KP2tleUlkPTE=

ECB的填充模式,没有iv值,先将传入的字符串转换成byte的形式,再将传入的keyID=1作为密钥,看起来这个是一个标准的DESede/ECB加密

安卓逆向  分析某库登录逻辑以及协议复现

将原数据base64解码,去除掉拼接的 ?keyId=1, 得到密文,尝试DESede解码

u7e2kFB6HBN94Cwu23jgUolX1Zyt6gbh+0de+lcnJpf2iXg3oQaalTULuiRuwMYxnfeJWg5/OC4Ynbqu4m0v6DwCZJWO2FFCYhFfy4mO3WS2hFwwtmSahoo1TAn+sAdkePnmfq4kzj22PritEqPhG3KJc3Za1SaWX2tV04KCD/z06cNPE+24ppLCGEjIUbSdrT56kDfKGj8XJbcrb9c3IjlOHZnQj9yRc5vsL9EslKiRMENkQRzFuZ7Y49PONIdyZlaBphnTNZdO/CrbCrhoN4YQC6r6bIN1q7bxyH90q662ODB7MeXsAW0n27xKTLuCZlaBphnTNZdO/CrbCrhoNwGSEZ5hlk6P30zX8QHi2QdHc3bAC/DH/bApbvJ5u2l9n7qNbAZOVNj3ZbyGJ2kVgA==?keyId=1
安卓逆向  分析某库登录逻辑以及协议复现

解密脚本

const CryptoJS require('crypto-js');key = "F#C@5IOBULR9L415C~ZX*97C"data = "u7e2kFB6HBN94Cwu23jgUolX1Zyt6gbh+0de+lcnJpf2iXg3oQaalTULuiRuwMYxnfeJWg5/OC4Ynbqu4m0v6DwCZJWO2FFCYhFfy4mO3WS2hFwwtmSahoo1TAn+sAdkePnmfq4kzj22PritEqPhG3KJc3Za1SaWX2tV04KCD/z06cNPE+24ppLCGEjIUbSdrT56kDfKGj8XJbcrb9c3IjlOHZnQj9yRc5vsL9EslKiRMENkQRzFuZ7Y49PONIdyZlaBphnTNZdO/CrbCrhoN4YQC6r6bIN1q7bxyH90q662ODB7MeXsAW0n27xKTLuCZlaBphnTNZdO/CrbCrhoNwGSEZ5hlk6P30zX8QHi2QdHc3bAC/DH/bApbvJ5u2l9n7qNbAZOVNj3ZbyGJ2kVgA=="functiondecodeDESede(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))
安卓逆向  分析某库登录逻辑以及协议复现

解密后的数据

{"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字段以及提前构建好了

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<StringString> createBaseParams = createBaseParams();        buildAndSetKeyParams(createBaseParams, createBaseKeyParams);        return this.mApiInterface.signIn(createBaseParams, createBaseHeaders()).map(new Function<NetSignInInfoTrustedResponse<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<TrustedSignInInfoapply(@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<TrustedSignInInfoTrustedSignInInfo>() { // 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<TrustedSignInInfoapply(@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跟自己手机有关,这个字段不用管了

安卓逆向  分析某库登录逻辑以及协议复现

这样一来数据包传输的东西只有password和username是不固定的,其他的基本固定,可以直接拿数值构建,再来看请求头的构造

return this.mApiInterface.signIn(createBaseParams, createBaseHeaders()).map(new Function<NetSignInInfoTrustedResponse<TrustedSignInInfo>>() {}// createBaseParams 是加密后的参数,createBaseHeaders()函数用于构造请求头

先获取令牌,由于是首次登录不存在令牌,在请求头添加 common 和 BaseInfo,内容相同

安卓逆向  分析某库登录逻辑以及协议复现
安卓逆向  分析某库登录逻辑以及协议复现

请求头的其他部分都是 OkHttp 默认添加的,也不存在时间戳等时间,开始简单仿造一个请求请求返回的数据经过gzip压缩,之前的请求头上以及表示可以接收gzip压缩了,可以写个判断,这里直接使用了,没判断

const https = require('https');const zlib = require('zlib');// 请求URLconst 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',    port443,    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();

和手机上结果相同

安卓逆向  分析某库登录逻辑以及协议复现
安卓逆向  分析某库登录逻辑以及协议复现

返回的数据是没有进行加密的,明文传输回来,就不需要写DESede解密方法了,写一个加密方法,完善这个请求

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解密functiondecodeDESede(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加密functionencodeDESede(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编码了。functionhandleBody(body{    var result = body + 'n' + '?keyId=1';    result = encodeURIComponent(btoa(result));    return `app=android&key=${result}`;}console.log(handleBody(encodeDESede(date, key)))// 构造请求functionpost(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代码

// 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层的逆向,请求也比较简单,没有时间戳部分。

 

原文始发于微信公众号(逆向有你):安卓逆向 -- 分析某库登录逻辑以及协议复现

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年5月15日09:49:45
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   安卓逆向 分析某库登录逻辑以及协议复现https://cn-sec.com/archives/4063754.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息