目录
- 一、网址及目标数据
- 二、分析接口反爬点(akamai执行过程)
- 三、逆向分析参数sensor_data
- 四、扣js算法代码加密详细步骤
一、网址及目标数据
文章原文
1、网站: https://www.dhl.com/cn-zh/home/tracking/tracking-supply-chain.html?submit=1&tacking-id=1232343
2、目标:爬取这个接口的数据 https://www.dhl.com/utapi?trackingNumber=1232343&language=zh&requesterCountryCode=CN&source=tt ,即如下图片内容的展示
3、直接模拟请求这个接口,响应状态码403,有反爬,目标让这个接口请求状态码响应返回200,且拿到响应文本内容
二、分析接口反爬点(akamai执行过程)
1、utapi的请求方式与请求链接:get请求,参数为固定值,无反爬加密值
2、请求头:无疑似反爬加密值
3、请求头cookie:有个_abck的cookie可能是反爬参数,包括其它ak_bmsc、bm_sz、bm_sv的cookie
4、_abck的cookie:由WDsB接口链接响应头set-cookie返回
5、_abck的cookie:详细看下WDsB接口 , 发现是POST请求,请求参数有个加密值sensor_data
6、ak_bmsc、bm_sz的cookie,由首页请求链接响应返回
这两个cookie直接get请求首页响应返回的, 同时它还返回了个_abck的cookie,但和我们目标utapi接口的cookie并不是同一个
from curl_cffi import requestsurl = "https://www.dhl.com/cn-zh/home/tracking/tracking-supply-chain.html?submit=1&tacking-id=1232343"response = requests.get(url, impersonate="chrome124", timeout=10)
res_cookies = dict(response.cookies)
print(response, res_cookies)
7、bm_sv的cookie,由下面这个json链接返回
我们测试下如下代码,发现这3个ak_bmsc、bm_sz、bm_sv的cookie值我们都拿到了
from curl_cffi import requestsurl = "https://www.dhl.com/cn-zh/home/tracking/tracking-supply-chain.html?submit=1&tacking-id=1232343"
response = requests.get(url, impersonate="chrome124", timeout=10)
res_cookies = dict(response.cookies)
print(response, res_cookies)url = "https://www.dhl.com/global/dhl/news-alerts.gnf.json"
response = requests.get(url, impersonate="chrome124", cookies=res_cookies, timeout=10)
res_cookies = dict(response.cookies)
print(response, res_cookies)
8、虽然前面两个接口请求也拿到了_abck的cookie,但是带入cookie请求utapi接口请求,并不能响应状态码200,且在步骤4和步骤5我们分析得到,是由WDsB的接口返回的_abck的cookie才有用
9、同时我们发现WDsB这个接口请求了2次,第一次是GET请求,第二次是POST请求,
10、且第一次get请求的WDsB接口返回的内容是js内容,
11、第二次Post请求的WDsB接口返回的内容是success:true,这个似乎是验证cookie : _abck是否正确的,所以我们接下来的目标也是拿到WDsB响应是success后的cookie: _abck
12、既然WDsB的第一次内容是js内容,我们需要找下它的js链接是哪里来的
13、我们在首次首页请求的响应文本里面找到了WDsB接口js链接的由来
14、从首页请求取js链接的url,我们多次运行发现js链接url是动态变化的
from curl_cffi import requests
import reurl = "https://www.dhl.com/cn-zh/home/tracking/tracking-supply-chain.html?submit=1&tacking-id=1232343"
response = requests.get(url, impersonate="chrome124", timeout=10)
res_cookies = dict(response.cookies)
print(response, res_cookies)
js_url = 'https://www.dhl.com' + re.search('type="text/javascript" src="(.*?)">', response.text).group(1)
print(js_url)
15、同时我们在网页上也能发现无法再次找到WDsB的接口链接,而是换成了其它的,也就是说js链接动态变化,同时可以自行观察的js文件内容混淆的也是动态变化的,不变的是算法加密逻辑
16、为了后面方便分析同一份js代码,我们需要保存一份首页的html文件,然后用overrides或者fiddler替换html的方法,保证始终分析的是同一份js代码,方便扣代码算法逻辑 ,这里直接鼠标右击首页接口Override content
17、由于步骤16我们固定了首页html,所以接下来我们的js请求链接也会同时固定,不再动态变化,从步骤5我们知道是post请求的那个WDsB链接能返回我们需要的_abck的cookie,所以要成功模拟那个接口请求,那么就要分析这个接口需要的请求参数sensor_data
18、通过xhr断点定位sensor_data的生成位置,如图我们打上xhr断点,清缓存后,刷新网页,看到了zfT的参数包含sensor_data
三、逆向分析参数sensor_data
1、定位sensor_data的生成位置可以通过xhr断点拦截js链接 , 如图我们定位到了sensor_data的生成即zfT
2、而zfT是由FIT拼接而成 ,这里的FIT就是sensor_data
3、搜索FIT=发现有12个匹配到,可疑位置打上断点清缓存再次刷新网页
4、第一次断住的位置是,FIT=0,这是初始赋值的地方
5、同时FIT最后生成的位置在这里,也就是从初始到这里的中间流程就是FIT的生成流程
FIT = jr(jr(jr(jr(jr(jr(jr(jr(jr(jr(MxT, wxT), ZfT[AA()[Lc(Dq)].apply(null, [Vt, Br(Uc), x8])]), wxT), O2T[zB]), wxT), O2T[Uc]), wxT), Vg), wxT), FIT);
6、怎么扣FIT生成值,要么倒着扣,要么正着扣,继续调试,会发现第二次断点断在了这里,简单分析了下,M2T是60位数组,然后和其它几个似乎是拼接起来的
四、扣js算法代码加密详细步骤
1、开始扣js代码,我们先逆着扣js代码,如图手动解混淆还原如下,当前我们差FIT的由来
function get_sensor_data(){// var zfT = (EM(typeof BA()[zd(HA)], 'undefined') ? BA()[zd(RO)].apply(null, [tB, lA]) : BA()[zd(mV)](f9T, tg))[S1()[MA(ld)](nh, Q0)](FIT, AA()[Lc(fD)](wp, FA, wB));// (true ? '{"sensor_data":"': BA()[zd(mV)](f9T, tg))['concat'](FIT, '"}');var zfT = '{"sensor_data":"' ['concat'](FIT, '"}');return zfT
}
2、由目录三的步骤5我们知道FIT最后的生成位置,手动解混淆还原如下,其中wxT是分号,现在我们还差O2T、Vg、FIT的由来
// FIT = jr(jr(jr(jr(jr(jr(jr(jr(jr(jr(MxT, wxT), ZfT[AA()[Lc(Dq)].apply(null, [Vt, Br(Uc), x8])]), wxT), O2T[zB]), wxT), O2T[Uc]), wxT), Vg), wxT), FIT);
// jr(jr(jr(jr(jr(jr(jr(jr(jr(jr(2, wxT), 0), wxT), O2T[zB]), wxT), O2T[Uc]), wxT), Vg), wxT), FIT)
// "2" + ';' + 0 + ';' + O2T[0] + ';' + O2T[1] + ';' + Vg + ';' + FIT;
function get_sensor_data(){FIT = "2" + ';' + 0 + ';' + O2T[0] + ';' + O2T[1] + ';' + Vg + ';' + FIT;var zfT = '{"sensor_data":"' ['concat'](FIT, '"}');return zfT
}
get_sensor_data()
3、全局搜索O2T = , 搜索定位到如下var O2T = fTT || g4() ,其中fTT是undefined ,说明O2T = 是g4()函数,进入到g4()函数
4、发现g4()函数如下,手动解混淆换下如下,其中有些常量我们这里写死了固定值,后续可能还需要多次比对验证常量是固定值;同时这里我们发现它取了document.cookie[‘bm_sz’],所以我们改成传参的形式
function g4(bm_sz) {var gJ = 8888888,qp = 7777777; // 写死固定值var J5 = [gJ, qp];var GQ = bm_szvar Aq = Xm['decodeURIComponent'](GQ)["split"]('~');if (Aq["length"] >= 4) {var kq = Xm["parseInt"](Aq[2], 10);var m0 = Xm["parseInt"](Aq[3], 10);kq = Xm['isNaN'](kq) ? gJ : kq;m0 = Xm["isNaN"](m0) ? qp : m0;J5 = [kq, m0];}var nG;return nG = J5, nG;
}
5、验证下g4()函数传入相同的bm_sz,出参是否相同,出参相同,g4()函数无误
6、现在还差Vg和FIT的由来
function get_sensor_data(bm_sz){var O2T = g4(bm_sz);FIT = "2" + ';' + 0 + ';' + O2T[0] + ';' + O2T[1] + ';' + Vg + ';' + FIT;var zfT = '{"sensor_data":"' ['concat'](FIT, '"}');return zfT
}
var bm_sz = '5FED1A3F67960E11A9FA43C5BE5B3468~YAAQnuBb2taNz2qSAQAATkERlBmfh1YIrwFk3EYaAGiyZsj3sgNaTicXx28L7Ys8mj7OSUlSg+qSvsCJLZszM3bkYX6sSVSoaYDdRACRU8OCIZ2oyKI1GZ30I6XfsfbGhfEwGzBQCozr0pJf9mAGqOcvlNdw2M8forrU4ScI/PWYsMh1UrBxrUpQSHvqEQ+bZTKibFCNCuWX0J2ePdW30WZiDvPgCASbHBypI5Yvkb1Bj82h66RHPStFogRHtNFSI8s5tOtzR/p2OpTSWVwWIACO0kKpXpI+Xz7rK2N4ivZxqC0F49DSIkK7djh3q6p3/rHcuxNVeH9l7ReOiX06fq5kHEfvrQPPPQBKANsxeY+4colUWJgbb8JPjxYeAA6EH4ChjkSl1/EPvOtkhqMxl9vTpDCPaBTKsx//EPkte57rnzqT8Zt7BtLDN7Rk47rou/jSO81mY25eHnA=~4337970~427345'
console.log(get_sensor_data())
7、全局搜索Vg = , 搜索定位到如下,手动解混淆还原后,发现v8() - czT在动态变化,这是因为v8()是当前时间戳,czT也是时间戳,它是一个时间差值; Vg值正常大概是’26,0,0,2,10,0’ , 这里计算的全是函数执行的时间差值,有几个0的我也直接写死了(但是但是如果后面过不掉的话,我们需要和源代码保持一致的逻辑,把这些时间差的逻辑再加回来)
// var Vg = BA()[zd(sr)].apply(null, [C2, gV])[S1()[MA(ld)](nh, MsT)](cR(v8(), czT), S1()[MA(v6)](Zq, Gd))[S1()[MA(ld)].apply(null, [nh, MsT])](J2T, S1()[MA(v6)](Zq, Gd))[S1()[MA(ld)](nh, MsT)](YfT, S1()[MA(v6)](Zq, Gd))[S1()[MA(ld)](nh, MsT)](tNT, S1()[MA(v6)](Zq, Gd))[S1()[MA(ld)](nh, MsT)](kXT, S1()[MA(v6)](Zq, Gd))[S1()[MA(ld)](nh, MsT)](U3);
// var Vg = ""["concat"](cR(v8(), czT), ",")["concat"](J2T, ",")["concat"](YfT, ",")["concat"](tNT, ",")["concat"](kXT, ",")["concat"](U3);
// var Vg = "" ["concat"](v8() - czT, ",")["concat"](0, ",")["concat"](0, ",")["concat"](tNT, ",")["concat"](kXT, ",")["concat"](0);
J2T差值是0
YfT差值是0
U3差值是0
8、现在还差czT、tNT、kXT、FIT的由来
function v8() {if (Xm["Date"]["now"] && typeof Xm["Date"]["now"]() === 'number') {return Xm["Date"]["now"]();} else {return +new(Xm["Date"])();}
}function get_sensor_data(bm_sz){var O2T = g4(bm_sz);var Vg = "" ["concat"](v8() - czT, ",")["concat"](0, ",")["concat"](0, ",")["concat"](tNT, ",")["concat"](kXT, ",")["concat"](0);FIT = "2" + ';' + 0 + ';' + O2T[0] + ';' + O2T[1] + ';' + Vg + ';' + FIT;var zfT = '{"sensor_data":"' ['concat'](FIT, '"}');return zfT
}
var bm_sz = '5FED1A3F67960E11A9FA43C5BE5B3468~YAAQnuBb2taNz2qSAQAATkERlBmfh1YIrwFk3EYaAGiyZsj3sgNaTicXx28L7Ys8mj7OSUlSg+qSvsCJLZszM3bkYX6sSVSoaYDdRACRU8OCIZ2oyKI1GZ30I6XfsfbGhfEwGzBQCozr0pJf9mAGqOcvlNdw2M8forrU4ScI/PWYsMh1UrBxrUpQSHvqEQ+bZTKibFCNCuWX0J2ePdW30WZiDvPgCASbHBypI5Yvkb1Bj82h66RHPStFogRHtNFSI8s5tOtzR/p2OpTSWVwWIACO0kKpXpI+Xz7rK2N4ivZxqC0F49DSIkK7djh3q6p3/rHcuxNVeH9l7ReOiX06fq5kHEfvrQPPPQBKANsxeY+4colUWJgbb8JPjxYeAA6EH4ChjkSl1/EPvOtkhqMxl9vTpDCPaBTKsx//EPkte57rnzqT8Zt7BtLDN7Rk47rou/jSO81mY25eHnA=~4337970~427345'
console.log(get_sensor_data())
9、全局搜索发现tNT、kXT分别是函数FIT = Sw(AZ, [FIT, O2T[Uc]]);执行的时间差值 FIT = Vk(FIT, O2T[kV[rh]]);的时间差值
// 函数时间差var tNT = v8();FIT = Sw(AZ, [FIT, O2T[1]]);tNT = v8() - tNT;var kXT = v8();FIT = Vk(FIT, O2T[0]);kXT = v8() - kXT;
10、网页上调试执行时间差[tNT,kXT]大致是[1, 4]
11、所以[tNT,kXT]大致是[1, 4]这里的值我又暂时写死了
12、同理,czT也是初始的时间戳,然后执行了一大串逻辑函数后到了Vg计算函数运行时间差,多次调试发现这个v8() - czT时间差值差不多是25~30之间
13、所以这里我们也给了个随机值,这样步骤8的czT、tNT、kXT、FIT的由来,只剩FIT的由来不清楚了
14、我们继续研究,进入Vk函数
15、Vk函数手动解混淆还原如下,右侧截图里面有成var zl = (qS >> 8) & 65535;
16、其中 Vk里面还有些变量Kq 、IW需要补充上
var Kq = new (Xm[S1()[MA(sv)].apply(null, [ht, JG])])(lM);
var Kq = new (Xm['Array'])(127)
var IW = "";
17、传入相同的参数,检验Vk函数输出结果一致,Vk函数无误 ,可以发现Vk函数是将明文加密的一个流程
18、继续进入到Sw函数里面
19、手动解混淆还原如下,传入相同的参数,验证结果输出一致,Sw52无误
20、继续往上研究,手动解混淆如下
FIT = jr(jr(jr(jr(jr(LzT, lIT), HPT), B2T(E4(FIT), Qt)), HPT), FIT);
FIT = LzT + lIT + HPT + (E4(FIT)^24) + HPT + FIT;FIT = jr(jr(jr(jr(nmT, HPT), nmT), HPT), FIT);
FIT = "2" + HPT + "2" + HPT + FIT;
21、现在还差LzT 、lIT 、E4、 HPT 、FIT的由来
22、LzT搜索定位如下,这里的扣代码算法逻辑和前面步骤一样,手动解混淆还原验证,如下2张图
23、lIT还原如下,其中这里写死了个值,后面如果换版本需要留意是否一样, 留意变化
24、E4函数如下
25、HPT先定义了个字符串,然后HPT = zg(M2T, 2, false);
26、我们还发现,zg这个函数传入的M2T参数,它是一个60位的数组,包含了一些环境信息,这里我们先写死M2T,后面逐个研究由来
27、手动解混淆zg函数还原如下,传入相同的参数,验证结果输出不一致,因为函数本身里面有很多随机数,所以这个问题不大,后面如果过不了风控,我们再回来对比研究
28、补完上面的内容后,这个时候我们已经能生成sensor_data了,如下
29、其中在扣js代码中,我们写死了一些值,后面如果过不了接口,还得回来反复核对,现在我们还剩M2T这个60位数组的研究, 到目前为止,整个加密算法200多行,重点是60位数组的生成逻辑
30、M2T在这里开始生成
31、我们把上面的代码整理下,得到下面的内容,然后逐步分析M2T的各个数组的值的来源
32、从60位数组里面再次还原下,发现近一半的是固定变量可以直接写死,剩下的需要研究的变量的由来有29个,我们需要一一研究由来,大多数是环境检测ua、屏幕尺寸、页面的input信息、请求页面地址/canvas/wbgl/字体指纹/陀螺仪/事件/鼠标轨迹/函数运行时间差
33、把要研究的变量单独拧出来,发现其它值似乎一直是固定的,有5个特别长的在动态变化,我们先研究这5个看看能不能出结果