2.1 京东特价版

京东特价版也是极速版,加密算法直接在 java 层,逆向比较简单

环境准备

京东特价版版本: v4.8.2 build2385

包名:com.jd.jdlite

抓包

京东没有对抓包有任何限制,直接就能抓到包,我们抓的是详情页的包

curl 'https://api.m.jd.com/api?functionId=lite_wareBusiness&t=1676628390470&appid=lite-android&clientVersion=4.8.2&build=2385&client=android&partner=360&eid=eidAe956812275saFCcPf4BmQkaSjDvZ+AuVQhQnEQXe8CSEA40cQtNt/ejkSoLBlQz8yTB0R4XvwmVPTH2mvZhgljN+4j1hELgdynSIRzkbWw71EW1i&sdkVersion=29&oaid=A8926B02ADF4394AAF857CC5FB26E3660B1C368609AD6E40D802DCA5AED89FFB&networkType=wifi&ext=%7B%22jdliteAppMode%22%3A%220%22%2C%22prstate%22%3A%220%22%7D&ef=1&ep=%7B%22hdid%22%3A%22JM9F1ywUPwflvMIpYPok0tt5k9kW4ArJEU3lfLhxBqw%3D%22%2C%22ts%22%3A1676627536345%2C%22ridx%22%3A-1%2C%22cipher%22%3A%7B%22area%22%3A%22CV83Cv8yDzu5XzK%3D%22%2C%22d_model%22%3A%22J05PUOnVU0O2CNKm%22%2C%22osVersion%22%3A%22CJK%3D%22%2C%22d_brand%22%3A%22J25vUQn1cm%3D%3D%22%2C%22screen%22%3A%22CtO1EIenCNqm%22%2C%22uuid%22%3A%22DNO0DtrrZJu5YwG0ZQS4ZK%3D%3D%22%2C%22aid%22%3A%22DNO0DtrrZJu5YwG0ZQS4ZK%3D%3D%22%2C%22openudid%22%3A%22DNO0DtrrZJu5YwG0ZQS4ZK%3D%3D%22%7D%2C%22ciphertype%22%3A5%2C%22version%22%3A%221.2.0%22%2C%22appname%22%3A%22com.jd.jdlite%22%7D&sign=106b74150c07311092af331d39b265f65ee2e1b80492ff0b950f6c4a1f49ddca' \
-X POST \
-H 'J-E-H: %7B%22hdid%22%3A%22JM9F1ywUPwflvMIpYPok0tt5k9kW4ArJEU3lfLhxBqw%3D%22%2C%22ts%22%3A1676627536362%2C%22ridx%22%3A-1%2C%22cipher%22%3A%7B%22user-agent%22%3A%22b2jedRHmBzCkCJSkCJjgZQn0YXLmE2PkZRTlaWG7dwVyc2vlbs80BtqkCtjsdWviZM8yCzq1E3DtcwVvbs8nCNqmoNSnDJu7b3ClCJK7%22%7D%2C%22ciphertype%22%3A5%2C%22version%22%3A%221.2.0%22%2C%22appname%22%3A%22com.jd.jdlite%22%7D' \
-H 'jdgs: {"b1":"646e68a4-f839-43fa-847f-781030181220","b2":"2.3.3.2_0","b3":"1.0","b4":"H+vTofVPT7Sa7p9XmgTaIe0IPkzynkNlusSEr+ANNPyfnaYpxVvrRGRaKqlg75iMTpCs92ocY+OnDM1GIZ0WdPafEAa+Wj8j24HVCU6gHBGQb31FNw4y5moRS/Kgrip1jhmeo9PGCHY7T0OCVGT09kq2+pXddvW5740wyWoTjlZd//G9u9xFGNSviWjGj6nhzIjdZ+R/DiRDjJq5sLfjk7KfSxJ+PKtdgcivIiWPMayD+L3VKjIc7ryMOCTQjr7P6+wgThmCFAGdCJyDD6/9M4a8CZgsi5ag8A3U9OLJC/b3CVkY4iENKwwiVoB502LL8qUBVO2gJrmcxBIoXm0whorqG5jO2GiUoYC612D2ut1iP8vWYab0cCK0JYUm2JphxTTN4zzzIaoO1g==","b5":"8ce98367edad2c7c8173db2e41ad899215faad9a","b7":"1676628390483","b6":"75d47de1398e32798e8a4d17ad5dc33142235a8b"}' \
-H 'Connection: keep-alive' \
-H 'Charset: UTF-8' \
-H 'user-agent: okhttp/3.12.1;jdltapp;android;version/4.8.2;build/2385;' \
-H 'Cache-Control: no-cache' \
-H 'Content-Length: 4833' \
-H 'Host: api.m.jd.com' \
-H 'Content-Type: application/x-www-form-urlencoded; charset=UTF-8' \
--cookie 'whwswswws=JD0121e332HIsmxdY5RH167662753705405CHMQe8h5HnEk6XZAAhHMgBbKziQmP4ta0WEvh-xM26mg4xGjfyaDogxSyYXIj_BS9pWP_kMVrq4_tGuBaUZ9xGZxyGRmkfiFlrrozctJv5E0lfs0f5~m0zb2zSavO77iq0X7QaljCd6w_qxiP0lPSMX93yZQs1GPlw8U8j7T6dY2RmHFL6sY69ke5YTGnqxscxUAVEo8gH08RJ2oL0HHja0lQ0mYGHzAOZFxWClagUmZgqdv7LkHOcVx1VMgaNOSE0zDffWIeJnx7LkFucPIJuvMynSvLA8;unionwsws={"devicefinger":"eidAe956812275saFCcPf4BmQkaSjDvZ+AuVQhQnEQXe8CSEA40cQtNt\/ejkSoLBlQz8yTB0R4XvwmVPTH2mvZhgljN+4j1hELgdynSIRzkbWw71EW1i","jmafinger":"JD0121e332HIsmxdY5RH167662753705405CHMQe8h5HnEk6XZAAhHMgBbKziQmP4ta0WEvh-xM26mg4xGjfyaDogxSyYXIj_BS9pWP_kMVrq4_tGuBaUZ9xGZxyGRmkfiFlrrozctJv5E0lfs0f5~m0zb2zSavO77iq0X7QaljCd6w_qxiP0lPSMX93yZQs1GPlw8U8j7T6dY2RmHFL6sY69ke5YTGnqxscxUAVEo8gH08RJ2oL0HHja0lQ0mYGHzAOZFxWClagUmZgqdv7LkHOcVx1VMgaNOSE0zDffWIeJnx7LkFucPIJuvMynSvLA8"};' \
-d 'body=%7B%22abTest800%22%3Atrue%2C%22adClickUrl%22%3A%22http%3A%2F%2Fccc-x.jd.local%2Fdsp%2Fnc%3Fext%3DaHR0cDovL2l0ZW0ubS5qZC5jb20vcHJvZHVjdC8xMDA2MDk0Nzg4MDk0My5odG1s%26log%3DvwbJpcnXGUp2-ZoL6bia-KN4XK0a9TTfUYy_pkJraXHzxOj3Z0_L721Wg3_wp6BFSEQZJwLBHSwghiLl8eT1UdC6EkGJuLzrPci6m6LSMKgp4L04Uu7V87NBn969-rBEwbDKiW2IRAUzS7BbEcqism5aLkro2et3NYj1WG1d9rlpdxbVQS9-zvoAAzygD1U9R8-nRaeUhQovzghNELeQyLQxw0dEX7mIzv7Zjbi-rv9QZ4qQ0ZB1JdGNMwBSUJRgmDtrfMkVoyzmwh1fr-fDVzGHo14FUsDiq2kcy6aFzJhUR2P1iTsw_PhoMeoY3AKj0m8sQvu0BhMgu3kD5rmX-hU4KWVNNKxyzmYEYkwDkkciBeKrTRs4N_4XIufC3BfA-Y3SFiWsiY2PO4R6uroP-sHUcdKZK9KQAJj_qvjMmAmcJoIpBSKSKIt_BqYU9_4QgWl7kMA7etsbRkB5XZ87rljDQT-XfwILTvGILNsW9Fp-cWtNeE5BzumwmjqL-8DjJfJFNI542ro46CvckAhffAAnC2xrkO9CAoyra0po2N1eZ8P36o1gXwkfClNbsBadpSjsED1PB_VxPMO-YIkG2n8rXSJ4hbDnRYqfwczQD8r8emEEkUtIStNYd9DRaJTkau3n6tEKXWpwNwIjRXYM3XkpiyYoXSQH_iHNxvYCUP-HuG5P9PwCnTS5GljImoQqZJiz-fpKunihNRJl5UuyyDVeNASKWJKgUdGAZh5jck5BdGDnrzeiarUwUdQ5M8HjNPr2krg2raWFvvTRrPYZOq8MwOZgNuz4nHTpQ85HjY6b3UBqjq5bJMMurxe_49RFQh_OMvLB8BBgLBhZ_o73zlfS024CfBf6JTINZ16FVxwW_nIdMIvKw44pAlXUAVknaCEb2McNHESklV0vQK7FSjuBqiah2snQ2FeIW5NfjLzAIp-H3k5gX6eV8sVXOkhv8jnC2G1PHoUXvtrm0JcAOjRgLyqK2ii6DM85WNoNUsoSgCUqeWhXQeo1-xGLJudjooFjnlSvCBDpr570ygvSESbhoBPw5Fnuno-HPHcvDDiMNupZVsJ5Ugxg1C22KgdCDzrvk3F2HD6MwKPrkR3VZ24QinRDOfeIWsJsxNLMwidojEIgJHm2IVUyjiz-4URFqoOPcCSWw0MbtS9dxK8qlWxPj2euipJ8AQ_ZZ5Wsi8lQpG6YchpkzpoSERQv1WhSJzPab2bi9lhih3PijnWQn8B9ivj9EFYP6KWeTmcTszoar-CRa2BZ8dlt7S_loapTIIN7bhYO0VxQsdCmiP1gkzgE65CySyjiDd5kOMEfjyxQsV51-svQdQ_mUxkpAuHqXiEfFUC5g29CpyPEx0jt13-_CGASkGgd0PWn8EpzkUzLFK1BkQDe4NkV9HTkOHD7hxtH9yQkr_Jxl3bq24TtwykHHpeQTLJI1mj93z9nm_YFGdy2n9__d1B-Tn4Jo8Q4zJBE6NLL_oDsjiOAk0SVZxL6tSTHzRtbJWXcvHf9MHN9N98IXWrEu5pgAmk_cKu6JPbNz17dIVa0o-xUDXheG_xnCaPYVOXi0ythenfWNV395MrTh41yRvCOIyAy35FoUZsKPK1bRj83kSCi4f84mLjS8RSIowKyN2WCHzZi6ClrHkPi7J-Di5mHDE9vFnFUqBUGYANEMNNPwerChJPD8QnfRPG1PokgwaVSJv1I9hpKEMXOnBRGqgq6DWAv0Ho6YaViGSRBfi3REZetyfkNCauiZoak1sC6B3CszYBWjNICe2KMdKAImyTc1HDAj9lGcyXzRTjnDdvd8ORzYFdtvtkNUkTTuuGKqjzYZ8itPFUCHiIw857ndHjl1cOt5hE5EJBFJGda4XrkTTqPB24TcLlOQsqDWzmMN3yJp0xGBJashXLJ81jaz37W1f08vdnm8E3f5DZSiKgj7PzvgO5uy5uCPxE8nz2viAJkHSDWS9XnFUIhP2nRYexpvPrd04lW0MPDB9cXS4j1HhQ-PWuxWf8-4LJtqDimjC3Q01jvi0klv03KD1Mkba3QDU_P08OjhNtnQ6gdi2K1XVC6n9bJBDzwnbruD5Mr_gMNpLQ-Joz2vD-q2gQZBeWt5--ueuoXYbllWC7vR7lDG9X0yhUDKXa8HqQqpVHoOYkeU7n5kInMztrBXq2lLMtL6--DDvN4VuQo9jRCchA7FuDsKVZPm-z8_hvYSAuZrMlkj6ruDueAtVq8BtKMiyG6s836YGgOQGD9SZ7TcxauPzdA89NawWcxnqDQF1hPrSmVu3pvtMKG8-oUnSi6P_0TFtWX5UawGvLFWUZhC5RhpBSK6aTbyg0Tj1W5m7ALDdEEs73dwDVOoUhm97D1jsnjADT49okpo1ZIDD2bUCNLrRBESqGuav8XH_ESV_t2dYJ6pSwBWIJ4zI71PNF8LF_zNUQCCHdHy0X4v1OX_2tVn9yiI-rFgScNDraMMTJIplMIaRNirLXxEHunELxITF6JTP_aGk1T1XsY9BnYu6QWiVNLooyAsA%26v%3D404%26clicktype%3D1%26%22%2C%22brand%22%3A%22OnePlus%22%2C%22csku%22%3A%2210060947880943%22%2C%22eventId%22%3A%22QHome_Productid%22%2C%22fromType%22%3A0%2C%22isDesCbc%22%3Atrue%2C%22latitude%22%3A%220.0%22%2C%22lego%22%3Atrue%2C%22longitude%22%3A%220.0%22%2C%22model%22%3A%22ONEPLUS+A6000%22%2C%22pluginVersion%22%3A20400%2C%22plusClickCount%22%3A0%2C%22provinceId%22%3A%221%22%2C%22skuId%22%3A%2210060947880943%22%2C%22source_type%22%3A%22unifiedRecommend%22%2C%22source_value%22%3A%22%7B%5C%22discountPoint%5C%22%3A%5C%221%5C%22%2C%5C%22broker_info%5C%22%3A%5C%22eyJyZXF1ZXN0X2lkIjoiMzYzZjlmYzEtY2NhNS00YWFmLWI2NDgtODgzZTU5ZjUzOTIxIiwicGFsYW50aXJfZXhwaWRzIjoiWl5SXkF8TUlYVEFHX1peUl5BUixaXlJeQV9OTl9DTF9SLFpeUl5BX05OX1hGMDAxX1IsWl5SXkFfTk5fRUNPX1IsWl5SXkFfTk5fTkhfUixaXlJeQV9OTl9TUERfUixaXlJeQV9OTl9TUEQyX1IsWl5SXkFfTk5fRlJVX1IsWl5SXkFfTk5fSkdYQ1hfUixaXlJeQV9OTl9KR1pESl9SLFpeUl5BX05OX0pHRFNKX1IsWl5SXkFfTk5fQVBQQl9SLG1ifEcqWl5SXkFfTk5fQ0xfTDIzNTIxIiwic2t1X2lkIjoxMDA2MDk0Nzg4MDk0MywicCI6NjE5NTA1LCJzb3VyY2UiOiIxIn0%3D%5C%22%2C%5C%22reason%5C%22%3A%5C%22-100%5C%22%2C%5C%22psource%5C%22%3A%5C%229%5C%22%2C%5C%22source%5C%22%3A%5C%221%5C%22%2C%5C%22sid%5C%22%3A%5C%22654175bf-a2a1-4118-910b-fca4dbbe28c2%5C%22%2C%5C%22pricetype%5C%22%3A%5C%22-100%5C%22%2C%5C%22saleinfo%5C%22%3A%5C%22-100%5C%22%2C%5C%22flow%5C%22%3A%5C%2266%5C%22%2C%5C%22foot%5C%22%3A%5C%22-100%5C%22%2C%5C%22index%5C%22%3A%5C%225%5C%22%2C%5C%22benefit%5C%22%3A%5C%22-100%5C%22%2C%5C%22acq_address%5C%22%3A%5C%22%5C%22%2C%5C%22p%5C%22%3A%5C%22619505%5C%22%2C%5C%22footname%5C%22%3A%5C%22-100%5C%22%2C%5C%22reasonid%5C%22%3A%5C%22-100%5C%22%2C%5C%22bigsale%5C%22%3A%5C%220%5C%22%2C%5C%22playshow%5C%22%3A%5C%22-100%5C%22%2C%5C%22page%5C%22%3A%5C%221%5C%22%2C%5C%22rcvdprice%5C%22%3Anull%2C%5C%22skutype%5C%22%3A%5C%22-100%5C%22%2C%5C%22skuid%5C%22%3A%5C%2210060947880943%5C%22%2C%5C%22jumptype%5C%22%3A%5C%220%5C%22%2C%5C%22promotion%5C%22%3A%5C%22-100%5C%22%2C%5C%22reqsig%5C%22%3A%5C%22810e0a7a0d7f8de1336a38f91fbab1dc35612550%5C%22%2C%5C%22expid%5C%22%3A%5C%22D1%40Lfirst%400205%2CD1%40Lsecond%400300%2CD1%40Lthird%400402%5C%22%7D%22%2C%22uAddrId%22%3A%220%22%7D&' \

