某青看点app逆向

1、抓包分析


抓包分析后发现接口请求的params参数和响应信息都是加密的,那么我们需要通过frida对加密参数进行分析

2、frida hook加密位置

我们首先通过hook HashMap的方式来看看是否能得到有用的信息

1
2
3
4
5
6
7
function hookMap() {
var hashMap = Java.use('java.util.HashMap')
hashMap.put.implementation = function (key, value) {
console.log(`key: ${key}, value: ${value}`)
return this.put(key, value)
}
}

hook完成之后发现并没有获取到有用的信息,那么我们接着尝试hook json的方式

1
2
3
4
5
6
7
8
9
10
11
12
function hookJson() {
var JSONObject = Java.use('org.json.JSONObject');
// Hook JSONObject.put() 方法
JSONObject.put.overload('java.lang.String', 'java.lang.Object').implementation = function (key, value) {
console.log('Hooked JSONObject.put()');
console.log('Key: ' + key.toString());
console.log('Value: ' + value.toString());
// 可在此处对参数进行修改或记录
// 调用原始的put()方法
return this.put(key, value);
};
}

通过hook json的这种方式也并没有获取到有用的信息,那么我们接着尝试hook StringBuilder的方式

1
2
3
4
5
6
7
8
function hookStrBuilder() {
var stringBuilderClass = Java.use("java.lang.StringBuilder");
stringBuilderClass.toString.implementation = function () {
var res = this.toString.apply(this, arguments)
console.log('tostring is called ', res)
return res
}
}

通过hook StringBuilder的方式发现了有用的信息

1
tostring is called  /v3/article/lists.json?zqkd_param=wXU6PBNNsHKc=Rse1ImYBpFW8PwGnvfcNJa5Yg4UIJOenwoUuu7mUCt0kEXDj5ACzfcISZdrlvya9h9ijWacMRP8N7qvjYgQNzL45veAD9SZlcNH7QJiW-AyOu-HKB7OjeSRZjvPBBbhhbTTyw1eCHnMU1uJ5rY0g4DsUz7lEmml-PRkpa-F1BmjpS2U6j9kmZC3j04W40ZV6Btru9WfGSEtyo55F50BGtVWw4gDRYBrqDVK3R4yTIlg4WcMjD_UWWS_LqaLpL-LgBIdx8PpPfoAqmkM6cOo-DdD-Sb5IZNA_-aRQJVT_RoscdZPNjM8u5Si_x_MSUwe16vJMr7rCd3_D5i9lNAQDlqAGfEkcgPQkWll25ietvQOJqZgTdv9L0Ko85-u7yPOwliw2rStygqCQXqOZ4j0m2UBpRoMQNJyGNO1tuK6zl-iqsCg-G7ChY15A6rAZhJzxBQ-6UTv_BLRrpRs-bQ2oV_eQif_2zDq4BvFsJ36xpfTrmu0eoclncG-Qmx8kyakYqk2Q_9TKO517I3G3P5hIfK4pPb0GDzgZH2fQ9VtkaWWMdHyCqcxlKRBd0lvQBqE8Gs7LivC1bVg1iCZNXQhiDUzjEUOjopcpoWullznXXRsje7qK87rQpKFKP85n24ppAbmBrT5kgnGkd8s9AvBM5yK-yMq3sDyhU-kJIgDDnKFIZM6dK9tFZYNuDH3SUw0CZ607P0CUz1X-CPmPD3AyHfFJyjUe_wDCwxmbsweN_TnUeP57IA4g-HHwOnEcBwS3LBOthXVepwDCjn5rFk4Jy8-XUAou-gRO7B6o-Tpb3kZcXH7T3ur0C150l-swLgYq1Wliep2WZLLrNAXSMPxzUflN5g5ATLTXCcAPYEmaO6f43GHGActJlY967F23PWQh7bG510CfNtnpHHa1UgfYQgeTy4el3pGuxB4YpN2YCFl074efcPCQU3LzwiMsbAQHOS3jwhfumesz-JGJrsfyxgLPr31HBmsswFZpDtjoVpUWFM8J7wHcdO1zdvlnACoRdeIYUTNibXUpRf80ONyt1PXalttPuHJpFgcR5EIXdSNVhAj4yNZEZTa0ot2TE7cxWsbNlP0U7UNRhxMAifcMbENf85nUtxrFdGDaMeQg8V_KN61_FELDhiI0Q_N8LhcWyc18Eyr-r153Gs6JF3uA02HUkKKRM5nzMJrywO5syHBvnSb80Wg2tjIiKJvwYE0ZY7bOHiv6Gbr6fX-hgWMTJ4777mClRdCJ5p0Q4eAvRskgkNwmavuw6lw1QwQwpVepPGl4GpbmZMTm6QXBrGMqcdUMrDuzKCgMoAZV7yhL8gk=

