2.3 京东到家

环境准备

com.jingdong.pdj == 8.28.0

抓包

先抓包,找到请求包

image-20230221114614269

逆向分析

djencrypt

我们发现其中有一大段加密放在 body 里面,Keydjencrypt,因为请求的 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 完成后查看是否成功

image-20230221114936748

效果不错,直接找到了,查看分析最有可能的堆栈

        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 反编译一下,找到堆栈处

image-20230221131634589

源码如下:

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@NcRfUjXn2r5u8xiv 应该是 t7w!z%C*F-JaNdRg ,尝试将 body 放置到解密工具里面试一下,发现解密成功!

image-20230221132100004

加密前的数据是:

{
    "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"
}	

signKeyV1

上面解密完 djencrypt 之后,发现 body 里面还有一个 signKeyV1, 我们接着分析。

我们重新 hook 一下,寻找 signKeyV1 字符串,发现堆栈没怎么变

image-20230221132642180

实际上是因为我们 hook 的是 getBytes 方法,在加密的时候里面就有 signKeyV1, 简单一点,直接分析堆栈,发现里面有一个 base.net.volley.BaseStringRequest.getParamsbase.net.volley.MyStringRequest.getParams ,猜测这个可能很有关系,用于生成参数!

查看源码得知,其实就是我们加密的地方(从这里也可以验证,其实我们的 djencrypt 的加密 )

image-20230221132840317

所以加密前的字符串来源于 this.myParams, 我们接着跟一下,看看 this.myParams 是在哪里赋值的,可以使用查找用例:

image-20230221142415612

就能看到其生成的位置了

image-20230221142502269

可以看到他其实是一个 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);


    }

})
image-20230221150757925

可疑堆栈: base.net.open.RequestEntity.putParam(RequestEntity.java:55) / jd.net.z.b

我们查看源码得知 putParam 其实就是一个 put 方法,继续查看 jd.net.z.b ,找到了真正的加密位置!

image-20230221151126267

定位到加密位置为:

 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 是根据 KSKV 的值来变化的,当两个都为真时, 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;
    };


})
image-20230221152108027
  • KVNDk1NGVlMzI0MjY0NDIyOGI4OGIwYjZlY2NhZDU4NWM=, 后面使用的时候进行了 base64 解密,实际是 4954ee3242644228b88b0b6eccad585c

  • KS^JE2

发现他们都有值,所以requestEntity.sFlagfalse, 应该是走的下面的算法,也就是:

 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;
};
image-20230221152728820

所以加密逻辑如下:

               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 加上 &, 分析完之后借助工具尝试分析一下,加密逻辑如下:

  1. 格式化 bodystr1

  2. base64 解密 KSsecretKey

  3. 使用 hamc sha256 方法加密 str1secretKeyKey, 加密生成 str2

  4. signKeyV1 就是 str2 + KV

image-20230221155026360
image-20230221155043611

KS & KV

查看源码发现,两个值的初始化都是空,查找用例,找到赋值的地方

image-20230221155617263

尝试跟踪源码发现好像是在一个下面的函数进行初始化的,并且只会初始化一次

image-20230221155727890

因此我们 Hook 一下,看看是不是我们想要的

image-20230221160009904

打开发现为 没有 Hook 到,所以重启一下 app 试试

image-20230221161637088
image-20230221161707296

其实根据对战就能看出来了,这些参数是从响应中拿到的!我们可以搜索抓包软件,就能找到

image-20230221161812235
image-20230221161819540

最后更新于

这有帮助吗?