工具准备:Pixel XL // 该软件不支持模拟器 雷电APP(脱壳用,不会脱壳,哭) frIDA
本贴仅作技术交流,如有侵权请在联系我立即删帖
符合360加固的特征,用雷电脱个壳,扔jadx编译一下
尝试一下抓包,抓到两个数据包
只有下面这一个数据包和登录有关
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的字段堆栈
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<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] // io.reactivex.functions.Function
public TrustedResponse<TrustedSignInInfo> apply( .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
// 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
// io.reactivex.functions.Function
public TrustedResponse<TrustedSignInInfo> apply( .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
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就是一个关键方法了
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加密之后的结果就是
dTdlMmtGQjZIQk45NEN3dTIzamdVb2xYMVp5dDZnYmgrMGRlK2xjbkpwZjJpWGczb1FhYWxUVUx1
aVJ1d01ZeG5mZUpXZzUvT0M0WQpuYnF1NG0wdjZEd0NaSldPMkZGQ1loRmZ5NG1PM1dTMmhGd3d0
bVNhaG9vMVRBbitzQWRrZVBubWZxNGt6ajIyUHJpdEVxUGhHM0tKCmMzWmExU2FXWDJ0VjA0S0NE
L3owNmNOUEUrMjRwcExDR0VqSVViU2RyVDU2a0RmS0dqOFhKYmNyYjljM0lqbE9IWm5Rajl5UmM1
dnMKTDlFc2xLaVJNRU5rUVJ6RnVaN1k0OVBPTklkeVpsYUJwaG5UTlpkTy9DcmJDcmhvTjRZUUM2
cjZiSU4xcTdieHlIOTBxNjYyT0RCNwpNZVhzQVcwbjI3eEtUTHVDWmxhQnBoblROWmRPL0NyYkNy
aG9Od0dTRVo1aGxrNlAzMHpYOFFIaTJRZEhjM2JBQy9ESC9iQXBidko1CnUybDluN3FOYkFaT1ZO
ajNaYnlHSjJrVmdBPT0KP2tleUlkPTE=
ECB的填充模式,没有iv值,先将传入的字符串转换成byte的形式,再将传入的keyID=1作为密钥,看起来这个是一个标准的DESede/ECB加密
将原数据base64解码,去除掉拼接的 ?keyId=1, 得到密文,尝试DESede解码
u7e2kFB6HBN94Cwu23jgUolX1Zyt6gbh+0de+lcnJpf2iXg3oQaalTULuiRuwMYxnfeJWg5/OC4Y
nbqu4m0v6DwCZJWO2FFCYhFfy4mO3WS2hFwwtmSahoo1TAn+sAdkePnmfq4kzj22PritEqPhG3KJ
c3Za1SaWX2tV04KCD/z06cNPE+24ppLCGEjIUbSdrT56kDfKGj8XJbcrb9c3IjlOHZnQj9yRc5vs
L9EslKiRMENkQRzFuZ7Y49PONIdyZlaBphnTNZdO/CrbCrhoN4YQC6r6bIN1q7bxyH90q662ODB7
MeXsAW0n27xKTLuCZlaBphnTNZdO/CrbCrhoNwGSEZ5hlk6P30zX8QHi2QdHc3bAC/DH/bApbvJ5
u2l9n7qNbAZOVNj3ZbyGJ2kVgA==
?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<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] */
// io.reactivex.functions.Function
public TrustedResponse<TrustedSignInInfo> apply( .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
// 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
// io.reactivex.functions.Function
public TrustedResponse<TrustedSignInInfo> apply( .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<NetSignInInfo, TrustedResponse<TrustedSignInInfo>>() {
}
// createBaseParams 是加密后的参数,createBaseHeaders()函数用于构造请求头
先获取令牌,由于是首次登录不存在令牌,在请求头添加 common 和 BaseInfo,内容相同
请求头的其他部分都是 OkHttp 默认添加的,也不存在时间戳等时间,开始简单仿造一个请求请求返回的数据经过gzip压缩,之前的请求头上以及表示可以接收gzip压缩了,可以写个判断,这里直接使用了,没判断
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();
和手机上结果相同
返回的数据是没有进行加密的,明文传输回来,就不需要写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层的逆向,请求也比较简单,没有时间戳部分。
原文始发于微信公众号(逆向有你):安卓逆向 -- 分析某库登录逻辑以及协议复现
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论