那么我们可以调整一下hook脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function showStacks() {
Java.perform(function () {
console.log(Java.use("android.util.Log").getStackTraceString(
Java.use("java.lang.Throwable").$new()
));
})
}

function hookStrBuilder() {
// 获取 StringBuilder 类并定义需要 Hook 的方法名
var stringBuilderClass = Java.use("java.lang.StringBuilder");
stringBuilderClass.toString.implementation = function () {
var res = this.toString.apply(this, arguments)
if (res.includes('zqkd_param')) {
showStacks()
console.log('tostring is called ', res)
}
// console.log('tostring is called ', res)
return res
}
}

如果字符串中包含zqkd_param关键字则打印堆栈信息,我们再次触发接口获取到对应的日志信息如下:

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
tostring is called  https://kandian.wkandian.com/v3/article/lists.json?zqkd_param=wXU6PBNNsHKc=Rse1ImYBpFW8PwGnvfcNJa5Yg4UIJOenwoUuu7mUCt0kEXDj5ACzfcISZdrlvya9h9ijWacMRP8N7qvjYgQNzL45veAD9SZlcNH7QJiW-AyOu-HKB7OjeSRZjvPBBbhhbTTyw1eCHnMU1uJ5rY0g4DsUz7lEmml-PRkpa-F1BmjpS2U6j9kmZC3j04W40ZV6Btru9WfGSEtyo55F50BGtVWw4gDRYBrqDVK3R4yTIlg4WcMjD_UWWS_LqaLpL-LgBIdx8PpPfoAqmkM6cOo-DdD-Sb5IZNA_-aRQJVT_RoscdZPNjM8u5Si_x_MSUwe16vJMr7rCd3_D5i9lNAQDlqAGfEkcgPQkWll25ietvQOJqZgTdv9L0Ko85-u7yPOwliw2rStygqCQXqOZ4j0m2UBpRoMQNJyGNO1tuK6zl-iqsCg-G7ChY15A6rAZhJzxBQ-6UTv_BLRrpRs-bQ2oV_eQif_2zDq4BvFsJ36xpfTrmu0eoclncG-Qmx8kyakYqk2Q_9TKO517I3G3P5hIfK4pPb0GDzgZH2fQ9VtkaWWMdHyCqcxlKRBd0lvQBqE8Gs7LivC1bVg1iCZNXQhiDUzjEUOjopcpoWullznXXRsje7qK87rQpKFKP85n24ppAbmBrT5kgnGkd8s9AvBM5yK-yMq3sDyhU-kJIgDDnKFIZM6dK9tFZYNuDH3SUw0CZ607P0CUz1X-CPmPD3AyHfFJyjUe_wDCwxmbsweN_TnUeP57IA4g-HHwOnEcBwS3LBOthXVepwDCjn5rFk4Jy8-XUAou-gRO7B6o-Tpb3kZcXH7T3ur0C150l-swLgYq1Wliep2WZLLrNAXSMPxzUflN5g5ATLTXCcAPYEmaO6f43GHGActJlY967F23PWQh7bG510CfNtnpHHa1UgfYQgeTy4el3pGuxB4YpN2YCFl074efcPCQU3LzwiMsbAQHOS3jwhfumesz-JGJrsfyxgLPr31HBmsswFZpDtjoVpUWFM8J7wHcdO1zdvlnACoRdeIYUTNibXUpRf80ONyt1PXalttPuHJpFgcR5EIXdSNVhAj4yNZEZTa0ot2TE7cxWsbNlP0U7UNRhxMAifcMbENf85nUtxrFdGDaMeQg8V_KN61_FELDhiI0Q_N8LhcWyc18Eyr-r153Gs6JF3uA02HUkKKRM5nzMJrywO5syHBvnSb80Wg2tjIiKJvwYE0ZY7bOHiv6Gbr6fX-hgWMTJ4777mClRdCJ5p0Q4eAvRskgkNwmavuw6lw1QwQwpVepPGl4GpbmZMTm6QXBrGMqcdUMrDuzKCgMoAZV7yhL8gk=
java.lang.Throwable
at java.lang.StringBuilder.toString(Native Method)
at okhttp3.internal.http.RequestLine.requestPath(RequestLine.java:62)
at okhttp3.internal.http.RequestLine.get(RequestLine.java:40)
at okhttp3.internal.http1.Http1Codec.writeRequestHeaders(Http1Codec.java:128)
at okhttp3.internal.http.CallServerInterceptor.intercept(CallServerInterceptor.java:50)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:147)
at okhttp3.internal.connection.ConnectInterceptor.intercept(ConnectInterceptor.java:45)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:147)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:121)
at okhttp3.internal.cache.CacheInterceptor.intercept(CacheInterceptor.java:93)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:147)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:121)
at okhttp3.internal.http.BridgeInterceptor.intercept(BridgeInterceptor.java:93)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:147)
at okhttp3.internal.http.RetryAndFollowUpInterceptor.intercept(RetryAndFollowUpInterceptor.java:126)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:147)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:121)
at cn.youth.news.network.api.YouthNetworkInterceptor.youthIntercept(YouthNetworkInterceptor.kt:246)
at cn.youth.news.network.api.YouthNetworkInterceptor.intercept(YouthNetworkInterceptor.kt:62)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:147)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:121)
at cn.youth.news.network.api.LogInterceptor.intercept(LogInterceptor.java:219)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:147)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:121)
at okhttp3.RealCall.getResponseWithInterceptorChain(RealCall.java:250)
at okhttp3.RealCall.execute(RealCall.java:93)
at retrofit2.l.a(OkHttpCall.java:186)
at retrofit2.a.a.c.b(CallExecuteObservable.java:45)
at io.reactivex.m.a(Observable.java:12267)
at retrofit2.a.a.a.b(BodyObservable.java:34)
at io.reactivex.m.a(Observable.java:12267)
at io.reactivex.internal.e.e.an$b.run(ObservableSubscribeOn.java:96)
at io.reactivex.internal.g.l.call(ScheduledDirectTask.java:38)
at io.reactivex.internal.g.l.call(ScheduledDirectTask.java:26)
at java.util.concurrent.FutureTask.run(FutureTask.java:266)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
at java.lang.Thread.run(Thread.java:764)
at com.blankj.utilcode.util.z$e$1.run(ThreadUtils.java:1150)

