本文章中所有内容仅供学习交流使用,不用于其他任何目的,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关!若有侵权,请联系作者删除。
逆向目标
目标网站:
aHR0cHM6Ly93d3cudGhhaWFpcndheXMuY29tL2VuLXVzLw==
抓包分析
进入页面后选择单程航班,填写完出发和到达地等基础信息后点击Search Flights搜索航班

1.png (40.24 KB, 下载次数: 3)
下载附件
2025-9-24 20:03 上传
此时会打开一个tab页跳转新的链接,速度将链接复制下来,后续直接访问这个链接抓包,就不用每次进首页填信息搜索了
链接我也贴下来吧,记得改下里面的航班日期
aHR0cHM6Ly93d3cudGhhaWFpcndheXMuY29tL3Bzc19yb3V0ZXIvcmVmeC9ib29raW5nP2FfYWlycG9ydD1UUEUmY2FiaW5fY2xhc3M9UEUmZF9kYXRlPTIwMjUtMDktMTYmZF9haXJwb3J0PVBFSyZsYW5nPWdiJm5fYWR1bHQ9MSZuX2NoaWxkPTAmbl9pbmZhbnQ9MCZ0cmlwX3R5cGU9TyZwb3NfY291bnRyeV9jb2RlPXVzJnNvdXJjZT13ZWIK
这个网站有两种请求链接的方式可以获取到目标加密参数
这里我使用抓包工具Reqable来展示(很好用的工具赶紧出3.0!!)
第一种方式:动态链接

2.png (54.25 KB, 下载次数: 2)
下载附件
2025-9-24 20:03 上传
搜索链接关键字:y-Almost-yet-know-Now-Son-ther-That-swearers-of-
发送了三次请求,一次get两次post
关键字后面的路径是动态更新的
我实际观察其实也就三种路径大概每几个小时轮换一下
3nOsRQ8irc_ZtcVNrFJG8gbVR_3mK-NK2L-00vw-NqY
1C6gqW7DCJxcjna2gXOHEpZ86Z_N1PlOqK21enI-F7g
OKH9SC3iqYKBQuit1BFulwBSToX748kxZM6TsL6An5E
get请求 ?s= 后面的是每次请求都会变更的
响应中返回了一个js,代码也是动态的
这是我们需要主要分析的,加密逻辑基本都在这个js里面
第一次post请求

3.png (60.5 KB, 下载次数: 3)
下载附件
2025-9-24 20:04 上传
固定载荷{"f":"gpc"},响应是一个base64编码的数据
解码后样式如下,包含一些字段参数,在主逻辑中需要用到

4.png (62.48 KB, 下载次数: 2)
下载附件
2025-9-24 20:04 上传
第二次post请求
url地址和第一次post一样,载荷变了

5.png (46.58 KB, 下载次数: 3)
下载附件
2025-9-24 20:04 上传
载荷中的 p 是最关键的加密参数
其他参数都可以用正则在js中提取到,有的可以随机生成
补环境返回完整的载荷直接用就行
响应中的token则是我们最终的目标,携带它即可获取接口数据
第二种方式:固定链接

6.png (34.24 KB, 下载次数: 2)
下载附件
2025-9-24 20:04 上传
搜索链接关键字:pplacked-bothe-right-eque-mine-in-him-aftend-Thi
发送了一次get请求,一次post请求
get请求和第一种方式get请求一样是获取动态js
post请求

7.png (49.02 KB, 下载次数: 3)
下载附件
2025-9-24 20:04 上传
可以看到载荷是第一种方式获取到的token
响应中的token并没有变化
一开始我以为是注册激活的接口,其实不然,怀疑是查询有效期
试着将local storage中存入的reese84 token删除掉

8.png (63.97 KB, 下载次数: 0)
下载附件
2025-9-24 20:04 上传
再重新请求航班搜索接口
或者不删除多等待一会看抓包内容
发现不会再请求动态链接的接口了

9.png (113.27 KB, 下载次数: 2)
下载附件
2025-9-24 20:04 上传
隔一段时间请求固定链接,载荷也和动态链接的一样
只是多个了old_token有值
old_token即上一次正确获取的token
实测固定链接post请求old_token为空也可以
巴拉巴拉说了一大段
总结就是用动态链接的js或者固定连接的js补环境都可以
js的核心逻辑基本一致
固定连接方便替换js并且少一次post请求
最后我们再分析看一下哪个接口返回了航班数据

