分析某软件登录逻辑以及协议复现
本贴仅作技术交流,如有侵权请在Github的issue联系我立即删帖
工具准备:Pixel XL // 该软件不支持模拟器
雷电APP(脱壳用,不会脱壳,哭)
frida
符合360加固的特征,用雷电脱个壳,仍jadx编译一下
尝试一下抓包,抓到两个数据包
只有第一个有用,下面那个跟登录无关
1 2 3 4 5 6 7 8 9 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
1 2 3 4 5 6 7 8 9 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 ) { if (a.equals ("username" )) { showStacks (); console .log ("hashMap.put :" , a, b); } console .log ("put: " , a, b); return this .put (a, b); } }
跟抓到的包对比一下,
根据这个堆栈信息去寻找登录加密函数,把没用的东西去掉,看看这几个函数
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 这个地址先
进去是个接口,定义了一个 signIn 方法
再去寻找一下刚刚堆栈的第一个方法,恰好也是 sigin,使用的就是上图接口
稍微分析了一下 str 是用户名 str2 是用户密码,这个 str3 是处理第三方登录的如qq,微信,测试用的账号密码登录,不管这个
从 str2 跳出去找到 PASSWORD_KEY = “password”,石锤了是密码字段,可能为了防止搜明文搜出来吧
然后就是调用方法 buildAndSetKeyParams
进行数据加密,加密完成时候,发起网络请求
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 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>>() { @Override 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>() { @Override public TrustedSignInInfo convert (TrustedSignInInfo trustedSignInInfo) { return trustedSignInInfo; } }); } }).map(new Function <TrustedResponse<TrustedSignInInfo>, TrustedResponse<TrustedSignInInfo>>() { @Override 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
函数,这个是继承父类的方法,父类方法中只有一句
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
类
绕了一圈,创建了一个EncryptManager
类 调用这个类的 encrypt
方法,传入map2,map是空的,map2包含用户的各种信息。
hook一下这个方法,查看出入值,返回值和抓包的值是否有一致的部分
1 2 3 4 5 6 7 8 9 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; }; });
处理一下着两份数据,不能说大差不差,只能说完全一样,只有间隔符不一样返回的数值的间隔符是 %0A ,抓包的是 %250A,而发包是要使用url编码,%25在url编码中就是%
这些东西将密文分成了 76 字符一组。
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 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就是一个关键方法了
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 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 (); String json = new Gson ().toJson(map); stringBuffer.append(DESedeCoder.encode(json, KEYS.get("1" ))); stringBuffer.append("?keyId=" ); stringBuffer.append("1" ); String stringBuffer2 = stringBuffer.toString(); try { 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加密之后的结果就是
1 2 3 4 5 6 7 8 dTdlMmtGQjZIQk45NEN3dTIzamdVb2xYMVp5dDZnYmgrMGRlK2xjbkpwZjJpWGczb1FhYWxUVUx1 aVJ1d01ZeG5mZUpXZzUvT0M0WQpuYnF1NG0wdjZEd0NaSldPMkZGQ1loRmZ5NG1PM1dTMmhGd3d0 bVNhaG9vMVRBbitzQWRrZVBubWZxNGt6ajIyUHJpdEVxUGhHM0tKCmMzWmExU2FXWDJ0VjA0S0NE L3owNmNOUEUrMjRwcExDR0VqSVViU2RyVDU2a0RmS0dqOFhKYmNyYjljM0lqbE9IWm5Rajl5UmM1 dnMKTDlFc2xLaVJNRU5rUVJ6RnVaN1k0OVBPTklkeVpsYUJwaG5UTlpkTy9DcmJDcmhvTjRZUUM2 cjZiSU4xcTdieHlIOTBxNjYyT0RCNwpNZVhzQVcwbjI3eEtUTHVDWmxhQnBoblROWmRPL0NyYkNy aG9Od0dTRVo1aGxrNlAzMHpYOFFIaTJRZEhjM2JBQy9ESC9iQXBidko1CnUybDluN3FOYkFaT1ZO ajNaYnlHSjJrVmdBPT0KP2tleUlkPTE=
ECB的填充模式,没有iv值,先将传入的字符串转换成byte的形式,再将传入的keyID=1作为密钥,看起来这个是一个标准的DESede/ECB加密
将原数据base64解码,去除掉拼接的 ?keyId=1, 得到密文,尝试DESede解码
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
解密脚本
1 2 3 4 5 6 7 8 9 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))
解密后的数据
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字段以及提前构建好了
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 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>>() { @Override 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>() { @Override public TrustedSignInInfo convert (TrustedSignInInfo trustedSignInInfo) { return trustedSignInInfo; } }); } }).map(new Function <TrustedResponse<TrustedSignInInfo>, TrustedResponse<TrustedSignInInfo>>() { @Override 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跟自己手机有关,这个字段不用管了
这样一来数据包传输的东西只有password和username是不固定的,其他的基本固定,可以直接拿数值构建,再来看请求头的构造
1 2 3 4 5 return this .mApiInterface.signIn(createBaseParams, createBaseHeaders()).map(new Function <NetSignInInfo, TrustedResponse<TrustedSignInInfo>>() {}
先获取令牌,由于是首次登录不存在令牌,在请求头添加 common 和 BaseInfo,内容相同
请求头的其他部分都是 OkHttp 默认添加的,也不存在时间戳等时间,开始简单仿造一个请求
请求返回的数据经过gzip压缩,之前的请求头上以及表示可以接收gzip压缩了,可以写个判断,这里直接使用了,没判断
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 const https = require ('https' );const zlib = require ('zlib' );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 }; 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); }); }); req.on ('error' , (error ) => { console .error ('请求错误:' , error); }); req.write (body); req.end ();
和手机上结果相同
返回的数据是没有进行加密的,明文传输回来,就不需要写DESede解密方法了,写一个加密方法,完善这个请求
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 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} }` 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; } 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; } 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 ) { 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(汗)
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 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 ) { 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层的逆向,请求也比较简单,没有时间戳部分。