根据日志中的堆栈信息我们可以跟进cn.youth.news.network.api.YouthNetworkInterceptor.youthIntercept这个方法中去看看对应的逻辑

3、jadx分析定位加密位置

搜索进入到youthIntercept方法

getExtraParams方法是一个比较可疑的方法,我们步入进去查看

getExtraParams方法应该是将加密参数添加进TreeMap中去,我们可以hook一下getExtraParams方法最终返回的TreeMap的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function hookGetExtraParams() {
let YouthNetworkInterceptor = Java.use("cn.youth.news.network.api.YouthNetworkInterceptor");
YouthNetworkInterceptor["getExtraParams"].implementation = function (i) {
console.log(`YouthNetworkInterceptor.getExtraParams is called: i=${i}`);
let result = this["getExtraParams"](i);
console.log(`YouthNetworkInterceptor.getExtraParams result=${result}`);
var key = result.keySet() // 这一行获取了Map对象的键集合
var it = key.iterator();
var count = 0
while (it.hasNext()) {
var keystr = it.next()
var valuestr = result.get(keystr)
console.log(`${count + 1}. ${keystr}: ${valuestr}`)
count++
}
return result;
};
}

日志输出:

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
1. ab_client:
2. ab_feature:
3. ab_version:
4. access: WIFI
5. app-version: 4.0.0
6. app_name: zqkd_app
7. app_version: 4.0.0
8. carrier: CHINA MOBILE
9. channel: c4015
10. device_brand: realme
11. device_id: 83790068
12. device_model: RMX3843
13. device_platform: android
14. device_type: android
15. dpi: 640
16. inner_version: 202203070952
17. jssdk_version:
18. language: zh-CN
19. memory: 3
20. mi: 0
21. mobile_type: 1
22. network_type: WIFI
23. oaid:
24. openudid: c71abbf59d5f8b13
25. os_api: 28
26. os_version: PQ3A.190705.05091555 release-keys
27. request_time: 1748938801
28. resolution: 1440x2560
29. rom_version: PQ3A.190705.05091555 release-keys
30. s_ad: tXU6PBNNsHKc=RhyCH9tvHyWred1oAcZFwPDUT1utyFcH
31. s_im: lXU6PBNNsHKc=kBxJn_kMptmu0oSq2gTyPg==DR
32. sim: 1
33. sm_device_id: 2025052611502897c113ea1f79fa4b89bd6ae043101a0d01ab161637167f19
34. storage: 32.73
35. uid: 88523216
36. version_code: 74
37. zqkey: MDAwMDAwMDAwMJCMpN-w09Wtg5-Bb36eh6CPqHualq2jmrCarWayp4myhLJ-3q_OqmqXr6NthJl7mI-shMmXeqDau4StacS3o7GFsoaars-uq4J5fWmEY2Ft
38. zqkey_id: 55b8c6713f25a80e4afb735753d87e14