10.png (63.01 KB, 下载次数: 2)
下载附件
2025-9-24 20:03 上传

11.png (46.38 KB, 下载次数: 3)
下载附件
2025-9-24 20:03 上传
search/air-bounds接口返回了航班数据
只需要请求头携带X-D-Token和Authorization
有的网站是cookie中存入reese84 token请求有的则是放在请求头X-D-Token中
Authorization请求token/initialization接口获取没啥好说的

12.png (35.54 KB, 下载次数: 2)
下载附件
2025-9-24 20:03 上传
解混淆
下面以动态链接方式作为补环境分析目标
(绝不是作者补完动态链接的才发现固态链接更好调)
把js拿到本地看看结构

13.png (67.26 KB, 下载次数: 3)
下载附件
2025-9-24 20:03 上传
第一部分主要用了大量的substr来做混淆

14.png (63.51 KB, 下载次数: 2)
下载附件
2025-9-24 20:03 上传
二三部分则是普通的ob混淆

15.png (107.56 KB, 下载次数: 2)
下载附件
2025-9-24 20:03 上传
所以咱们第一步先解混淆
先处理第一部分代码的混淆
获取第一部分源码,将其包裹在try-catch中安全的通过eval在全局作用域中执行,使顶层变量生效,随后识别并预计算所有 variable.substr(start, length) 调用,将其替换为静态字符串字面量
window = global;
const wrapTryBlock = template(`try{
$BODY;
}catch(err){}`);
function extractEvalSource(ast) {
const initialStmt = ast.program.body[0].expression.callee.body;
const wrappedBlock = wrapTryBlock({ "$BODY": initialStmt });
const generatedSource = generator(wrappedBlock).code;
return generatedSource;
}
const compiledCode = extractEvalSource(ast);
eval(compiledCode);
const stringLiteralOptimizer = {
CallExpression(path) {
const { node } = path;
if (!node.callee.object || typeof node.callee.object.name !== 'string') return;
if (!node.callee.property || node.callee.property.name !== 'substr') return;
const sourceVar = node.callee.object.name;
const startIndex = node.arguments[0].extra.rawValue;
const lengthParam = node.arguments[1].extra.rawValue;
const computedValue = eval(`${sourceVar}.substr(${startIndex}, ${lengthParam})`);
path.replaceWith(types.StringLiteral(computedValue));
}
};
traverse(ast, stringLiteralOptimizer);
看看处理后的效果对比

16.png (118.76 KB, 下载次数: 3)
下载附件
2025-9-24 20:03 上传
接着处理二三部分ob混淆
先遍历所有 VariableDeclarator(变量声明节点)
检查变量是否由 a1_0x89f8 或其“别名变量”初始化
将符合条件的变量名加入 trackedIdentifiers 集合收集起来
const trackedIdentifiers = new Set();
const declarationVisitor = {
VariableDeclarator(path) {
const { node } = path;
if (!node.id?.name || !node.init?.name) return;
const declaredName = node.id.name;
const initializerName = node.init.name;
if (initializerName === 'a1_0x89f8' || trackedIdentifiers.has(initializerName)) {
trackedIdentifiers.add(declaredName);
}
}
};
traverse(ast, declarationVisitor)
然后将第二部分代码执行,声明出解密环境
obEnv = `第二部分代码`
eval(obEnv)
再将符合条件的表达式进行函数调用解密
const callExpressionVisitor = {
CallExpression(path) {
const { node } = path;
const calleeName = node.callee.name;
if (
node.arguments.length !== 1 ||
!node.arguments[0].value ||
typeof node.arguments[0].value !== 'string' && typeof node.arguments[0].value !== 'number'
) {
return;
}
const argValue = node.arguments[0].value;
if (trackedIdentifiers.has(calleeName)) {
if (typeof a1_0x89f8 !== 'function') {
console.warn(`Function a1_0x89f8 is not defined.`);
return;
}
try {
const result = a1_0x89f8(argValue);
path.replaceWith(types.stringLiteral(String(result)));
} catch (err) {
console.error(`Error evaluating ${calleeName}(${argValue}):`, err);
}
}
}
};
traverse(ast, callExpressionVisitor);
最后再来个常量折叠
const constantFold = {
"BinaryExpression|UnaryExpression"(path) {
if(path.isBinaryExpression({operator:"/"})||
path.isUnaryExpression({operator:"-"}) ||
path.isUnaryExpression({operator:"void"}))
{
return;
}
const {confident, value} = path.evaluate();
if (!confident)
return;
if (typeof value == 'number' && (!Number.isFinite(value))) {
return;
}
path.replaceWith(types.valueToNode(value));
},
}
traverse(ast, constantFold);
看看效果对比

