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&' \
可以看到请求包含了一些加密:
Sign
Sign
算法定位
首先先尝试使用 r0tracer
调试一下,进入评论页面后看一下当前的 activity
› adb shell dumpsys window | grep mCurrentFocus
mCurrentFocus=Window{6550c38 u0 com.jd.jdlite/com.jd.lib.productdetail.ProductDetailActivity}
可以知道包名为:com.jd.jdlite
, 当前 activity
为 com.jd.lib.productdetail.ProductDetailActivity
使用 r0tracer
尝试 hook
一下发现 frida
直接崩溃了,所以是有 frida
校验的
询问了大佬得知,需要上魔改版的 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 ... !")
})
发现 hook
成功,接下来就可以使用 frida
调试了!
先尝试用 r0captre
调试抓包一下,看看能不能找到加密接口的直接调用堆栈,但是发现直接卡死了
那就使用常规的 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
app
已经正常启动了,此时我们随便点一下详情页,发现已经找到输出的调用堆栈了
通过抓包软件我们也能看到,这是极速版的详情页接口!
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
就可以 ),然后找到该函数
搜索一下 sign
这个字符串,看看哪里赋值了,很容易发现 其实 sign
就是 r
这个变量。
而 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
之后找到详情页输出
可以发现加密函数第一个参数是我们请求的 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 "";
}
通过分析源码可以知道加密步骤:
程序将 URL 内的参数解析出来,并且将
body
合并至一起之后,将其Key
加入到了TreeSet
里面,查询了一下TreeSet
的作用,实际上会默认升序排序,参考文章见:java集合-TreeSet的两种排序规则 - 掘金 (juejin.cn)循环拿出来每一个
Key
,获取key
的值添加到stringBuffer
添加一个
ContainerUtils.FIELD_DELIMITER
, 也就是&
遍历完成后将
stringBuffer
转换为stringBuffer2
去除
stringBuffer2
末尾的&
调用
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
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.UniqueId
的 base64
形式,解开编码后是 --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;
}
继续跟进去
第 1
个 if
是直接从缓存中拿,我们不看,看看第 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
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);
}
根据代码很容易看出来,定义了一个 table
为 0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
, 然后从这里面随机挑选出一个 10
位的字符串!
String str = "" + System.currentTimeMillis();
这行代码的意思就是生成了一个时间戳
String m15901d = m15901d();
需要点进去看 m15901d
的具体逻辑
private static String m15901d() {
return String.format("%02x", Integer.valueOf(new SecureRandom().nextInt(6) + 1));
发现其就是从 1
到 6
中生成了一个随机数, 并且将前面补 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);
发现其就是几个拼接,里面包含了 AndroidId
和 sequenceId
, 这两个 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
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()
最后更新于