继续往下看,找到构建请求前的加密结果

步入到encrypt方法中

这里可以看到初始化了一个StringBuilder对象,然后对arrayList中的数据进行循环遍历取值然后进行拼接,拼接成key1=value1&key2=value2这样的字符串,然后将拼接后的字符串传入SecurityHelper.encryptDES方法中进行加密

4、加密算法分析

我们直接hook对应的DES传入的key和data值看看传入加密的字符串是什么

1
2
3
4
5
6
7
8
9
function hookEncryptDes() {
let SecurityHelper = Java.use("cn.youth.news.utils.SecurityHelper");
SecurityHelper["encryptDES"].implementation = function (str, str2) {
console.log(`SecurityHelper.encryptDES is called: str=${str}, str2=${str2}`);
let result = this["encryptDES"](str, str2);
console.log(`SecurityHelper.encryptDES result=${result}`);
return result;
};
}

日志输出:

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
str=6HPjSZFH8RLv4dBj0YlmC2wFUySX, str2=access=WIFI&app-version=4.0.0&app_name=zqkd_app&app_version=4.0.0&carrier=CHINA+MOBILE&channel=c4015&device_brand=realme&device_id=83790068&device_model=RMX3843&device_platform=android&device_type=android&dpi=640&inner_version=202203070952&language=zh-CN&memory=3&mi=0&mobile_type=1&network_type=WIFI&openudid=c71abbf59d5f8b13&os_api=28&os_version=PQ3A.190705.05091555+release-keys&phone_sim=1&request_time=1749032877&resolution=1440x2560&rom_version=PQ3A.190705.05091555+release-keys&s_ad=sYdVi_XPUOzA%3D9T1NsxwRsf6yGN2cfL26dQVFZJ4e2aZlhS&s_im=7YdVi_XPUOzA%3DStkU0MGsTl9hM-LhDCDUCQ%3D%3Dez&sim=1&sm_device_id=2025052611502897c113ea1f79fa4b89bd6ae043101a0d01ab161637167f19&storage=32.73&uid=88523216&version_code=74&zqkey=MDAwMDAwMDAwMJCMpN-w09Wtg5-Bb36eh6CPqHualq2jmrCarWayp4myhLJ-3q_OqmqXr6NthJl7mI-shMmXeqDau4StacS3o7GFsoaars-uq4J5fWmEY2Ft&zqkey_id=55b8c6713f25a80e4afb735753d87e14&catid=20030&op=0&behot_time=1749032741251&oid=0&video_catid=1453&sign=0154d6b73a7fee7de3940294328c9648
SecurityHelper.encryptDES result=YdVi_XPUOzA=cmEU2wd-Oc7Ne4apw1WrWujGkpodE-ali3LriI_3CUsbRE_85-fWvvjL_7kT0JI-WFkZakMEukUwuk2cEW6MTxtBn6RNxUkWkuivVHFuLcWk5y2djViVJIpDbO1pftSiZpXK3c5QnACRXz0Htki2PZ6ik6QJfBHyRPO2VmYVIhUfiNM-xuP53uTn7Dg8hMwJP3xacZYA_E6EiJE7U92ufPHECTjD1v9uipzFWSGWrZFWuuZc22xTvQkd6CbaJD8MxmfrYNGujvlwDHJkarxjdH9AT0CffaQD461Jd3elrLBsAKfnj5nut7GX4k5LeCDr3jIVU9hc7avkfhFMALzkBjLtYhTxPwaFOmL56OnnpryMnDdyoYOhN_Gi0RZlmx1RzWoybElXKR9uLxuAkOGnRVKz3Yftg8MxlFLwVCYy5CcQR0F6JiPFbxy_XLIUJhJAGGPFCnohVYlMb3aO8tDEiJqW-1EJnQOw8BsKOJ3BEgS44nHd3ATWFNR7hQ4fFakTJgG4gut2ZtkuSZ5lV6Krjwo18bEMH3O7gNhCUtvpemHLErweMACCbBOemQNOBMEPaRasRQUJlYxXjLyw_HBfRLPdFkR5tEEJWAtd2ff1ikuo2gh8ss1Lf6VsOVTawVrlDjeqFRBTicMw5SEMGaSHuuopLN29kbX5XkBAVNP5A55UhLb2g9tKJV682ZzW5_OO3zb2VzQghMGBlHWA3nZxRbozJbDadVrucusn1l9gbjy7qqc5_jELYOmg97iJR3zqH_7A5Oc9VF-UukNyq7Kvj08xOCN7uxe--FZCTIsK05i_n6ARRexRoHc3wHtmOuzw79tieiYI84osPnus0_Zsf0Huhe8KTNUUq5931ehla338BjIjOci2YJ2Y0NAMzclB_bcsFXXjq1L6X3MbXf2f8o1ABmQiCmq1hsefbFl1nEn5AABOTIdvVcQqr9os2XlMQ5_rsihn4xCr51wG68nHXgYoBZD4tgCwSq63_POFT0DRSDmYfODcE2OkO_lo1_GHvjagNHKCk_xC35OdsNV9zAPfSWrw43-vuZ4HId43nZ6lLsZTK6YhDp53lh4Vm9IDebMgDOtnjVxMyKUMFOctA3tQCbAMHLHojuNOsHxS0eloVzrFMwHSDM435qumcpKR9vDeSZHllicFEnI5hMD8p7UP0sEcG4Elc4Kthu5NgEolIGfPOeWzDELV0svS0cwNGkf4HRrrnEHjIKU3AinWvJzc4PnOMDLdPqEfcbinbqGJztNp8NzMb785zxpuuAK68DS9danKjSgGb6SZi-lmGR65hDQy7y8MoeRspFFqs_0=