17.png (208.41 KB, 下载次数: 2)
下载附件
2025-9-24 20:03 上传
到此解混淆差不多了,能搜索关键字就够了,当然,还有继续优化的空间,比如优化下三目运算,删除无用死代码,一些美化格式啥的,感兴趣可以继续去研究
定位加密位置
先将我们上一步解混淆的文件替换到浏览器中
(如何替换这里不多赘述)
替换有一个关键点需要注意,解混淆文件中有一个路径代码如下

18.png (35.89 KB, 下载次数: 2)
下载附件
2025-9-24 20:03 上传
需要改为和当前时间段的请求路径一致
比如,我现在当前请求路径如下

19.png (31.64 KB, 下载次数: 3)
下载附件
2025-9-24 20:03 上传
那么解混淆文件中的路径就需要改为
3nOsRQ8irc_ZtcVNrFJG8gbVR_3mK-NK2L-00vw-NqY
不然替换后会报错,报错看控制台提示如下就说明得改路径了

20.png (35.59 KB, 下载次数: 2)
下载附件
2025-9-24 20:03 上传
替换完后打上关键xhr断点执行到如下位置

21.png (29.21 KB, 下载次数: 3)
下载附件
2025-9-24 20:03 上传
在抓包分析中说过的第一次post请求传一个固定载荷
放开断点继续又跳到这个位置

22.png (21.2 KB, 下载次数: 2)
下载附件
2025-9-24 20:03 上传
第二次的post请求,有我们需要的关键载荷
所以我们补环境能到这个位置就算成功了
继续分析
js内搜索一下关键字 aih 定位到如下位置

23.png (62.44 KB, 下载次数: 2)
下载附件
2025-9-24 20:03 上传
能看到一些参数,往下看 return 了一个 Promise
下个断点到这看看
清空一下cookie和storage缓存(建议每次重写访问都清一下)
这里有个关键点,动态链接断点断不住怎么办
直接在js文件第一行加一行debuuger;

24.png (17 KB, 下载次数: 2)
下载附件
2025-9-24 20:03 上传
进入js文件第一行了,再定位到Promise位置下断点发现还是跳不过去
仔细一点能发现它进入的路径变了

25.png (15.32 KB, 下载次数: 3)
下载附件
2025-9-24 20:03 上传
进入了一个cachebuster
搜索一下发现其生成位置

26.png (9.98 KB, 下载次数: 3)
下载附件
2025-9-24 20:04 上传
往上找这个函数的进入位置

27.png (34.49 KB, 下载次数: 3)
下载附件
2025-9-24 20:04 上传
发现是在一个reloadScript方法里进行了脚本重载
这里直接说原因
搜索关键字 reese84interrogatorconstructor

28.png (9.71 KB, 下载次数: 3)
下载附件
2025-9-24 20:04 上传
就是这个时间戳导致的
校验这个时间戳发现超时了
修改为取当前时间即可
this["st"] = Math.floor(Date.now() / 1000);
重新再来一遍终于可以断点到Promise了

29.png (14.92 KB, 下载次数: 3)
下载附件
2025-9-24 20:04 上传
Promise中return了一个interrogate执行函数
传入包含aih的一些参数和两个函数
跟着往下走

30.png (30.64 KB, 下载次数: 3)
下载附件
2025-9-24 20:04 上传
眼熟吗,这不就是刚才改时间戳位置的st吗
再往下走,就进入了最主要的加密逻辑方法里了

31.png (29.16 KB, 下载次数: 2)
下载附件
2025-9-24 20:04 上传
创建一个隐藏的 iframe 元素
并为其绑定一个 load 事件监听器
当 iframe 加载完成后,会执行这个回调函数
分析函数结构