可以看到请求包含了一些加密:

image-20230217140852430

Sign

算法定位

首先先尝试使用 r0tracer 调试一下,进入评论页面后看一下当前的 activity

› adb shell dumpsys window | grep mCurrentFocus

  mCurrentFocus=Window{6550c38 u0 com.jd.jdlite/com.jd.lib.productdetail.ProductDetailActivity}

可以知道包名为:com.jd.jdlite, 当前 activitycom.jd.lib.productdetail.ProductDetailActivity

使用 r0tracer 尝试 hook 一下发现 frida 直接崩溃了,所以是有 frida 校验的

image-20230217143450923

询问了大佬得知,需要上魔改版的 frida

Python 的依赖也需要修改,依赖如下:

frida==15.1.28
frida-tools==10.8.0

简单写一个脚本 hook 一下,启动命令为:

frida -f com.jd.jdlite -l test1.js  -H 192.168.0.41:9999 --no-pause

Hook 脚本为:

Java.perform(function () {
    console.log("frida begin ... !")
})
image-20230217154220322
image-20230217154229105

发现 hook 成功,接下来就可以使用 frida 调试了!

先尝试用 r0captre 调试抓包一下,看看能不能找到加密接口的直接调用堆栈,但是发现直接卡死了

image-20230217155516240