将加密字符串转换为python字典
params = {
"access": "WIFI",
"app-version": "4.0.0",
"app_name": "zqkd_app",
"app_version": "4.0.0",
"carrier": "CHINA MOBILE",
"channel": "c4015",
"device_brand": "realme",
"device_id": "83790068",
"device_model": "RMX3843",
"device_platform": "android",
"device_type": "android",
"dpi": "640",
"inner_version": "202203070952",
"language": "zh-CN",
"memory": "3",
"mi": "0",
"mobile_type": "1",
"network_type": "WIFI",
"openudid": "c71abbf59d5f8b13",
"os_api": "28",
"os_version": "PQ3A.190705.05091555 release-keys",
"phone_sim": "1",
"request_time": "1749032877",
"resolution": "1440x2560",
"rom_version": "PQ3A.190705.05091555 release-keys",
"s_ad": "sYdVi_XPUOzA=9T1NsxwRsf6yGN2cfL26dQVFZJ4e2aZlhS",
"s_im": "7YdVi_XPUOzA=StkU0MGsTl9hM-LhDCDUCQ==ez",
"sim": "1",
"sm_device_id": "2025052611502897c113ea1f79fa4b89bd6ae043101a0d01ab161637167f19",
"storage": "32.73",
"uid": "88523216",
"version_code": "74",
"zqkey": "MDAwMDAwMDAwMJCMpN-w09Wtg5-Bb36eh6CPqHualq2jmrCarWayp4myhLJ-3q_OqmqXr6NthJl7mI-shMmXeqDau4StacS3o7GFsoaars-uq4J5fWmEY2Ft",
"zqkey_id": "55b8c6713f25a80e4afb735753d87e14",
"catid": "20030",
"op": "0",
"behot_time": "1749032741251",
"oid": "0",
"video_catid": "1453",
"sign": "0154d6b73a7fee7de3940294328c9648"
}

