【🏠作者主页】:吴秋霖
【💼作者介绍】:擅长爬虫与JS加密逆向分析!Python领域优质创作者、CSDN博客专家、阿里云博客专家、华为云享专家。一路走来长期坚守并致力于Python与爬虫领域研究与开发工作!
【🌟作者推荐】:对爬虫领域以及JS逆向分析感兴趣的朋友可以关注《爬虫JS逆向实战》《深耕爬虫领域》
未来作者会持续更新所用到、学到、看到的技术知识!包括但不限于:各类验证码突防、爬虫APP与JS逆向分析、RPA自动化、分布式爬虫、Python领域等相关文章
作者声明:文章仅供学习交流与参考!严禁用于任何商业与非法用途!否则由此产生的一切后果均与作者无关!如有侵权,请联系作者本人进行删除!
1. 写在前面
今天端午节,祝大家端午安康!最近事情比较多,所以又有很长一段时间没有写文章了(差点忘了这个好习惯~~
)。刚好今天过节有时间,把前些天看的一些小案例分享一下。这期分析的是某航空网站的加密参数跟它的设备指纹风控,通过非登录游客身份搜索航班机票查询然后解决风控标记弹行为验证(比较简单适合新手研究
)
分析网站:
aHR0cHM6Ly93d3cuYWlyY2hpbmEuY29tLmNuL2dhdGV3YXkvYXBpL2ZsaWdodC9saXN0
2. 接口分析
随便在搜索入口查询一趟航班信息,可以看到发包请求参数params
一段密文,如下所示:
除了请求参数加密外,请求头也有一个x-device-token
参数疑似动态生成的(但不是直接网站的JS代码层面生成的
)。如下所示:
像这种Token
请求头的参数一般情况下我们可以先不去管它,先去分析请求参数的加密。头部一般非签名的核心参数,你固定测试一次两次是可以重放的。但是请求参数的加密对什么加密了、明文是什么以及后续能够模拟伪造出请求来测试是必须
初看这个Token
的时候我感觉在哪里见过(比较熟悉
)。有经验的可能会猜测它是由某个接口动态请求之后服务端下发的(在有效的时间范围可以有效固定去使用
)做过某东逆向分析的可以发现它的这个参数开头tak01...
跟它们官方的那个设备指纹高度相似(后续验证发现就是用的某东的设备指纹风控)
3. 加密分析
开始定位找到发包加密的位置,这里可以直接通过XHR
断点去跟栈就能够溯源到整个发包跟调用加密的位置,如下所示:
可以看到o.P
就是加密方法,直接跳转到对应的JS代码,如下:
var c = function(e) {
try {
var t = encodeURIComponent(JSON.stringify(e));
return o.sm2.doEncrypt(t, "04064c2a3bcafba2c1ca4f5fb8ecd876b23d70fc4479b78f3c8066c02a8c17749458bca86361bc563d2501b61e2ac93a676a1305893aafcc6be2ea48ecb048672e", a.yV)
} catch (n) {
return console.log(n),
""
}
}
看上面加密代码的入口,比较明显的可以看到使用了疑似sm2
国密算法。像一般我们猜测到了加密算法大致看看是否标准的就可以直接使用自己擅长的语言导个包去进行还原。后面那一串的话看起来就是它的密钥,sm2
一般长度256 bit
,开头04
表示非压缩。后面那64
字节(128个十六进制字符
),大致如下得出:
04 || <X (64 hex)> || <Y (64 hex)> = 1 + 64 + 64 = 129 hex digits
略微看一下它的那个JS实现,是标准一个sm2
。然后要还原这个加密算法的方式可以直接把doEncrypt
的JS代码扣出来(webpack
)然后导出模块再调用即可。还有就是确定完整个加密算法是标准的
还是变异的
或者魔改的
后用其他语言实现(代码量会大大降低
)
4. 算法还原
既然是分享,这里扣webpack
跟使用纯算还原的方式都说一下。首先跳转到sm2.doEncrypt
代码处,开始扣一些JS
。扣代码的也是讲究精扣
跟粗扣
的
这里精扣
的话就几百行涉及加密的这几段JS代码就行。新手你全部粗扣
下来也无所谓(4W多行-不用在意这些细节-能用就行
)
然后再把webpack
内部的模块加载函数导出来,模块加载器的结构一般如下所示:
function enc(n) {
var f = t[n];
if (void 0 !== f) return f.exports;
...
e[n].call(r.exports, r, r.exports,enc)
...
return r.exports
}
t[n]
: 缓存已加载模块
e[n]
: 模块定义函数对象(模块id -> 函数
)类似Dict
r.exports
: 模块的导出结果
之后找到SM2
加密方法的所在模块ID(70686
),如下所示:
那么这个时候我们就可以在JS中直接导出扣下来的webpack
模块,重新封装一下加密方法。代码实现如下:
function encrypt(e) {
var o = sm2(70686), // 加密模块
try {
var t = encodeURIComponent(JSON.stringify(e));
return 0.sm2.doEncrypt(t, "04064c2a3bcafba2c1ca4f5fb8ecd876b23d70fc4479b78f3c8066c02a8c17749458bca86361bc563d2501b61e2ac93a676a1305893aafcc6be2ea48ecb048672e", 1);
} catch (n) {
console.log(n);
return "";
}
}
除了上面说到的扣代码,再就是直接使用导包的方式来还原(前提上面的分析我们已经知道了它是一个标准的加密算法
),这里我们直接可以使用NodeJS
导出加密模块的方式实现,实现代码如下:
const smCrypto = require('sm-crypto');
const { sm2 } = smCrypto;
function encrypt(e) {
try {
const t = encodeURIComponent(JSON.stringify(e));
// 密钥
const publicKey = "04064c2a3bcafba2c1ca4f5fb8ecd876b23d70fc4479b78f3c8066c02a8c17749458bca86361bc563d2501b61e2ac93a676a1305893aafcc6be2ea48ecb048672e";
return sm2.doEncrypt(t, publicKey, 1);
} catch (error) {
console.error("加密失败:", error);
return "";
}
}
这个密钥好像是定期会更新的,然后接下来我们验证一下已经通过逆向分析还原出来的加密算法,对接到单次的请求中是否可以正常拿到接口的响应数据,代码实现如下:
import execjs
import requests
from loguru import logger
from getuseragent import UserAgent
def encrypt_request_data(data):
with open("sm2.js", encoding='utf-8') as f:
ctx = execjs.compile(f.read())
res = ctx.call(
"encrypt",
data)
return res
def send_request(encrypted_data):
random_ua_list = ["chrome", "firefox", "safari"]
ua = UserAgent(random.choice(random_ua_list))
useragent = ua.Random()
url = "https://www.xxx.com.cn/gateway/api/flight/list"
headers = headers = {
"Content-Type": "application/json",
"Accept": "application/json, text/plain, */*",
"Sec-Fetch-Site": "same-origin",
"Accept-Language": "zh-CN,zh-Hans;q=0.9",
"Accept-Encoding": "gzip, deflate, br",
"Sec-Fetch-Mode": "cors",
"Host": "www.xxx.com.cn",
"Origin": "https://www.xxx.com.cn",
"User-Agent": useragent,
"Referer": "https://www.xxx.com.cn/flight/oneway/pek-ctu/2025-06-04",
"Content-Length": "846",
"Connection": "keep-alive",
"Sec-Fetch-Dest": "empty",
"X-Locale": "zh-CN",
"X-Device-Token": "" # 自行获取
}
data = {
'params': encrypted_data,
'RequestParameterEncryptionIdentificationBit': True
}
data = json.dumps(data, separators=(',', ':'))
response = requests.post(url, headers=headers, data=data)
return response.json()
if __name__ == "__main__":
# 请求明文参数
params = {
"Trip": [{"Date": "2025-06-05", "Dep": "PEK", "Arrival": "CTU"}],
"Passenger": {"adult": 1, "child": 0, "baby": 0},
"notchType": None,
"aimPrice": None,
"RequestParameterSecurityIdentificationBit": True
}
encrypted_data = encrypt_request_data(params)
logger.info(f'加密参数: {encrypted_data}')
result = send_request(encrypted_data)
logger.info(f'查询数据: {result}')
运行一下上面封装好的Python
请求代码,可以看到是没有问题的。能够正常拿到数据,如下所示:
5. 设备指纹风控分析与绕过
上面测试的是非登录状态下的情况,其实登不登录都无所谓。它本身是有一个风控参的,整个爬虫的风控也都基于它来开展。就是前面我一开始分析提到的X-Device-Token
这个参数就是设备指纹的信息,用来防护恶意请求的。一个X-Device-Token
可以请求的次数在10
次以内,浏览器的环境就会被标记再配合风控判定推送行为验证码(点选
),如下所示:
所以如果需要多次持续的请求查询一些数据的话,要么就是直接再逆向分析点选
的协议,拿到验证的Token
提交也是可以的。再就是解决这个参数绕过点选
。继续进一步分析这个参数发现是使用的某东设备指纹风控(包括这个点选也是某东云的验证码系统
)如下所示:
这个参数收集了【浏览器的指纹
、Canvas
、WebGL
、分辨率、Storage...
】以及一些初始化加载行为
针对这个X-Device-Token
参数的对抗方案其实也是有很多种的,第一个就是自己定制魔改,第二个就是使用开源好已经从Chromium
源码层修改浏览器指纹信息的方案(外面的指纹浏览器也是可以的
)
这里我们就简单的用魔改过的方案自己在本地搭一个服务,然后通过注入JS
的方式刷票务的接口Hook
到最新的这个参数,如下所示:
经过多次测试,一个设备指纹的新参数在请求风控出现行为验证码
的时候,调用测试搭建的刷新设备指纹的服务获取新的X-Device-Token
都是可以立即绕过行为验证正常再次请求数据的(因为我们使用的魔改方案、它会认为我们是一个新的设备及浏览器
),如下所示:
它这个不管是自动化浏览器的方案还是接口协议的方案都会遇到行为验证码的风控(它的行为验证不像其他平台是必须要过的、可以规避掉
)。然后像这种非登录状态或者游客模式可以访问的爬虫方案,厂商针对的风控一般只会从IP
、设备指纹
开展
最后多说一句(爬虫目前想要持续抓取一些头部平台的数据、除了很普遍的很基本的接口验签逆向外。需要解决的就是风控,风控涉及的细分领域有很多。而其中最常遇见的就是设备指纹风控跟行为验证风控,那么对抗是对抗的什么呢?就是这一系列的风控防护,所以定制或魔改指纹、改机这些都是需要涉及的。以前可能只需要考虑策略,但那已经是S3赛季了...
)