爬虫逆向学习(十五):Akamai 3.0反爬分析与sensor-data算法逆向经验
此分享只用于学习用途,不作商业用途,若有冒犯,请联系处理
Akamai 3.0反爬分析与sensor-data算法逆向经验
- Akamai
- 开始正题前须知
- 站点信息
- 接口分析反爬点
- 反爬点定位
- _abck
- 定位结果
- 逆向前准备工作
- sensor_data生成位置
- 本地替换文件
- 请求体sensor_data逆向分析
- qtx五次赋值分析
- 第一次赋值
- 第二次赋值
- 第三次赋值
- 第四次赋值
- 第五次赋值
- 请求测试
- 结尾
Akamai
Akamai
是啥就不多说了,这在爬虫圈可是有名的反爬产品。Akamai 3.0以前的产品我没有研究过,不过在扣Akamai 3.0 sensor-data算法时也看了其他博主的分享,感觉逆向流程差不多的,只不过Akamai 3.0现在每个一两天就会变换js文件,然后js文件每隔十分钟又会变换代码(不同站点规则可能不一样)
严重怀疑变换js文件后相当于变更了算法,而同一份js文件的更新则是对参数的重新混淆和打乱大数组长度与取值
开始正题前须知
我是2024年12月份开始研究的,期间一直是拿保存到本地js文件去跟流程扣算法的,然后到2025年1月份破解算法还能拿到成功数据。
这里要着重说一下:大家研究时代码肯定跟我的不一样了,所以不要抱着能一一跟学的想法,这篇博客重点是提供给大家扣算法思路和一些处理经验。
我尽可能写详细,有点难写hhh…
站点信息
- 网站:
https://www.dhl.com/cn-zh/home/tracking/tracking-supply-chain.html?submit=1&tacking-id=1232343
- 接口:
https://www.dhl.com/utapi?trackingNumber=1232343&language=zh&requesterCountryCode=CN&source=tt
接口分析反爬点
反爬点定位
复制utapi
接口的发包内容,然后将其转成python requests
请求格式,拿到本地python环境下执行看看结果。
Akamai有tls指纹检测
,所以需要用curl_cffi
这个库来请求。
可以看到这里是可以直接请求拿到数据的,然后观察请求头和请求连接并没有反爬点,这时可以推测cookies存在反爬点。
当然不可能全部cookie都是反爬点,这里可以用排除法来确定真正的反爬点。
这里试出来确定请求必须携带_abck
cookie,不然请求响应码是428
。
既然知道cookie _abck
是反爬点,那我们就得知道它从哪里生成的。
看cookie列表这个参数的Secure
是勾上的,说明它是服务器响应返回的,这就好找了,用fiddler
抓包搜一下就知道。
打开fiddler
然后清空浏览器数据,刷新页面,并搜索一下接口,拿到最新的 _abck
,拿到fiddler
,快捷键Ctrl F
打开搜索框搜索,匹配到的包就会标为黄色,找到第一次出现的包就是了。
拿到目标链接后,在开发者工具那搜一下,发现这个接口请求了两次,第一次是GET请求,第二次是POST请求,而第二次请求响应结果返回了需要的cookie _abck
这两次请求是啥关系呢:第一个GET请求返回的是JS代码,然后这些JS代码会生成得到请求体sensor_data
,用来第二次POST请求。
那这个接口链接又是在哪里得到的呢,我们继续在fiddler
搜一下这个接口,发现它第一次出现在网站链接响应内容里
_abck
这个cookie其实在首页就有返回,包括后面的请求有些接口也会返回,但是这里它的中间值都是~-1~
,而对于Akamai来说,它的中间值需要是~0~
才有效。
定位结果
- 请求首页链接拿到JS代码接口
- 通过接口返回的JS代码生成得到请求体
sensor_data
- 使用POST请求接口,并带上请求体
sensor_data
,得到需要的cookie_abck
_abck
中间内容为~0~
说明cookie有效,反之cookie无效
逆向前准备工作
sensor_data生成位置
把JS接口加入xhr断点捕捉,这个能在接口发包时断住,我这里这个接口结尾是BHBSM
,大家按自己的来,然后清空浏览器数据,刷新页面。
看到没,OIY
的值就是我们想要的请求体sensor_data
,理论上只要逆向出OIY
的生成算法就能实现破解了。
本地替换文件
一开始我就说了Akamai同一份js文件的代码会不定时更新,所以我们需要将首页链接和JS接口GET链接保存到本地并进行替换。
这样就替换成功了,后续刷新就不会再变更代码或者文件了
请求体sensor_data逆向分析
承接上文,这里我们开始分析请求体sensor_data生成算法,由于JS代码太多太乱,所以我们选择逆向推理,从结果推理过程。
再次强调,本文所演示的代码肯定跟大家实操时不一致,大家重点学历逆向思路
上文我们已经找到了请求体sensor_data的生成位置,这里是RmX
,我们看下图右边,发现pUX
与RmX
有关联,我们先创建个空白js文件,将它俩的赋值代码扣过去。
大家看下pUX
的赋值:Yx[Sm()[vd(dd)](Vd, Ym)][Fn()[ft(Y9)](cE, sO)](qtX);
,看着是很复杂,但是打印一下发现其实就是某些方法的混淆。看下图,这就是Akamai另一个比较恶心的地方了,如果会AST的可以尝试去解混淆,这里就手动解混淆了
后面不会在这一块讲解太多,大家自己来…
通过上图我们需要找一下qtX
的生成位置,直接在代码那搜索qtX =
,发现有五个位置,这里把五个位置全部打上断点并把代码全部扣下来,然后刷新页面,这样后面才能解混淆。
老样子,先手动解混淆,然后补未定义的参数,如果不知道哪些未定义的可以执行一下脚本,按报错的内容补也行。
qtx五次赋值分析
题外话大家往下看会发现很多参数都是写死的,这里原因有两个:
- 参数属于
Akamai 3.0核心部分
,后续再详细讲解; - 参数属于常量,是在js文件执行时就生成固定了,我们只要知道它怎么取值就行;
言归正传,对于这五个赋值我们这里选择从前往后推。
第一次赋值
var qtX = ‘’;
第二次赋值
qtX = JSON['stringify'](MUX);
中的MUX
先写死,复制浏览器的值就行,这是Akamai 3.0核心部分大字典
,后面再细究
第三次赋值
qtX = qO(45, [qtX, xQX[1]]);
看下xQX
,var xQX = h9X || bb();
-> bb();
所以咱直接处理bb();
就行,把它代码拿下来
bb
方法需要用到cookie bm_sz
然后看下q0
,这里跟进入拿代码
q0
方法本身是一个switch控制流,但是这里它不会执行很多次,只会进入LQ
这一步,拿下这一步的代码即可,打上断点,执行到这里。
q0
方法解混淆后
搞定后我们打印一下执行结果然后跟浏览器对比一下,没问题。
第四次赋值
qtX = hDX(qtX, xQX[0]);
看下hDX
方法,直接把它扣下来解混淆
搞定后我们打印一下执行结果然后跟浏览器对比一下,没问题。
第五次赋值
qtX = ''['concat'](CqX, ';')['concat'](kQX, ';')['concat'](qtX);
看下CqX
和kQX
,往上找就行
先看看var kQX = ''['concat'](WV() - MmX, ',')['concat'](0, ',')['concat'](0, ',')['concat'](ZOX, ',')['concat'](xBX, ',')['concat'](0);
WV:直接复制js文件提供的方法,或者返回当前时间
MmX、ZOX、xBX跟WV有关,且它们是在qtX
多次赋值操作前后
再看var CqX = InX(xQX);
,直接拿下InX
方法解混淆
到这里理论上已经拿到了请求体sensor-data
的结果了,后面我们需要测试一下能够成功拿到数据。
请求测试
这是根据Akamai
反爬流程开发的python请求代码
import re
import execjs
from bs4 import BeautifulSoup
from curl_cffi import requests
class DhlAkamai:
index_url = 'https://www.dhl.com/cn-zh/home/tracking/tracking-supply-chain.html?submit=1&tacking-id=1232343'
data_api = 'https://www.dhl.com/utapi?trackingNumber=1232343&language=zh&requesterCountryCode=CN&source=tt'
def __init__(self):
self.session = requests.Session()
self.session.headers.update({
'accept': '*/*',
'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6',
'content-type': 'text/plain;charset=UTF-8',
'dnt': '1',
'origin': 'https://www.dhl.com',
'priority': 'u=1, i',
'referer': 'https://www.dhl.com/cn-zh/home/tracking/tracking-supply-chain.html?submit=1&tacking-id=1232343',
'user-agent': 'user-agent'
})
self.session.cookies.set('cookieDisclaimer', 'seen')
def get_crack_url(self):
resp = self.session.get(self.index_url)
de_url = re.search('type="text/javascript" {2}src="(.*?)">', resp.text).group(1)
return 'https://www.dhl.com' + de_url
def get_js_data(self, js_url):
self.session.get(js_url) # 代码可以不拿,但是要得到返回的cookie
def post_js_data(self, js_url):
with open('crack_00.js', 'r', encoding='utf8') as js_file:
js_text = js_file.read()
js = execjs.compile(js_text)
sensor_data = js.call('gen_sensor_data', self.session.cookies.get('bm_sz'))
print('sensor_data: ', sensor_data)
resp = self.session.post(js_url, data=sensor_data)
return resp
def get_data(self):
resp = self.session.get(self.data_api)
return resp
def str_cookie(self):
return '; '.join([f'{k}={v}' for k, v in dict(self.session.cookies).items()])
def run(self):
js_url = self.get_crack_url()
print(js_url)
print('index _abck: ', dict(self.session.cookies)['_abck'])
self.get_js_data(js_url)
print('getJs _abck: ', dict(self.session.cookies)['_abck'])
p_resp = self.post_js_data(js_url)
print('posJs _abck: ', dict(self.session.cookies)['_abck'])
print(p_resp.status_code, p_resp.text)
d_resp = self.get_data()
print('gData _abck: ', dict(self.session.cookies)['_abck'])
print(d_resp.status_code, d_resp.text)
if __name__ == '__main__':
dhl_akamai = DhlAkamai()
dhl_akamai.run()
crack_00.js
文件就是扣出来的js代码文件,只不过需要封装出来一个gen_sensor_data
方法。
执行后发现是可以成功拿到数据的。
结尾
这篇先写到这,主要是介绍了Akamai的反爬内容和破解入口,以及对请求体sensor_data
的逆向分析,至于核心部分写死那块我们后面再出博客讲解。
其实最后能拿到数据也是网站风控没那么严格,细心的朋友会发现代码写死了三个位置,而那三个位置其实是很重要的,但凡你随便改了一个都无法请求成功。
我研究过一段时间,虽然能扣出它们的生成算法,但是这个算法只能适用当前JS文件提供的代码(或者一部分),已更新JS文件就不适用了,归根结底我觉得是每个JS文件会生成一个大数组,这三个位置的生成都需要用到它,而每个JS文件的大数组长度和每次取值都不一定相同,也就导致无规律性了。
估计是还没研究透,后续有时间再战…