那么我们就已经确定了需要的参数,其中s_ad、s_im和sign参数是需要我们进一步分析的,request_time是时间戳参数,其他的参数都是固定不变的,接下来直接对算法进行分析

这里对key进行了一个MD5加密,然后对拼接的字符串进行了DES加密,然后将两个字符串拼接得到最终的字符串

RandomValidateCode.getDisturbString方法的逻辑是:

  • 将一个字符添加在字符串前面。
  • 根据该字符的 ASCII 值决定要在末尾添加几个随机字符(0~2 个)

经过以上的操作后就得到了最终的zqkd_param的值

5、加密参数分析

5.1 sign参数分析


回到最开始的位置,发现这里signature方法加密后将signature方法返回的Pair属性的first属性作为Map的键,signature方法返回的Pair属性的second属性作为Map的值
我们直接跟进signature方法

这里我们看到了两处sign的值,这里对i做了判断,根据i的值来决定sign在哪进行返回,,我们直接hook signature方法看看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function hookSignature() {
let YouthNetworkInterceptor = Java.use("cn.youth.news.network.api.YouthNetworkInterceptor");
YouthNetworkInterceptor["signature"].implementation = function (map, i) {
console.log(`YouthNetworkInterceptor.signature is called: map=${map}, i=${i}`);
var key = map.keySet() // 这一行获取了Map对象的键集合
var it = key.iterator();
var count = 0
while (it.hasNext()) {
var keystr = it.next()
var valuestr = map.get(keystr)
console.log(`${count + 1}. ${keystr}: ${valuestr}`)
count++
}
let result = this["signature"](map, i);
console.log(`YouthNetworkInterceptor.signature result=${result}`);
return result;
};
}

日志输出:

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
YouthNetworkInterceptor.signature is called: map=[object Object], i=3
1. ab_client:
2. ab_feature:
3. ab_version:
4. access: WIFI
5. app-version: 4.0.0
6. app_name: zqkd_app
7. app_version: 4.0.0
8. carrier: CHINA MOBILE
9. channel: c4015
10. device_brand: realme
11. device_id: 83790068
12. device_model: RMX3843
13. device_platform: android
14. device_type: android
15. dpi: 640
16. inner_version: 202203070952
17. jssdk_version:
18. language: zh-CN
19. memory: 3
20. mi: 0
21. mobile_type: 1
22. network_type: WIFI
23. oaid:
24. openudid: c71abbf59d5f8b13
25. os_api: 28
26. os_version: PQ3A.190705.05091555 release-keys
27. phone_sim: 1
28. request_time: 1749032385
29. resolution: 1440x2560
30. rom_version: PQ3A.190705.05091555 release-keys
31. s_ad: hYdVi_XPUOzA=9T1NsxwRsf6yGN2cfL26dQVFZJ4e2aZlL
32. s_im: ZYdVi_XPUOzA=StkU0MGsTl9hM-LhDCDUCQ==
33. sim: 1
34. sm_device_id: 2025052611502897c113ea1f79fa4b89bd6ae043101a0d01ab161637167f19
35. storage: 32.73
36. uid: 88523216
37. version_code: 74
38. zqkey: MDAwMDAwMDAwMJCMpN-w09Wtg5-Bb36eh6CPqHualq2jmrCarWayp4myhLJ-3q_OqmqXr6NthJl7mI-shMmXeqDau4StacS3o7GFsoaars-uq4J5fWmEY2Ft
39. zqkey_id: 55b8c6713f25a80e4afb735753d87e14
40. catid: 20030
41. op: 0
42. behot_time: 1749031397776
43. oid: 0
44. video_catid: 1453
YouthNetworkInterceptor.signature result=Pair{sign 9a40f9786b729d500a7b4076a6e6bc9b}

