某航司Reese84逆向分析-补环境篇

查看 55|回复 9
作者:LiSAimer   
声明
本文章中所有内容仅供学习交流使用,不用于其他任何目的,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关!若有侵权,请联系作者删除。
逆向目标
目标网站:
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 上传

下载次数, 下载附件

buluo533   

学习了,大佬
dhsfb   

向牛人学习,不断进步
onetwo888   

向大佬学习!!
吃饱奶粉干活   

牛逼,学习了!!!!!!!!!!!
韩梓语   

向大佬学习!!想看看AST代码
hfhskf2005   

值得学习,感谢分享
NewhopeBCY   

看了LZ的帖子,我只想说一句很好很强大!
LKE380   

不明觉厉,向牛人学习
dandan946   

大佬,太强了
您需要登录后才可以回帖 登录 | 立即注册

返回顶部