2.3 京东到家
最后更新于
最后更新于
com.jingdong.pdj == 8.28.0
先抓包,找到请求包
我们发现其中有一大段加密放在 body
里面,Key
为 djencrypt
,因为请求的 API
其实都有一个 functionId
,所以我尝试定位这个 functionId
, 来防止有太多的 hook
我们还是用 hook getBytes
的方式来 hook
字符串(原因是加密的时候大多数都会调用这个方法)
Java.perform(function () {
// 获取 Java 的字符串
const str = Java.use('java.lang.String');
// 重载 getBytes 方法 (但是我也不知道为什么要这样, 是大佬的经验)
str.getBytes.overload().implementation = function () {
var response = this.getBytes()
var str1 = this.toString();
// 如果 string 里面包含了
if (str1.indexOf("homeSearch/searchSkuResultByTab") >= 0) {
// 输出找到了字符串
console.log("find string:", str1);
// 打印一下堆栈
console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()));
}
return response;
}
})
Hook
完成后查看是否成功
效果不错,直接找到了,查看分析最有可能的堆栈
at java.lang.String.getBytes(Native Method)
at com.jd.jdsdk.security.AesCbcCrypto.encrypt(AesCbcCrypto.java:75)
at base.net.DaojiaAesUtil.encrypt(DaojiaAesUtil.java:15)
at com.jddjlib.http.DJHttpManager.createHttpRequestEntity(DJHttpManager.java:237)
at com.jddjlib.http.DJHttpManager.httpRequest(DJHttpManager.java:130)
at com.jddjlib.http.DJHttpManager.request(DJHttpManager.java:73)
at com.jddjlib.http.DJHttpManager.request(DJHttpManager.java:59)
at com.jingdong.pdj.plunginsearchall.tab.AllHourGoFragment.requestSkuData(AllHourGoFragment.java:1292)
at com.jingdong.pdj.plunginsearchall.tab.AllHourGoFragment.showFirstData(AllHourGoFragment.java:957)
at com.jingdong.pdj.plunginsearchall.tab.AllHourGoFragment.onViewCreated(AllHourGoFragment.java:240)
at androidx.fragment.app.Fragment.performViewCreated(Fragment.java:2987)
at androidx.fragment.app.FragmentStateManager.createView(FragmentStateManager.java:546)
at androidx.fragment.app.FragmentStateManager.moveToExpectedState(FragmentStateManager.java:282)
at androidx.fragment.app.FragmentManager.executeOpsTogether(FragmentManager.java:2189)
at androidx.fragment.app.FragmentManager.removeRedundantOperationsAndExecute(FragmentManager.java:2100)
at androidx.fragment.app.FragmentManager.execSingleAction(FragmentManager.java:1971)
at androidx.fragment.app.BackStackRecord.commitNow(BackStackRecord.java:305)
at androidx.viewpager2.adapter.FragmentStateAdapter.placeFragmentInViewHolder(FragmentStateAdapter.java:341)
at androidx.viewpager2.adapter.FragmentStateAdapter.onViewAttachedToWindow(FragmentStateAdapter.java:276)
at androidx.viewpager2.adapter.FragmentStateAdapter.onViewAttachedToWindow(FragmentStateAdapter.java:67)
at androidx.recyclerview.widget.RecyclerView.dispatchChildAttached(RecyclerView.java:7867)
at androidx.recyclerview.widget.RecyclerView$5.addView(RecyclerView.java:893)
at androidx.recyclerview.widget.ChildHelper.addView(ChildHelper.java:107)
at androidx.recyclerview.widget.RecyclerView$LayoutManager.addViewInt(RecyclerView.java:8902)
at androidx.recyclerview.widget.RecyclerView$LayoutManager.addView(RecyclerView.java:8860)
at androidx.recyclerview.widget.RecyclerView$LayoutManager.addView(RecyclerView.java:8848)
at androidx.recyclerview.widget.LinearLayoutManager.layoutChunk(LinearLayoutManager.java:1645)
at androidx.recyclerview.widget.LinearLayoutManager.fill(LinearLayoutManager.java:1591)
at androidx.recyclerview.widget.LinearLayoutManager.onLayoutChildren(LinearLayoutManager.java:668)
at androidx.recyclerview.widget.RecyclerView.dispatchLayoutStep2(RecyclerView.java:4309)
at androidx.recyclerview.widget.RecyclerView.dispatchLayout(RecyclerView.java:4012)
at androidx.recyclerview.widget.RecyclerView.onLayout(RecyclerView.java:4578)
at android.view.View.layout(View.java:19659)
at android.view.ViewGroup.layout(ViewGroup.java:6075)
at androidx.viewpager2.widget.ViewPager2.onLayout(ViewPager2.java:527)
at android.view.View.layout(View.java:19659)
at android.view.ViewGroup.layout(ViewGroup.java:6075)
at android.widget.RelativeLayout.onLayout(RelativeLayout.java:1083)
at android.view.View.layout(View.java:19659)
at android.view.ViewGroup.layout(ViewGroup.java:6075)
at android.widget.FrameLayout.layoutChildren(FrameLayout.java:323)
at android.widget.FrameLayout.onLayout(FrameLayout.java:261)
at android.view.View.layout(View.java:19659)
at android.view.ViewGroup.layout(ViewGroup.java:6075)
at androidx.drawerlayout.widget.DrawerLayout.onLayout(DrawerLayout.java:1231)
at android.view.View.layout(View.java:19659)
at android.view.ViewGroup.layout(ViewGroup.java:6075)
at android.widget.FrameLayout.layoutChildren(FrameLayout.java:323)
at android.widget.FrameLayout.onLayout(FrameLayout.java:261)
at android.view.View.layout(View.java:19659)
at android.view.ViewGroup.layout(ViewGroup.java:6075)
at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1791)
at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1635)
at android.widget.LinearLayout.onLayout(LinearLayout.java:1544)
at android.view.View.layout(View.java:19659)
at android.view.ViewGroup.layout(ViewGroup.java:6075)
at android.widget.FrameLayout.layoutChildren(FrameLayout.java:323)
at android.widget.FrameLayout.onLayout(FrameLayout.java:261)
at com.android.internal.policy.DecorView.onLayout(DecorView.java:761)
at android.view.View.layout(View.java:19659)
at android.view.ViewGroup.layout(ViewGroup.java:6075)
at android.view.ViewRootImpl.performLayout(ViewRootImpl.java:2496)
at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:2212)
at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:1392)
at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:6752)
at android.view.Choreographer$CallbackRecord.run(Choreographer.java:911)
at android.view.Choreographer.doCallbacks(Choreographer.java:723)
at android.view.Choreographer.doFrame(Choreographer.java:658)
at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:897)
at android.os.Handler.handleCallback(Handler.java:790)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:164)
at android.app.ActivityThread.main(ActivityThread.java:6494)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:438)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:807)
这里居然有一个 com.jd.jdsdk.security.AesCbcCrypto.encrypt
,看起来像 Aes
加密,猜测可能就是这个位置,当然也可能是 base.net.DaojiaAesUtil.encrypt
,将 app
反编译一下,找到堆栈处
源码如下:
public class DaojiaAesUtil {
private static final String S_KEY = "J@NcRfUjXn2r5u8x";
private static final String S_PARAMETER = "t7w!z%C*F-JaNdRg";
public static String encrypt(String str) {
String encrypt = AesCbcCrypto.encrypt(str, S_KEY, S_PARAMETER.getBytes());
return encrypt == null ? "" : encrypt;
}
}
可以看到是一个 Aes CBC
模式的加密, Key
应该是 J@NcRfUjXn2r5u8x
, iv
应该是 t7w!z%C*F-JaNdRg
,尝试将 body
放置到解密工具里面试一下,发现解密成功!
加密前的数据是:
{
"appVersion": "8.28.0",
"lat_pos": "28.153399",
"platVersion": "8.1.0",
"channel": "tencent",
"screen": "1440*2392",
"poi": "特来电长沙特来电新建西路桥下充电站",
"body": "{\"displayKey\":\"牛奶\",\"orderType\":\"desc\",\"originKey\":\"牛奶\",\"storeCount\":209,\"pageSize\":\"20\",\"channelType\":-1,\"tabType\":\"1\",\"loadId\":\"f572b945-58f7-4aa8-b236-02997e2882f6\",\"sortType\":\"\",\"needAggrCats\":true,\"page\":1,\"filterList\":[],\"tabParams\":{\"recallType\":1,\"searchAll\":true,\"storeIdList\":[\"11671059\",\"11810115\",\"11970114\",\"12294419\",\"11918532\",\"12518540\",\"11896574\",\"12422536\",\"12432403\",\"12524386\",\"12485218\",\"12332032\",\"12264117\",\"11765625\",\"12403796\",\"12331086\",\"12484988\",\"12331586\",\"12410557\",\"12532081\",\"12128847\",\"12423952\",\"11999626\",\"11959928\",\"12059094\",\"12548338\",\"12626494\",\"12312430\",\"11839247\",\"12079141\",\"12343300\",\"12023687\",\"12119044\",\"12501767\",\"11766313\",\"11813460\",\"12486014\",\"12125568\",\"12128897\",\"12128911\",\"12125556\",\"11030229\",\"11896554\",\"12410544\",\"12410589\",\"12410545\",\"12128855\",\"12002917\",\"12410464\",\"11731994\",\"12301440\",\"12300972\",\"12301206\",\"12513258\",\"12076068\",\"12257750\",\"12423712\",\"12518542\",\"12524388\",\"12217068\",\"12301550\",\"12301082\",\"12301316\",\"12363566\",\"12422537\",\"12301596\",\"12301128\",\"12301362\",\"12264115\",\"12484991\",\"12532080\",\"12423885\",\"12518546\",\"12524392\",\"12432404\",\"12513257\",\"12485219\",\"12264114\",\"12423803\",\"12518544\",\"12264119\",\"12484987\",\"12524390\",\"12301606\",\"12301138\",\"12301372\",\"12423902\",\"12518539\",\"12264118\",\"12484986\",\"12301409\",\"12300941\",\"12301175\",\"12422538\",\"12432400\",\"12485215\",\"12518541\",\"12506530\",\"12566358\",\"12520287\",\"12122748\",\"12501111\",\"11999513\",\"12088796\",\"12609921\",\"12507868\",\"12047939\",\"12609923\",\"12609922\",\"12609919\",\"12609926\",\"12592713\",\"12609925\",\"12530546\",\"12530547\",\"12285465\",\"11802443\",\"12437858\",\"12350372\",\"12350132\",\"12310647\",\"12245067\",\"12598431\",\"12135431\",\"12376817\",\"11914975\",\"11910619\",\"12040396\",\"12122505\",\"12186817\",\"11999811\",\"12215659\",\"12542142\",\"12233103\",\"12423203\",\"12228097\",\"12210239\",\"11786386\",\"11786402\",\"11910631\",\"11786385\",\"11902121\",\"11786414\",\"12350390\",\"12350371\",\"11922721\",\"12542129\",\"11864371\",\"12350370\",\"11802421\",\"12542121\",\"12437763\",\"11802442\",\"11999813\",\"12350409\",\"12437841\",\"12215655\",\"12215651\",\"12005461\",\"12535835\",\"11999817\",\"12437759\",\"11999784\",\"12148230\",\"12235150\",\"11880108\",\"11907373\",\"12210244\",\"12407654\",\"12075822\",\"12211014\",\"12310637\",\"12454726\",\"12210874\",\"11981115\",\"12583484\",\"12522862\",\"12280801\",\"12620255\",\"12186702\",\"12208474\",\"12509630\",\"12414631\",\"12447860\",\"12592385\",\"12241475\",\"12509338\",\"12609807\",\"11702899\",\"12620175\",\"12625630\",\"11999819\",\"12659566\",\"12581717\",\"12629760\",\"12618221\",\"12659324\",\"12618222\",\"12364983\",\"12620254\",\"12632736\",\"11989056\",\"12516202\",\"12541219\",\"12625806\",\"11981153\",\"12516203\",\"12583483\"]},\"key\":\"牛奶\",\"longitude\":\"112.98453\",\"latitude\":\"28.153399\",\"needPreSell\":true,\"ref\":\"search\",\"ctp\":\"search_results\",\"pageSource\":\"storeListByKey\"}",
"deviceId": "3ec85eadfa9aec291c72588a80418214",
"signNeedBody": "1",
"platCode": "android",
"functionId": "homeSearch/searchSkuResultByTab",
"lng_pos": "112.98453",
"networkType": "UNKNOWN",
"brand": "google",
"subVersion": "8.28.0.3",
"androidId": "6e07fbf8e82a1129",
"lat": "28.153399",
"traceId": "3ec85eadfa9aec291c72588a8041821416769458316161676945857606",
"area": "18_1482_48939_53648",
"lng": "112.98453",
"appName": "Paidaojia",
"pageId": "f572b945-58f7-4aa8-b236-02997e2882f6",
"partner": "tencent",
"t": "1676945857607",
"deviceModel": "Nexus 6P",
"signKeyV1": "FB369D29ABB6AA52B9E1047239EBD349FF03458E5EBCDC8EE95AA92C3A86D70E^Rt2",
"city_id": "1482"
}
上面解密完 djencrypt
之后,发现 body
里面还有一个 signKeyV1
, 我们接着分析。
我们重新 hook
一下,寻找 signKeyV1
字符串,发现堆栈没怎么变
实际上是因为我们 hook
的是 getBytes
方法,在加密的时候里面就有 signKeyV1
, 简单一点,直接分析堆栈,发现里面有一个 base.net.volley.BaseStringRequest.getParams
和 base.net.volley.MyStringRequest.getParams
,猜测这个可能很有关系,用于生成参数!
查看源码得知,其实就是我们加密的地方(从这里也可以验证,其实我们的 djencrypt
的加密 )
所以加密前的字符串来源于 this.myParams
, 我们接着跟一下,看看 this.myParams
是在哪里赋值的,可以使用查找用例:
就能看到其生成的位置了
可以看到他其实是一个 hashMap
,那么我们可以直接 Hook HashMap
,使用以下代码
Java.perform(function () {
// 获取 Java 的 HashMap
var linkerHashMap = Java.use('java.util.HashMap');
// 重载 put 方法
linkerHashMap.put.implementation = function (key, value) {
if (key == "signKeyV1") {
console.log("find key: ", value);
// 打印一下堆栈
console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()));
}
// console.log("key => ", key, " value => ", value)
return this.put(key, value);
}
})
可疑堆栈: base.net.open.RequestEntity.putParam(RequestEntity.java:55) / jd.net.z.b
我们查看源码得知 putParam
其实就是一个 put
方法,继续查看 jd.net.z.b
,找到了真正的加密位置!
定位到加密位置为:
if (requestEntity.sFlag) {
requestEntity.putParam(KEY_NEW_SIGN, k2((NetUtils.getInstance().formatQueryParaMap(requestEntity.getParams(), false, isNewSigin, KEY_NEW_SIGN, SIGNNEEDBODY) + PLACE_HOLDER).getBytes()));
} else {
String formatQueryParaMap = NetUtils.getInstance().formatQueryParaMap(requestEntity.getParams(), false, isNewSigin, KEY_NEW_SIGN, SIGNNEEDBODY);
String str = new String(Base64.decode(KS.getBytes(), 0));
String str2 = KV;
String HMACSHA256 = HMACSHA256(formatQueryParaMap.getBytes(), str.getBytes());
requestEntity.putParam(KEY_NEW_SIGN, HMACSHA256 + str2);
}
查看了一下源码知道 requestEntity.sFlag
是根据 KS
和 KV
的值来变化的,当两个都为真时, requestEntity.sFlag
就为 false
, 我们来 hook
一下看看
Java.perform(function () {
// // 获取 Java 的 HashMap
// var linkerHashMap = Java.use('java.util.HashMap');
// // 重载 put 方法
// linkerHashMap.put.implementation = function (key, value) {
// if (key == "signKeyV1") {
// console.log("find key: ", value);
// // 打印一下堆栈
// console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()));
// }
// // console.log("key => ", key, " value => ", value)
// return this.put(key, value);
// }
let z = Java.use("jd.net.z");
z["b"].implementation = function (obj) {
console.log('b is called' + ', ' + 'obj: ' + obj);
let ret = this.b(obj);
console.log('b ret value is ' + ret);
console.log('KS ' + this.KS.value);
console.log('KV ' + this.KV.value);
return ret;
};
})
KV
: NDk1NGVlMzI0MjY0NDIyOGI4OGIwYjZlY2NhZDU4NWM=
, 后面使用的时候进行了 base64
解密,实际是 4954ee3242644228b88b0b6eccad585c
KS
: ^JE2
发现他们都有值,所以requestEntity.sFlag
为 false
, 应该是走的下面的算法,也就是:
String formatQueryParaMap = NetUtils.getInstance().formatQueryParaMap(requestEntity.getParams(), false, isNewSigin, KEY_NEW_SIGN, SIGNNEEDBODY);
String str = new String(Base64.decode(KS.getBytes(), 0));
String str2 = KV;
String HMACSHA256 = HMACSHA256(formatQueryParaMap.getBytes(), str.getBytes());
requestEntity.putParam(KEY_NEW_SIGN, HMACSHA256 + str2);
}
分析找到了加密前的数据准备
public static String a(Map<String, String> map, boolean z, boolean z2, String str, String str2) {
String str3;
JSONObject parseObject;
try {
ArrayList arrayList = new ArrayList(map.entrySet());
Collections.sort(arrayList, new NetComparator());
StringBuffer stringBuffer = new StringBuffer();
for (int i = 0; i < arrayList.size(); i++) {
Map.Entry entry = (Map.Entry) arrayList.get(i);
if (entry.getKey() != null && !"".equals(entry.getKey()) && !TextUtils.isEmpty((CharSequence) entry.getValue())) {
String str4 = (String) entry.getKey();
String str5 = (String) entry.getValue();
if (z) {
str5 = URLEncoder.encode(str5, "UTF-8").replace(MqttTopic.SINGLE_LEVEL_WILDCARD, "%20");
}
if (z2 && UrlTools.BODY.equals(str4) && (parseObject = JSON.parseObject(str5)) != null) {
String a2 = a(parseObject);
if (!TextUtils.isEmpty(a2)) {
if (!a2.endsWith(ContainerUtils.FIELD_DELIMITER)) {
a2 = a2 + ContainerUtils.FIELD_DELIMITER;
}
stringBuffer.append(a2);
}
}
if (z2) {
if (!"functionId".equals(str4) && !str.equals(str4) && !str2.equals(str4) && !UrlTools.BODY.equals(str4)) {
str3 = str5 + ContainerUtils.FIELD_DELIMITER;
stringBuffer.append(str3);
}
} else if (!"functionId".equals(str4) && !str.equals(str4)) {
str3 = str5 + ContainerUtils.FIELD_DELIMITER;
stringBuffer.append(str3);
}
}
}
String stringBuffer2 = stringBuffer.toString();
return (stringBuffer2 == null || "".equals(stringBuffer2)) ? stringBuffer2 : stringBuffer2.substring(0, stringBuffer2.length() - 1);
} catch (Exception e) {
throw new RuntimeException(e.getMessage());
}
}
直接 Hook
一下:
let a = Java.use("com.jingdong.pdj.netencryption.a");
a["a"].overload('java.util.Map', 'boolean', 'boolean', 'java.lang.String', 'java.lang.String').implementation = function (map, z, z2, str, str2) {
console.log('a is called' + ', ' + 'map: ' + map + ', ' + 'z: ' + z + ', ' + 'z2: ' + z2 + ', ' + 'str: ' + str + ', ' + 'str2: ' + str2);
let ret = this.a(map, z, z2, str, str2);
console.log('a ret value is ' + ret);
return ret;
};
所以加密逻辑如下:
if (!"functionId".equals(str4) && !str.equals(str4) && !str2.equals(str4) && !UrlTools.BODY.equals(str4)) {
str3 = str5 + ContainerUtils.FIELD_DELIMITER;
stringBuffer.append(str3);
}
只要 map
里面的 key
不为 functionId / signKeyV1 / signNeedBody / body
,就用其 value
加上 &
, 分析完之后借助工具尝试分析一下,加密逻辑如下:
格式化 body
为 str1
base64
解密 KS
为 secretKey
使用 hamc sha256
方法加密 str1
, secretKey
为 Key
, 加密生成 str2
signKeyV1
就是 str2 + KV
查看源码发现,两个值的初始化都是空,查找用例,找到赋值的地方
尝试跟踪源码发现好像是在一个下面的函数进行初始化的,并且只会初始化一次
因此我们 Hook
一下,看看是不是我们想要的
打开发现为 没有 Hook
到,所以重启一下 app
试试
其实根据对战就能看出来了,这些参数是从响应中拿到的!我们可以搜索抓包软件,就能找到