那就使用常规的 Hook 方法:

Hook 字符串,当字符串中包含了 sign 的时候,打印堆栈

脚本如下:

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("sign") >= 0) {
            // 输出找到了字符串
            console.log("find string:", str1);

            // 打印一下堆栈
            console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()));
        }

        return response;
    }

})

启动命令如下:

frida -f com.jd.jdlite -l jdlite.js  -H 192.168.0.41:9999 --no-pause		
image-20230217175017770

app 已经正常启动了,此时我们随便点一下详情页,发现已经找到输出的调用堆栈了

image-20230217175348013

通过抓包软件我们也能看到,这是极速版的详情页接口!

find string: POST /api appid=lite-android&build=2385&client=android&clientVersion=4.8.2&d3486b027becc21f68f0ab334f3e4362bbedb500dda05a388beca755da68ca00=&ef=1&eid=eidAe956812275saFCcPf4BmQkaSjDvZ+AuVQhQnEQXe8CSEA40cQtNt%2FejkSoLBlQz8yTB0R4XvwmVPTH2mvZhgljN+4j1hELgdynSIRzkbWw71EW1i&ep=%7B%22hdid%22%3A%22JM9F1ywUPwflvMIpYPok0tt5k9kW4ArJEU3lfLhxBqw%3D%22%2C%22ts%22%3A1676627536345%2C%22ridx%22%3A-1%2C%22cipher%22%3A%7B%22area%22%3A%22CV83Cv8yDzu5XzK%3D%22%2C%22d_model%22%3A%22J05PUOnVU0O2CNKm%22%2C%22osVersion%22%3A%22CJK%3D%22%2C%22d_brand%22%3A%22J25vUQn1cm%3D%3D%22%2C%22screen%22%3A%22CtO1EIenCNqm%22%2C%22uuid%22%3A%22DNO0DtrrZJu5YwG0ZQS4ZK%3D%3D%22%2C%22aid%22%3A%22DNO0DtrrZJu5YwG0ZQS4ZK%3D%3D%22%2C%22openudid%22%3A%22DNO0DtrrZJu5YwG0ZQS4ZK%3D%3D%22%7D%2C%22ciphertype%22%3A5%2C%22version%22%3A%221.2.0%22%2C%22appname%22%3A%22com.jd.jdlite%22%7D&ext=%7B%22jdliteAppMode%22%3A%220%22%2C%22prstate%22%3A%220%22%7D&functionId=lite_wareBusiness&networkType=wifi&oaid=A8926B02ADF4394AAF857CC5FB26E3660B1C368609AD6E40D802DCA5AED89FFB&partner=360&sdkVersion=29&sign=1d493ce95bc7c71d27e6e93da30ee4ff517d567fb16d04573f7d5f77a0ddf119&t=1676627558524
java.lang.Throwable
        at java.lang.String.getBytes(Native Method)
        at com.jd.security.jdguard.a.d.a(JDPreprocessor.java:93)
        at com.jd.security.jdguard.a.d.jJ(JDPreprocessor.java:39)
        at com.jd.security.jdguard.a.a.a(AddSigUtils.java:117)
        at com.jd.security.jdguard.a.a.a(AddSigUtils.java:36)
        at com.jingdong.common.network.JDNetworkDependencyFactory$15.genSign(JDNetworkDependencyFactory.java:563)
        at com.jingdong.jdsdk.network.toolbox.k.a(HttpSettingTool.java:99)
        at com.jingdong.jdsdk.network.toolbox.v.a(ParamBuilderForThirdApp.java:255)
        at com.jingdong.jdsdk.network.toolbox.k.a(HttpSettingTool.java:56)
        at com.jingdong.jdsdk.network.toolbox.HttpGroupAdapter$RequestTask.run(HttpGroupAdapter.java:170)
        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:919)