32.png (32.58 KB, 下载次数: 3)
下载附件
2025-9-24 20:04 上传
向re数组里push了22个函数(不同站点可能不一样)
最后执行QN方法去遍历执行数组里的每个方法
在push的最后一个方法里能看到 p 值生成

33.png (17.5 KB, 下载次数: 3)
下载附件
2025-9-24 20:04 上传
再往上可以看到p值是由nt这个数组加密而成的

34.png (45 KB, 下载次数: 3)
下载附件
2025-9-24 20:04 上传
而nt里面所有的参数都是那22个函数生成存进来的
补环境
补环境有两种方式
第一种
只拿第一部分代码来补,因为核心加密方法都在第一部分
只需要如下构造interrogate方法和包含aih的参数即可
注意!这是一个异步函数!

35.png (69.62 KB, 下载次数: 2)
下载附件
2025-9-24 20:04 上传
第二种
一二三部分代码全要
不需要构造interrogate方法
只要成功hook到fetch请求做处理即可

37.png (43.84 KB, 下载次数: 3)
下载附件
2025-9-24 20:04 上传
两种方式要补的环境差不多,第二种方式稍微多一点
主要还是那22个函数里的环境
下面我列举一下这22个函数都大概校验了哪些环境
函数一:
通过document.addEventListener 添加很多的监听事件
函数二:
通过window.OfflineAudioContext的Web Audio API离线音频处理方法
渲染计算一些值
函数三:
通过document.addEventListener 添加很多的监听事件
通过document.__selenium_evaluate检测一些自动化环境
函数四:
随机数组生成字符串
函数五:
检验navigator、screen、window窗口的一些参数、生成四个canvas链接
函数六:
拿上一步的一个canvas链接做处理
函数七:
检测window.WebAssembly计算出一些值
函数八:
再拿函数五生成的一个canvas链接做处理
函数九:
再拿函数五生成的一个canvas链接做处理
函数十:
创建一个canvas,获取WebGL 上下文
函数十一:
拿上一步的webgl生成一些值,检测了大量的WebGL 扩展列表属性值和着色器
数值,还生成了两个canvas链接
函数十二:
拿函数十一生成的一个canvas链接生成hash值
函数十三:
随机数组生成字符串
函数十四:
再拿一个canvas链接处理
函数十五:
检测了window.WebGLRenderingContext
检测了navigator一些属性
document.createEvent主动报错
window.ontouchstart
document.createElement('video')
document.createElement('audio')
window.chrome
history.length
检测了一些自动化环境
window.PERSISTENT
window.TEMPORARY
检测了PerformanceObserver
创建一个canvas并获取其2D渲染上下文
document.documentElement.children
document.head.children
document.body.children
document中的一些函数
函数十六:
随机数组生成字符串
函数十七:
向nt数组push一个value为true的参数
函数十八:
检测WebAssembly
winodw.BigInt
函数十九:
document.createElement
navigator一些属性
new window.Audio()
函数二十:
检测了一些原型链如
Function.prototype.toString
Function.prototype.call
Function.prototype.apply
Function.prototype.bind
还有一些window和navigator等环境
函数二十一:
检测了window.performance.now()
window.Object.getPrototypeOf()校验一些属性
函数二十二:
没啥校验了,生成p值
二十二个函数的大概环境说完,再说一些注意点
canvas相关
创建了很多次的链接需要注意顺序

38.png (37.2 KB, 下载次数: 2)
下载附件
2025-9-24 20:04 上传
window.OfflineAudioContext
调用它的方法模拟生成一些数值

39.png (14.84 KB, 下载次数: 3)
下载附件
2025-9-24 20:04 上传
document补的东西最多,主要是canvas和webgl相关

40.png (40.35 KB, 下载次数: 1)
下载附件
2025-9-24 20:04 上传
其它都还简单,最后注意toString保护函数就行
结果验证
补了差不多1千行代码
最后将环境单独打包成一个文件,每次和请求的js代码合并一下执行就行
因为js是异步执行的
所以我是python执行js文件后将生成的参数再写入本地文件读取
纯算法的话最近比较忙等我抽空看有时间再写吧
动态链接的验证

41.png (67.13 KB, 下载次数: 3)
下载附件
2025-9-24 20:04 上传
固定连接的验证

42.png (49.55 KB, 下载次数: 2)
下载附件
2025-9-24 20:04 上传