这里可以看到i=3,那么返回sign的位置应该是 return new Pair<>("sign", EncryptUtils.getMD5(sb.toString() + ZQNetUtils.KEY));

那么我们现在需要关注sb参数是怎么样的,我们这里直接对md5算法进行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
33
function bytesToString(arr) {
if (typeof arr === 'string') {
return arr;
}
var str = '',
_arr = arr;
for (var i = 0; i < _arr.length; i++) {
var one = _arr[i].toString(2),
v = one.match(/^1+?(?=0)/);
if (v && one.length == 8) {
var bytesLength = v[0].length;
var store = _arr[i].toString(2).slice(7 - bytesLength);
for (var st = 1; st < bytesLength; st++) {
store += _arr[st + i].toString(2).slice(2);
}
str += String.fromCharCode(parseInt(store, 2));
i += bytesLength - 1;
} else {
str += String.fromCharCode(_arr[i]);
}
}
return str;
}

function hookMd5() {
let EncryptUtils = Java.use("cn.youth.news.utils.EncryptUtils");
EncryptUtils["getMD5"].overload('[B').implementation = function (bArr) {
console.log(`EncryptUtils.getMD5 is called: bArr=${bytesToString(bArr)}`);
let result = this["getMD5"](bArr);
console.log(`EncryptUtils.getMD5 result=${result}`);
return result;
};
}

日志输出:

1
2
3
4
EncryptUtils.getMD5 is called: bArr=access=WIFIapp-version=4.0.0app_name=zqkd_appapp_version=4.0.0behot_time=1749032328595carrier=CHINA MOBILEcatid=20030channel=c4015device_brand=realmedevice_id=83790068device_model=RMX3843device_platform=androiddevice_type=androiddpi=640inner_version=202203070952language=z
h-CNmemory=3mi=0mobile_type=1network_type=WIFIoid=0op=0openudid=c71abbf59d5f8b13os_api=28os_version=PQ3A.190705.05091555 release-keysphone_sim=1request_time=1749032631resolution=1440x2560rom_version=PQ3A.190705.05091555 release-keyss_ad=6YdVi_XPUOzA=9T1NsxwRsf6yGN2cfL26dQVFZJ4e2aZl7s_im=kYdVi_XPUOzA=StkU0MG
sTl9hM-LhDCDUCQ==Dsim=1sm_device_id=2025052611502897c113ea1f79fa4b89bd6ae043101a0d01ab161637167f19storage=32.73uid=88523216version_code=74video_catid=1453zqkey=MDAwMDAwMDAwMJCMpN-w09Wtg5-Bb36eh6CPqHualq2jmrCarWayp4myhLJ-3q_OqmqXr6NthJl7mI-shMmXeqDau4StacS3o7GFsoaars-uq4J5fWmEY2Ftzqkey_id=55b8c6713f25a80e4afb735753d87e14jdvylqchJZrfw0o2DgAbsmCGUapF1YChc
EncryptUtils.getMD5 result=6ae715eb3e9d8dac48e7449f19982101

这里可以看出是将请求参数先根据键名进行排序,排序完成后通过key=value的方式一次拼接,最后再和字符串jdvylqchJZrfw0o2DgAbsmCGUapF1YChc拼接得到最终的字符串,将最终的字符串进行md5加密就得到了最后的sign值

5.2 s_ad和s_im参数分析

我们找到刚开始看到的getExtraParams方法,步入进去查看s_ad和s_im的值是怎么来的

这两个值看起来是获取到一些设备的信息然后通过getSecurityParamsByDES方法进行加密,那么暂时先将这两个参数写死

6、整体加密逻辑梳理

  • 先将请求参数按照key排序拼接为key=value的形式,再和字符串jdvylqchJZrfw0o2DgAbsmCGUapF1YChc进行拼接,将最终的字符串进行md5加密得到sign的值
  • 将请求参数及加密后的sign参数拼接成字符串(key=value&key2=value2的形式),把拼接的字符串进行DES加密
  • 最后通过getDisturbString方法在DES算法加密的值前后随机添加字符串