通过分析堆栈找到最可能是加密入口的地方:

com.jingdong.jdsdk.network.toolbox.v.a(ParamBuilderForThirdApp.java:255)

因为名字里面有一个 ParamBuilder,我们先讲 app 反编译后查看一下源代码(就是直接将 app 拖入 jadx 就可以 ),然后找到该函数

image-20230217175813680

搜索一下 sign 这个字符串,看看哪里赋值了,很容易发现 其实 sign 就是 r 这个变量。

image-20230217175924953

r 的生成规则如下:

String r = h.r(url, str, secretKey);

所以加密函数就是 h.r,直接赋值为 frida 片段

// 跟踪 h.r 函数
let h = Java.use("com.jingdong.jdsdk.network.toolbox.h");
h["r"].implementation = function (str, str2, str3) {
    console.log('=============================');
    console.log('r is called' + ', ' + 'str: ' + str);
    console.log('r is called' + ', ' + 'str2: ' + str2);
    console.log('r is called' + ', ' + 'str3: ' + str3);
    let ret = this.r(str, str2, str3);
    console.log('r ret value is ' + ret);
    return ret;
};

重新 hook 之后找到详情页输出

image-20230217180645019

可以发现加密函数第一个参数是我们请求的 URL, 第二个参数就是 body ,第三个参数是 12aea658f76e453faf803d15c40a72e0, 也就是 sercretKey

算法分析

通过 jadx 可以看到加密函数的源码为:

public static String r(String str, String str2, String str3) {
        if (TextUtils.isEmpty(str)) {
            return str;
        }
        HttpUrl parse = HttpUrl.parse(str);
        if (parse != null) {
            Set<String> queryParameterNames = parse.queryParameterNames();
            LinkedHashSet linkedHashSet = new LinkedHashSet();
            linkedHashSet.addAll(queryParameterNames);
            if (!TextUtils.isEmpty(str2) && !queryParameterNames.contains(JshopConst.JSKEY_JSBODY)) {
                linkedHashSet.add(JshopConst.JSKEY_JSBODY);
            }
            if (linkedHashSet.size() > 0) {
                TreeSet treeSet = new TreeSet();
                Iterator it = linkedHashSet.iterator();
                while (it.hasNext()) {
                    treeSet.add((String) it.next());
                }
                StringBuffer stringBuffer = new StringBuffer();
                Iterator it2 = treeSet.iterator();
                while (it2.hasNext()) {
                    String obj = it2.next().toString();
                    String queryParameter = parse.queryParameter(obj);
                    if (TextUtils.equals(obj, JshopConst.JSKEY_JSBODY) && TextUtils.isEmpty(queryParameter) && !TextUtils.isEmpty(str2)) {
                        queryParameter = str2;
                    }
                    if (OKLog.D) {
                        String str4 = TAG;
                        OKLog.d(str4, "sorted key : " + obj + ", value : " + queryParameter);
                    }
                    if (!TextUtils.isEmpty(queryParameter)) {
                        stringBuffer.append(queryParameter);
                        stringBuffer.append(ContainerUtils.FIELD_DELIMITER);
                    }
                }
                String stringBuffer2 = stringBuffer.toString();
                if (stringBuffer2.endsWith(ContainerUtils.FIELD_DELIMITER) && stringBuffer2.length() > 1) {
                    stringBuffer2 = stringBuffer2.substring(0, stringBuffer2.length() - 1);
                }
                if (OKLog.D) {
                    String str5 = TAG;
                    OKLog.d(str5, "raw signature param str : " + stringBuffer2);
                }
                String format = String.format("&sign=%s", d(cG(stringBuffer2), cG(str3)));
                if (OKLog.D) {
                    String str6 = TAG;
                    OKLog.d(str6, "sign result : " + format);
                }
                return format;
            }
            return "";
        }
        return "";
    }

通过分析源码可以知道加密步骤:

  1. 程序将 URL 内的参数解析出来,并且将 body 合并至一起之后,将其 Key 加入到了 TreeSet 里面,查询了一下 TreeSet 的作用,实际上会默认升序排序,参考文章见:java集合-TreeSet的两种排序规则 - 掘金 (juejin.cn)

  2. 循环拿出来每一个 Key,获取 key 的值添加到 stringBuffer

  3. 添加一个 ContainerUtils.FIELD_DELIMITER, 也就是 &

  4. 遍历完成后将 stringBuffer 转换为 stringBuffer2

  5. 去除 stringBuffer2 末尾的 &

  6. 调用 d(cG(stringBuffer2), cG(str3))) 生成加密,str3 就是 sercretKey

跟进堆栈查看函数 d 的源码如下:

    private static String d(byte[] bArr, byte[] bArr2) {
        try {
            SecretKeySpec secretKeySpec = new SecretKeySpec(bArr2, "HmacSHA256");
            Mac mac = Mac.getInstance("HmacSHA256");
            mac.init(secretKeySpec);
            return byte2hex(mac.doFinal(bArr));
        } catch (InvalidKeyException e2) {
            e2.printStackTrace();
            return null;
        } catch (NoSuchAlgorithmException e3) {
            e3.printStackTrace();
            return null;
        }
    }
	

发现其实就是一个 hamc 方法,加密用的 sha256

算法还原

算法还原如下:

import copy
import hmac
from hashlib import sha256
from urllib import parse


def encrypted(url: str, body: str, secret_key: str = '12aea658f76e453faf803d15c40a72e0'):
   parsed = parse.urlparse(url)
   querys = parse.parse_qs(parsed.query)
   querys = {k: v[0] for k, v in querys.items()}

   data = copy.deepcopy(querys)

   if body and "body" not in querys:
      data.update(body=body)

   str1 = ""
   for key in sorted(data.keys()):
      str1 += data[key] + '&'

   if str1.endswith('&'): str1 = str1[:-1]

   secret_key = secret_key.encode('utf-8')
   data_str = str1.encode('utf-8')
   sign = hmac.new(secret_key, data_str, digestmod=sha256).digest().hex()
   return sign

getinfo

通过这个接口可以获取 whwswswws, 这个参数经常被用到 Cookie 里面

接口

curl 'https://blackhole.m.jd.com/getinfo' \
-X POST \
-H 'Charset: UTF-8' \
-H 'User-Agent: Dalvik/2.1.0 (Linux; U; Android 10; ONEPLUS A6000 Build/QKQ1.190716.003)' \
-H 'Host: blackhole.m.jd.com' \
-H 'Connection: Keep-Alive' \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data-raw 'content=%7B%22appname%22%3A%22com.jd.jdlite_com.jma.track%22%2C%22whwswswws%22%3A%22%22%2C%22jdkey%22%3A%22jidwhzqalpkLS1mOWY0MDE3NWJmNGM2NzEwLWMxNmI4NDU3ODE1NzlhMmEwOWRmM2NiNmZiZGNiNDMxNGNkOGY%3D%22%2C%22installtionid%22%3A%2291b86fcd33b8437abb7e2deeec68d1f6%22%2C%22appid%22%3A%22105%22%2C%22dataset%22%3A%5B%22alterationinfo%22%2C%22fixedinfo%22%5D%2C%22sdkversion%22%3A%222.5.8%22%2C%22clientversion%22%3A%226.10.0%22%2C%22client%22%3A%22android%22%2C%22body%22%3A%7B%7D%7D' 

参数

{
    "appname": "com.jd.jdlite_com.jma.track",
    "whwswswws": "",
    "jdkey": "jidwhzqalpkLS1mOWY0MDE3NWJmNGM2NzEwLWMxNmI4NDU3ODE1NzlhMmEwOWRmM2NiNmZiZGNiNDMxNGNkOGY=",
    "installtionid": "91b86fcd33b8437abb7e2deeec68d1f6",
    "appid": "105",
    "dataset": [
        "alterationinfo",
        "fixedinfo"
    ],
    "sdkversion": "2.5.8",
    "clientversion": "6.10.0",
    "client": "android",
    "body": {}
}

