某大厂生鲜超市加密协议分析
## 接口抓包 分析### 工具
**Charles**
关于charles的使用可翻阅我之前的charles专题文章
### 测试手机
**Nexus 5x**
**郑重声明,本文只分享思路,不做它用,为保护案例商家安全隐私,敏感信息用xxx代替**
#### 接口:附近可服务的门店
curl
所有接口调用url均为(https://colour.xxxxxxx.com/api), 通过postbody参数functionId控制获取具体的数据
```sql
curl -H 'Host: colour.xxxxxxx.com' -H 'x-mlaas-at: wl=0' -H 'user-agent: xxxxxxxapp_android' -H 'content-type: application/x-www-form-urlencoded; charset=utf-8' --data-binary "commonExtend=&loginType=4&sign=b6beeee33ad4142cc54f3e55a045fbb1c70ecdfdbffa985b559cc36797d20357&screen=1794*1080&d_brand=LGE&body=%7B%22commonExtend%22%3A%22%22%2C%22data%22%3A%7B%22lon%22%3A%22120.02877%22%2C%22lat%22%3A%2230.278442%22%7D%2C%22appName%22%3A%22xxxxxxx%22%2C%22screen%22%3A%221794*1080%22%2C%22lon%22%3A%22120.143936%22%2C%22platformId%22%3A%221%22%2C%22clientVersion%22%3A%223.6.4%22%2C%22storeId%22%3A%22232686%22%2C%22recommendSwitch%22%3A%22true%22%2C%22eu%22%3A%2275B6364667C69667%22%2C%22fv%22%3A%220333461727947597%22%2C%22osVersion%22%3A%228.1.0%22%2C%22partner%22%3A%22huawei%22%2C%22v%22%3A2%2C%22tenantId%22%3A%221%22%2C%22client%22%3A%22android%22%2C%22clientVersionBuild%22%3A%222110251117%22%2C%22model%22%3A%22Nexus5X%22%2C%22networkType%22%3A%22wifi%22%2C%22brand%22%3A%22LGE%22%2C%22lat%22%3A%2230.323437%22%7D&clientVersion=3.6.4&eu=75B6364667C69667&fv=0333461727947597&d_model=Nexus5X&functionId=xxxxxxx_platform_address_getPosition&t=1636957653670&partner=huawei&osVersion=8.1.0&build=2110251117&appid=****fresh_APP&client=xxxxxxx_android&lang=zh_CN&networkType=wifi" --compressed 'https://colour.xxxxxxx.com/api'
```
postBody URL解码后
```sql
commonExtend=&loginType=4&sign=b6beeee33ad4142cc54f3e55a045fbb1c70ecdfdbffa985b559cc36797d20357&screen=1794*1080&d_brand=LGE&body={"commonExtend":"","data":{"lon":"120.02877","lat":"30.278442"},"appName":"xxxxxxx","screen":"1794*1080","lon":"120.143936","platformId":"1","clientVersion":"3.6.4","storeId":"232686","recommendSwitch":"true","eu":"75B6364667C69667","fv":"0333461727947597","osVersion":"8.1.0","partner":"huawei","v":2,"tenantId":"1","client":"android","clientVersionBuild":"2110251117","model":"Nexus5X","networkType":"wifi","brand":"LGE","lat":"30.323437"}&clientVersion=3.6.4&eu=75B6364667C69667&fv=0333461727947597&d_model=Nexus5X&functionId=xxxxxxx_platform_address_getPosition&t=1636957653670&partner=huawei&osVersion=8.1.0&build=2110251117&appid=****fresh_APP&client=xxxxxxx_android&lang=zh_CN&networkType=wifi
```
接口正确返回 responseBody
```javascript
{
"code": "0",
"success": true,
"msg": null,
"data": {
"success": true,
"businessCode": 0,
"msg": null,
"type": 1,
"locationInfo": {
"addressExt": "浙江省杭州市余杭区",
"addressSummary": "浙江省杭州市",
"storeId": null,
"lat": "30.278442",
"lon": "120.02877",
"testShop": false
},
"defaultAddress": null,
"tenantShopInfoList": [{
"storeId": 232xxx,
"storeName": "华东****鲜云超",
"storeAddress": "江东中路与江东门北街交汇处",
"promiseInfo": "最快30分钟达 | 230.96KM",
"tenantDesc": "",
"businessInfo": "",
"tenantInfo": {
"tenantId": 1,
"tenantName": "****鲜",
"bigLogo": "http://img12.360buyimg.com/freshapp/jfs/t1/144864/26/9153/27011/5f6ae507E9dfc96a5/fc2f58d77bcbf2cd.png",
"smallLogo": "http://img12.360buyimg.com/freshapp/jfs/t1/143548/8/13335/6626/5fa4affbE87f4ded3/f46b57081818d3ba.png",
"circleLogo": "http://img12.360buyimg.com/freshapp/jfs/t1/140501/2/9085/6589/5f6ae50bE0508f49c/ae52b1fc1dc2aa59.png",
"contactTel": "4006068768",
"supportGiftCard": false,
"supportEmployeeCard": false,
"supportInvoiceCenter": false,
"supportBalance": false,
"clientInfo": null
},
"lon": "118.737681",
"lat": "32.036757",
"valid": true,
"freeBuy": false,
"delivery": false
}, {
"storeId": 196243,
"storeName": "华中****鲜云超",
"storeAddress": "光谷保利广场",
"promiseInfo": "最快30分钟达 | 539.52KM",
"tenantDesc": "",
"businessInfo": "",
"tenantInfo": {
"tenantId": 1,
"tenantName": "****鲜",
"bigLogo": "http://img12.360buyimg.com/freshapp/jfs/t1/144864/26/9153/27011/5f6ae507E9dfc96a5/fc2f58d77bcbf2cd.png",
"smallLogo": "http://img12.360buyimg.com/freshapp/jfs/t1/143548/8/13335/6626/5fa4affbE87f4ded3/f46b57081818d3ba.png",
"circleLogo": "http://img12.360buyimg.com/freshapp/jfs/t1/140501/2/9085/6589/5f6ae50bE0508f49c/ae52b1fc1dc2aa59.png",
"contactTel": "4006068768",
"supportGiftCard": false,
"supportEmployeeCard": false,
"supportInvoiceCenter": false,
"supportBalance": false,
"clientInfo": null
},
"lon": "114.410486",
"lat": "30.490744",
"valid": true,
"freeBuy": false,
"delivery": false
}],
"nearStore": false,
"fix": false,
"fixLat": null,
"fixLon": null
},
"extMap": {}
}
```
接口错误返回:接口有调用时效,会检测时间戳参数t,时效性为五分钟,五分钟后再次调用返回异常
```javascript
{"code":"1","echo":"invalid signature"}
```
接口分析得知,有三个加密参数 sign eu fv
## 逆向分析
### 工具
**jadx-反编译**
**frida+objection 动态调试**
### sign加密分析
sign从字符串特征和长度来看,看起来像sha256
jadx打开apk搜索关键字“HmacSha256”,看到加密HmacSha256搜选出多个
!(https://gitee.com/lixiaolevae/tuchuang/raw/master/20211118200627.png)
frida+objection跟踪调用入参
需要objection个个追踪然后和抓包得到的sign比对,最终确定调用的为 com.xxx.common.http.GatewaySignatureHelper.HMACSHA256
!(https://gitee.com/lixiaolevae/tuchuang/raw/master/20211118200712.png)
!(https://gitee.com/lixiaolevae/tuchuang/raw/master/20211118200745.png)
objection追踪
```javascript
android hooking watch class_method com.xxx.common.http.GatewaySignatureHelper.HMACSHA2
56--dump-args --dump-return --dump-backtrace
```
--dump-backtrace追踪调用方法栈
```shell
(agent) Called com.xxx.common.http.GatewaySignatureHelper.HMACSHA256([B, [B)
(agent) Backtrace:
com.xxx.common.http.GatewaySignatureHelper.HMACSHA256(Native Method)
com.xxx.common.http.GatewaySignatureHelper.signature(TbsSdkJava:60)
com.xxx.common.http.HttpRequest.paramHandler(TbsSdkJava:108)
com.xxx.common.http.HttpRequest.add(TbsSdkJava:9)
com.xstore.****fresh.modules.search.SearchRequest.getWareInfosIcon(TbsSdkJava:11)
com.xstore.****fresh.modules.productdetail.utils.GetWareInfoIconUtils.getWareInfoMsg(TbsSdkJava:7)
com.xstore.****fresh.modules.category.menulist.NewProductCategoryFragment.setListView(TbsSdkJava:34)
com.xstore.****fresh.modules.category.menulist.NewProductCategoryFragment.initView(TbsSdkJava:32)
com.xstore.****fresh.modules.category.menulist.NewProductCategoryFragment.onCreateView(TbsSdkJava:3)
androidx.fragment.app.Fragment.performCreateView(TbsSdkJava:4)
androidx.fragment.app.FragmentStateManager.createView(TbsSdkJava:15)
androidx.fragment.app.FragmentStateManager.moveToExpectedState(TbsSdkJava:23)
androidx.fragment.app.FragmentManager.executeOpsTogether(TbsSdkJava:34)
androidx.fragment.app.FragmentManager.removeRedundantOperationsAndExecute(TbsSdkJava:10)
androidx.fragment.app.FragmentManager.execPendingActions(TbsSdkJava:4)
androidx.fragment.app.FragmentManager$5.run(TbsSdkJava:1)
android.os.Handler.handleCallback(Handler.java:790)
android.os.Handler.dispatchMessage(Handler.java:99)
android.os.Looper.loop(Looper.java:164)
android.app.ActivityThread.main(ActivityThread.java:6494)
java.lang.reflect.Method.invoke(Native Method)
com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:438)
com.android.internal.os.ZygoteInit.main(ZygoteInit.java:807)
(agent) Arguments com.xxx.common.http.GatewaySignatureHelper.HMACSHA256(, )
(agent) Return Value: b196066d6b926a9e032ea9ae1b0a52a048b03ac063ad55bfbc3fca0fe88959c6
```
上游重要的方法为这三个:
com.xxx.common.http.GatewaySignatureHelper.HMACSHA256(Native Method)
com.xxx.common.http.GatewaySignatureHelper.signature(TbsSdkJava:60)
com.xxx.common.http.HttpRequest.paramHandler(TbsSdkJava:108)
追signature
```shell
android hooking watch class_method com.xxx.common.http.GatewaySignatureHelper.signature--dump-args --dump-return
```
多次触发抓包发现调用的是以下方法
```java
public static String signature(Map<String, String> map, String str) {
if (map == null || map.isEmpty() || TextUtils.isEmpty(str)) {
return null;
}
TreeSet treeSet = new TreeSet();
for (String str2 : map.keySet()) {
treeSet.add(str2);
}
StringBuffer stringBuffer = new StringBuffer();
Iterator it = treeSet.iterator();
while (it.hasNext()) {
String obj = it.next().toString();
String str3 = map.get(obj);
if (DEBUG) {
String str4 = TAG;
Log.d(str4, "sorted key : " + obj + ", value : " + str3);
}
if (!TextUtils.isEmpty(str3)) {
stringBuffer.append(str3);
stringBuffer.append("&");
}
}
String stringBuffer2 = stringBuffer.toString();
if (stringBuffer2.endsWith("&") && stringBuffer2.length() > 1) {
stringBuffer2 = stringBuffer2.substring(0, stringBuffer2.length() - 1);
}
if (DEBUG) {
String str5 = TAG;
Log.d(str5, "raw signature param str : " + stringBuffer2);
}
return HMACSHA256(strToByteArray(stringBuffer2), strToByteArray(str));
}
```
多次触发调用后,可确定第二个参数str是加密盐值,且为恒定值:**fa5010c35exxxxxxx40060d65d3f3801 **
第一个参数是map,objection只显示为,无法显示其具体kv内容
写个frida hook脚本将map kv打印出来
```javascript
function main() {
console.log("Enter hook js");
Java.perform(function x() {
console.log("Inside Java Perform");
var cls = Java.use("com.xxx.common.http.GatewaySignatureHelper");
cls.signature
.overload('java.util.Map', 'java.lang.String')
.implementation
= function(map, salt) {
console.log("args1: " + map.toString());
console.log("args2: " + salt);
var result = this.signature(map, salt);
return result;
}
})
}
setImmediate(main)
```
```shell
frida -U -f com.xstore.****fresh -l hook_signature.js --no-pause
```
但很遗憾,有反注入检测,执行后app直接重启
也没关系,用笨办法试一下,将postbodyStr按&切开后组装成map,调用signature看看得到的sign是否一致,或者相差多少
```java
package com.*******.hmdd.spider.cmptr.xxxxxxxxxx;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.TreeSet;
public class xxxxxxxxxxTestSign {
public static void main(String[] args) {
String postBody = "commonExtend=&loginType=4&sign=a992b4c32cb9e81500ea37adb4dfbc8a63a661bb3a290c4fdb39abf4d752d4ac&screen=1794*1080&d_brand=LGE&body={\"commonExtend\":\"\",\"data\":{\"lon\":\"120.02877\",\"lat\":\"30.278442\"},\"appName\":\"xxxxxxx\",\"screen\":\"1794*1080\",\"lon\":\"120.02877\",\"platformId\":\"1\",\"clientVersion\":\"3.6.4\",\"storeId\":\"232686\",\"recommendSwitch\":\"true\",\"eu\":\"8646C6D4A785B424\",\"fv\":\"937385A766E41727\",\"osVersion\":\"8.1.0\",\"partner\":\"huawei\",\"v\":2,\"tenantId\":\"1\",\"client\":\"android\",\"clientVersionBuild\":\"2110251117\",\"model\":\"Nexus5X\",\"networkType\":\"wifi\",\"brand\":\"LGE\",\"lat\":\"30.278442\"}&clientVersion=3.6.4&eu=8646C6D4A785B424&fv=937385A766E41727&d_model=Nexus5X&functionId=xxxxxxx_platform_address_getPosition&t=1636963490297&partner=huawei&osVersion=8.1.0&build=2110251117&appid=****fresh_APP&client=xxxxxxx_android&lang=zh_CN&networkType=wifi";
String[] kvStr = postBody.split("&");
Map<String, String> map = new HashMap<>();
for (String kv : kvStr) {
String[] cell = kv.split("=");
if (cell.length == 2) {
map.put(cell, cell);
}
}
String sign = signature(map, SALT);
System.out.println("sign = " + sign);
}
public static final String SALT = "fa5010c35exxxxxxx40060d65d3f3801";
public static String signature(Map<String, String> map, String str) {
if (map == null || map.isEmpty() || str == null || str.length() == 0) {
return null;
}
TreeSet treeSet = new TreeSet();
for (String str2 : map.keySet()) {
treeSet.add(str2);
}
StringBuffer stringBuffer = new StringBuffer();
Iterator it = treeSet.iterator();
while (it.hasNext()) {
String key = it.next().toString();
if ("sign".equals(key)) {
continue;
}
String value = map.get(key);
if (value == null || value.length() == 0) {
continue;
}
stringBuffer.append(value);
stringBuffer.append("&");
}
String stringBuffer2 = stringBuffer.toString();
if (stringBuffer2.endsWith("&") && stringBuffer2.length() > 1) {
stringBuffer2 = stringBuffer2.substring(0, stringBuffer2.length() - 1);
}
return HMACSHA256(strToByteArray(stringBuffer2), strToByteArray(str));
}
private static String HMACSHA256(byte[] bArr, byte[] bArr2) {
try {
SecretKeySpec secretKeySpec = new SecretKeySpec(bArr2, "HmacSHA256");
Mac instance = Mac.getInstance("HmacSHA256");
instance.init(secretKeySpec);
return byte2hex(instance.doFinal(bArr));
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
return null;
} catch (InvalidKeyException e2) {
e2.printStackTrace();
return null;
}
}
public static String byte2hex(byte[] bArr) {
StringBuilder sb = new StringBuilder();
int i = 0;
while (bArr != null && i < bArr.length) {
String hexString = Integer.toHexString(bArr & 255);
if (hexString.length() == 1) {
sb.append('0');
}
sb.append(hexString);
i++;
}
return sb.toString().toLowerCase();
}
public static byte[] strToByteArray(String str) {
if (str == null) {
return null;
}
return str.getBytes();
}
}
```
```java
sign = a992b4c32cb9e81500ea37adb4dfbc8a63a661bb3a290c4fdb39abf4d752d4ac
```
好家伙,运行后发现sign和接口抓包的一毛一样
说明postBody中所有的参数都参与了sign的运算,换句话说sign参数是postBody参数构造的最后一步。
### fv&eu 分析
这两个一致在变化,应该也是加密
搜下代码
!(https://gitee.com/lixiaolevae/tuchuang/raw/master/20211118201231.png)
有多个,xxxcrashreport像是崩溃报告类,排除。其他的一个一个watch追吧
定位到
!(https://gitee.com/lixiaolevae/tuchuang/raw/master/20211118201322.png)
!(https://gitee.com/lixiaolevae/tuchuang/raw/master/20211118201401.png)
!(https://gitee.com/lixiaolevae/tuchuang/raw/master/20211118201438.png)
由于eu和fv一致变化,猜测androidId是随机的,即调用了getRandomString方法,此可以通过hook证明确实调用了getRandomString
!(https://gitee.com/lixiaolevae/tuchuang/raw/master/20211118201513.png)
eu和fv的加工
!(https://gitee.com/lixiaolevae/tuchuang/raw/master/20211118201546.png)
对象EncryptResult只是个简单的封装,含有eu和fv两个参数
那么java很好还原,做做变体即可。
用到的HexUtils如下,还原时照抄就好了
!(https://gitee.com/lixiaolevae/tuchuang/raw/master/20211118201621.png)
## 心得
1. 逆向需要耐心也需要大胆的猜想去不断尝试,同时需要寻求巧妙的验证方式。本例的分析向上和向下的追溯均有,灵活应对
2. 逆向工作会用到的很多好用的工具,平时注意多收集一些好用的工具或博文以事半功倍,本文所用到的工具和相关扩展知识点均贴出了链接,方便读者收藏~
3. 本文旨在分享一些逆向技巧和思路,本文所举case相关敏感已打码略去,读者不可利用本文所述内容进行非法商业获取利益,若执意带来的法律责任由读者自行承担。 学习了不错 学习了不错 支持支持支持 写得很详细,学习了 哇,这个厉害,虽然看了半天JAVA也没看懂{:1_908:}。 写得不错!值得学习一小时 有点意思 这下可以不花钱吃肉蛋奶了 好厉害呀,值得我学习!