入口定位

搜索 "jdkey"

1.1 appname

看起来应该是固定的

所以生成规则为: com.jd.jdlite + "_com.jma.track"

1.2 whwswswws

第一次是空的

1.3 jdkey

public static String m15753b() {
        C4086b.m16012b("JDMob.Security.UniqueId", "called getEncryptJDkey");
        try {
            String m15754a = m15754a(C4174c.f13300a);
            if (TextUtils.isEmpty(m15754a)) {
                return "";
            }
            return "jidwhzqalpk" + Base64.encodeToString(m15754a.getBytes(), 2);
        } catch (Exception e) {
            e.printStackTrace();
            return "";
        }
    }

jidwhzqalpk + JDMob.Security.UniqueIdbase64 形式,解开编码后是 --f9f40175bf4c6710-c16b845781579a2a09df3cb6fbdcb4314cd8f

看看 m15754a 是什么,由下面的代码知道是:JDMob.Security.UniqueId

public static String m15754a(Context context) {
        C4086b.m16012b("JDMob.Security.UniqueId", "called getUniqueIdWithCacheInSDK");
        String m15749e = m15749e(context);
        return m15749e == null ? "" : m15749e;
    }

继续跟进去

1if 是直接从缓存中拿,我们不看,看看第 2 个,看起来不像,估计也是拿一下缓存的 cpa_ududud_new 之类的

看第 3 个,可以看出来生成规则了,格式是 - + - + AndroidId + - + sequenceId

AndroidId来自于 BaseInfo.getAndroidId()sequenceId 可以看下面追踪进去的代码

    private static String m15810b() {
        try {
            // m15811a -> md5 
            String str = m15811a(System.currentTimeMillis() + "" + Build.VERSION.SDK_INT + "" + Process.myPid() + "" + UUID.randomUUID()) + m15808c();
            String str2 = str + m15809b(str);
            C4090f.m15999a("sequenceId", str2);
            return str2;
        } catch (Exception unused) {
            return "";
        }
    }

    private static String m15808c() {
        try {
            String str = "";
            Random random = new Random();
            for (int i = 0; i < 4; i++) {
                str = str + Integer.toHexString(random.nextInt(16)).toLowerCase();
            }
            return str;
        } catch (Exception unused) {
            return "";
        }
    }

1.4 installtionid

查找引用发现这个值来源于下面这段代码

点进去看看发现又是返回的一个变量,因此要继续追踪这个变量的来源

最终找到这一行代码

所以生成规则就是: str(uuid.uuid4()).replace("-", "")

响应

{
    "code": 0,
    "whwswswws": "AAhCesjqKEAAAAAAAAAAAAAAAAPn0AXW_TGcQQAAAAAA",
    "openall": 1,
    "openalltouch": 1,
    "processtype": 1,
    "appidStatuscode": 1,
    "interval": 1,
    "dd": "",
    "alterationinfo": 1,
    "fixedinfo": 480
}

jmafinger

这个参数是设备指纹,长这样:JD0121e332LzlDP9SQnn169327413572802Zxyho-EPTLaVrMfW7p2zCXNRD-21aqFt50ftAL5ODL-t_4eH_PJDzv2B3YFXtcX-vZpprU-V662zCIfR7Ce9VA1dnd75z~,一般放置在请求头里面!

加密定位

进去代码

    public static String getSoftFingerprint(Context context) {
        if (context == null) {
            C4086b.m16018a("JDMob.Security.JMA", "context is NULL!");
            return "";
        }
        C4174c.m15645a(context);
        try {
            String m15914a = C4102f.m15914a();
            String m15558a = C4181e.m15557a(context).m15558a();
            return m15914a + Constants.WAVE_SEPARATOR + m15558a;
        } catch (Throwable unused) {
            return "";
        }
    }

Constants.WAVE_SEPARATOR 就是波浪号, 那么 m15558a 就是空字符串了,所以继续找 m15914a

    public static String m15914a() {
        return TextUtils.isEmpty(f13204a) ? "" : f13204a;
    }

发现其来自于 f13204a, 继续找

所以发现下面就是生成设备指纹的具体代码了

    public static String m15913a(Context context) {
        C4086b.m16012b("JDMob.Security.DeviceFingerprint", "getDeviceFinger called.");
        try {
            String m15904c = m15904c();
            String str = "" + System.currentTimeMillis();
            String m15901d = m15901d();
            String str2 = "" + m15904c + str + m15901d;
            byte[] m15905b = m15905b(m15901d, C4134r.m15754a(context), m15910a(m15904c, str, m15901d));
            String encodeToString = m15905b != null ? Base64.encodeToString(m15905b, 11) : "";
            String m15896f = m15896f(C4094c.m15960d(context) + C4174c.m15626m());
            int charAt = m15896f.charAt(0) % '\n';
            String str3 = "JD" + HiAnalyticsConstant.KeyAndValue.NUMBER_01 + "21" + m15896f.substring(charAt, charAt + 4) + str2 + encodeToString;
            String str4 = str3 + m15895g(str3);
            f13204a = str4;
            return str4;
        } catch (Exception e) {
            C4086b.m16018a("atf", "getDeviceFinger error:" + e.getMessage());
            return "";
        }
    }

代码分析

接下来我们需要一步步分析了,首先看到代码行

String m15904c = m15904c();

点进去发现具体代码如下:

private static String m15904c() {
     SecureRandom secureRandom = new SecureRandom();
     StringBuffer stringBuffer = new StringBuffer();
     for (int i = 0; i < 10; i++) {
       stringBuffer.append("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".charAt(secureRandom.nextInt(62)));
     }
     return String.valueOf(stringBuffer);
   }

根据代码很容易看出来,定义了一个 table0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ, 然后从这里面随机挑选出一个 10 位的字符串!

String str = "" + System.currentTimeMillis();

这行代码的意思就是生成了一个时间戳

String m15901d = m15901d();

需要点进去看 m15901d 的具体逻辑

private static String m15901d() {
     return String.format("%02x", Integer.valueOf(new SecureRandom().nextInt(6) + 1));

发现其就是从 16 中生成了一个随机数, 并且将前面补 0,补成 2 位

String str2 = "" + m15904c + str + m15901d;

字符串相加

byte[] m15905b = m15905b(m15901d, C4134r.m15754a(context), m15910a(m15904c, str, m15901d));

当前调用了 m15905b 方法, 这个方法接收三个入参, 第一个 m15901d 是已知的, 第二个通过 C4134r.m15754a 生成,点击去这个函数发现其就是获取了 unique_id

而具体生成 unique id 的代码如下:

经过分析,具体代码应该为: String m15752b = m15752b(context);

发现其就是几个拼接,里面包含了 AndroidIdsequenceId, 这两个 ID 的生成规则其实还是很简单的, AndroidId 可以随机生成,而 sequenceId 的话,可以看到以下代码,实际就是随机生成一段代码之后进行处理。

这边不做过多的赘述了,直接附上还原好的Python 代码,代码如下:

def get_sequence_id():
    def get_flag(str_):
        try:
            if not str_ or len(str_) != 36:
                return "X"

            char_array = list(str_)
            i = 0
            i2 = 0
            for i3 in range(len(char_array)):
                if i3 % 2 == 0:
                    i += int(char_array[i3], 16)
                else:
                    parse_int = int(char_array[i3], 16) * 2
                    i2 = i2 + parse_int if parse_int < 16 else i2 + (parse_int // 16) + (parse_int % 16)

            i4 = (i + i2) % 16
            return "0" if i4 == 0 else hex(16 - i4)[2:]
        except Exception:
            return "X"

    s = str(uuid.uuid4()).replace("-", "") + "".join([format(random.randint(0, 15), 'x') for _ in range(4)])
    return s + get_flag(s)


def get_android_id():
    return "".join(random.choices(string.ascii_lowercase + string.digits, k=16))


def get_unique_id():
    """
    - + - + AndroidId + - + sequenceId

    :return:
    """
    return f"--{get_android_id()}-{get_sequence_id()}"

然后第三个参数是通过 m15910a(m15904c, str, m15901d) 生成的,这边也需要三个入参,这三个入参我们上面已经分析了的,具体要看 m15910a 这个函数

发现他有好几个分支,目前我们已知了 m15901d 是一个随机数字补全的,分支也是跟着他走的,如果 m15901d 的值不一样,可能调用的算法也不一样,所以我们可以使用 Frida 调试一下进行分析,下面我们来分析里面每个分支的函数逻辑。

首先是 m15897e 这个函数

很明显可以看出来是一个 des 加密,实际是 encrypt3DES 算法

接下来是 m15899d 这个函数

发现他调用的是 AES 加密

String encodeToString = m15905b != null ? Base64.encodeToString(m15905b, 11) : "";

这行代码是将上一步加密的结果转为 Base64 字符串,并且使用的是 URL Safe 的模式

 String m15896f = m15896f(C4094c.m15960d(context) + C4174c.m15626m());

然后是这个函数,点进去其实可以看到这个好像是 sha256

可以先 Hook 一下这个方法, 看看入参,

let C4102f = Java.use("com.jd.stat.common.f");
C4102f["f"].implementation = function (str) {
    let result = this["f"](str);
    HookBox.log({
        "tag": "C4102f.f",
        "msg":{
            "str": str,
            "result": result
        }
    })
    return result;

可以看到入参就是包名,使用现成的工具看看是不是原生的 sha256

str: com.jd.jdlite
result: ca4e07726e332c404ed39134eb3e1251a1f80181306cab335dc0753b1a157076

没错,就是原生的 sha256

下面的代码就是一些简单的拼接了,到这里算法基本就理清楚了,这边附上还原好的代码(因为 Python 这边调用加密的代码比较麻烦,而且有时候结果也不一致,所以这边采用了 JS 的代码)

import base64
import random
import re
import string
import time
import uuid
import zlib
from hashlib import sha256

import execjs

js_code = """
const CryptoJS = require('crypto-js');
const crypto = require('crypto');

function toUrlSafeBase64(base64String) {
    return base64String.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}

// 加密函数
function encrypt3DES(message, secret, iv) {
    const keyHex = CryptoJS.enc.Utf8.parse(secret);
    const ivHex = CryptoJS.enc.Utf8.parse(iv);
    const encrypted = CryptoJS.TripleDES.encrypt(message, keyHex, {
        mode: CryptoJS.mode.CBC,
        padding: CryptoJS.pad.Pkcs7,
        iv: ivHex
    });
    return toUrlSafeBase64(encrypted.toString());  // 默认输出Base64
}


function encryptAES(message, secret, iv) {
    let bytes = Buffer.from(secret);

    if (bytes.length % 16 !== 0) {
        const bArr = Buffer.alloc(((bytes.length / 16) + 1) * 16);
        bArr.fill(0);
        bytes.copy(bArr);
        bytes = bArr;
    }
    bytes = bytes.slice(0, 16)
    try {
        const secretKeySpec = crypto.createCipheriv('aes-128-cbc', bytes, Buffer.from(iv, 'utf-8'));
        let encrypted = secretKeySpec.update(message, 'utf-8', 'base64');
        encrypted += secretKeySpec.final('base64');
        return toUrlSafeBase64(encrypted);
    } catch (error) {
        console.error(error);
        return null;
    }
}

"""

context = execjs.compile(js_code)


def get_sequence_id():
    def get_flag(str_):
        try:
            if not str_ or len(str_) != 36:
                return "X"

            char_array = list(str_)
            i = 0
            i2 = 0
            for i3 in range(len(char_array)):
                if i3 % 2 == 0:
                    i += int(char_array[i3], 16)
                else:
                    parse_int = int(char_array[i3], 16) * 2
                    i2 = i2 + parse_int if parse_int < 16 else i2 + (parse_int // 16) + (parse_int % 16)

            i4 = (i + i2) % 16
            return "0" if i4 == 0 else hex(16 - i4)[2:]
        except Exception:
            return "X"

    s = str(uuid.uuid4()).replace("-", "") + "".join([format(random.randint(0, 15), 'x') for _ in range(4)])
    return s + get_flag(s)


def get_android_id():
    return "".join(random.choices(string.ascii_lowercase + string.digits, k=16))


def get_unique_id():
    """
    - + - + AndroidId + - + sequenceId

    :return:
    """
    return f"--{get_android_id()}-{get_sequence_id()}"


def str2arr(str_, length=None, start=1, step=2):
    arr = []
    length = length or len(str_)
    for i in range(start, length, step):
        if str(str_[i]).isdigit():
            arr.append(int(str_[i]))
        else:
            arr.append(int(ord(str_[i]) % 10))

    return arr


def hash_code(str1, str2, str3):
    def type1(str1_, str2_):
        num1 = int(re.sub(r'[^0-9]', "", str1_) or "0")
        num2 = int("".join(map(str, str2arr(str2_, start=1, length=len(str2_), step=2))))
        return abs(num1 - num2)

    def type2(str1_, str2_):
        arr1 = str2arr(str1_, start=0, length=len(str1_), step=2)
        arr2 = str2arr(str2_, start=1, length=len(str2_), step=2)
        if len(arr1) == 5 and len(arr2) == 6:
            sb = []
            i = 0
            sb.append(abs(arr2[i]))
            while i < len(arr1):
                i += 1
                sb.append(abs(arr1[i - 1] - arr2[i]))

            return "".join(map(str, sb))
        return ""

    def type3(str1_, str2_):
        arr1 = str2arr(str1_, length=5, start=0, step=1)
        arr2 = str2arr(str2_, length=len(str2_), start=len(str2_) - 5, step=1)
        sb = []
        for i in range(len(arr1)):
            sb.append(abs(arr1[i] - arr2[i]))

        return "".join(map(str, sb))

    if str3 in ["01", "04"]:
        return type1(str1, str2)
    elif str3 in ["02", "05"]:
        return type2(str1, str2)
    elif str3 in ["03", "06"]:
        return type3(str1, str2)
    else:
        return ""


def m15905b(str1, str2, str3):
    if str1 in ['01', '02', '03']:
        return context.call("encrypt3DES", str(str2), str(str3), "00000000")
    else:
        return context.call("encryptAES", str(str2), str(str3), "0000000000000000")


def calculate_crc32_str(data):
    try:
        crc32_value = zlib.crc32(data.encode())
        crc_str = base36encode(crc32_value)

        if len(crc_str) > 7:
            return crc_str[-7:]
        elif len(crc_str) < 7:
            padding = '0' * (7 - len(crc_str))
            return padding + crc_str
        else:
            return crc_str
    except Exception:
        return ""


def base36encode(number):
    alphabet = '0123456789abcdefghijklmnopqrstuvwxyz'
    base36 = ''
    while number:
        number, i = divmod(number, 36)
        base36 = alphabet[i] + base36
    return base36


def get_jmafinger():
    table = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
    m15904c = "".join(random.choices(table, k=10))
    # m15904c = "f6QtgJmpJ3"
    str1 = str(int(time.time() * 1000))
    # str1 = "1693302585903"
    tag = f'{random.randint(1, 6):02}'
    # tag = "06"
    str2 = "" + m15904c + str1 + tag
    unique_id = get_unique_id()
    encode_to_string = m15905b(
        tag,
        unique_id,
        hash_code(m15904c, str1, tag)
    )
    m15896f = sha256("com.jd.jdlite".encode()).hexdigest()
    char_at = ord(m15896f[0]) % 10
    str3 = "JD" + "01" + "21" + m15896f[char_at:char_at + 4] + str2 + (encode_to_string or "")
    return str3 + calculate_crc32_str(str3)


if __name__ == '__main__':
    print("jmafinger: ", get_jmafinger())

cipher

京东极速版以及 APP 这边后续有些接口的 body 全部加密了,因此需要查询一下这个加密算法

加密定位

搜索 cipher

选择第一个进去

看起来具体实现在这边:

这里有两个分支,需要判断具体走的是哪个分支,可以通过 Frida Hook 一下发现实际走的上面那个分支,所以实际的加密函数就是一个魔改的 Base64

点进去发现解码函数也在

算法还原

逻辑比较简单,直接还原就可以


def cipher_encode(b_arr):
    if isinstance(b_arr, str):
        b_arr = b_arr.encode()
    sb = ""
    for i in range(0, len(b_arr), 3):
        b_arr2 = [0] * 4
        b2 = 0
        for i2 in range(3):
            i3 = i + i2
            if i3 <= len(b_arr) - 1:
                b_arr2[i2] = (b2 | ((b_arr[i3] & 255) >> ((i2 * 2) + 2)))
                b2 = ((((b_arr[i3] & 255) << (((2 - i2) * 2) + 2)) & 255) >> 2)
            else:
                b_arr2[i2] = b2
                b2 = 128
        b_arr2[3] = b2
        for i4 in range(4):
            if b_arr2[i4] <= 63:
                sb += chr(STR2BASE64[b_arr2[i4]])
            else:
                sb += '='
    return sb


def cipher_decode(string):
    output_stream = bytearray()
    b_arr = []
    for i in bytearray(string.encode()):

        try:
            b_arr.append(BASE642STR[i])
        except:
            pass

    for i2 in range(0, len(b_arr), 4):
        b_arr2 = [0] * 3
        i3 = 0

        for i4 in range(3):
            i5 = i2 + i4
            i6 = i5 + 1
            if i6 <= len(b_arr) - 1 and b_arr[i6] >= 0:
                b_arr2[i4] = b_arr[i4] = (((b_arr[i5] & 255) << ((i4 * 2) + 2)) & 255) | (
                        (b_arr[i6] & 255) >> (((2 - (i4 + 1)) * 2) + 2))
                i3 += 1
        for i7 in range(i3):
            output_stream.append(b_arr2[i7])
    return output_stream.decode()

最后更新于

这有帮助吗?