我们来用一个案例来解释js扣代码:qimai数据
寻找加密位置
下xhr断点
打开网站,打开开发者工具,抓取翻页数据包(多取几个,对比数据)

发现**analysis**是加密数据,尝试搜索关键字:

全都没找到数据,很可能是混淆了,别白白费力气了,这个参数也不特殊,本人不知道怎么hook,所以我选择xhr
下面来xhr定位:

粘贴到xhr断点的地方然后关闭开发者工具 ,因为我们之前为了对比数据包中的参数把翻页翻完了,数据加载过了,没办法重新触发断点,所以要重新刷新网页,再打开开发者工具,触发翻页接口,打上xhr断点

看参数
我们直接看最外层那个函数的传参:

我们所在的栈是作用域跟到的位置(就是后面有黄色字体的地方) ,而我们看的是所在栈的外层函数 ,只有调用了外层函数 才能进来内层 ,所以外层也是程序运行经过的地方 ,自然放入了栈堆,正好是在所在栈的前一栈 ,所以相当与我们直接看的是前一个栈 ,而这个栈参数是加密的,所在栈就不用看了。我的意思是,既然这种可以,(栈多的时候)我们也可以在右边直接二分法跳栈分析,前一个栈都是加密的,那后一栈就不用看了,节省时间
然后我们看所在栈的上上一个栈:

然后再继续跟发现跟不过去(作用域不在上个栈了)

这是因为异步调用无法跟栈,异步标志:

跟进异步
我们可以在异步执行之前的代码中打上断点:

然后下拉页面触发断点:

然后看加密参数是否是加密的,结果是正常的,没有加密参数,url也是正常的:

那可能加密逻辑就在这个函数中,但是我们看整个函数的逻辑也没有加密的逻辑,然后异步回调之后的e就有了加密参数:

说明加密逻辑藏在异步中,我们来着重看一下这段代码(在js中这段代码会很常见):

首先是一个for循环,然后看for后面那个括号后一半括号在哪儿,是在t.length之后,然后t.length前面是分号,根据for循环语法:
javascript
for (初始化表达式; 条件表达式; 更新表达式) {
// 循环体
}
说明t.length才是for循环的条件,然后n.then是循环体
这里有两点说一下:
1.t.shift()是每次删除一个t中的元素然后返回出来删除的元素
2...then异步调用成功执行括号中第一个·t.shift(),失败调用第二个t.shift(),但是不管调不调用某一个,那一个都会取出一个元素然后放在那里等着调用,意思就是每次运行完.then这一行t中都会少两个
所以每次循环一遍就少俩,此循环就循环三次,刚我们说了加密逻辑藏在异步中,一般来说异步都会成功,所以调用的很可能就是第一个t.shift,那么每次取出的就是0,2,4,我们逐个打断点(如果不放心可以每一个都打上断点)然后运行:

断下来了,说明走这条路而且这里数据也是加密的,那说明加密位置很可能就在这里了,为什么?因为你都打上断点了,他走的第一条路就会被断下来,走之前没加密,走过来就加密了,那很可能就在此了,这里混淆了,这里我们从上往下分析(这个图是因为运行到最下面加密完数据了,t就变成了密文):

上面其实就是个拦截器(函数.interceptors.request.use):
javascript
axios = require('axios');
// 请求拦截器
axios.interceptors.request.use(function (config) { // 请求成功
console.log('success')
config.headers['sign'] = '123456' // 此处可以添加参数加密
return config
})
// 写要拦截的网站
axios.get('https://httpbin.org/get').then(res=>{console.log(res)})
这不就是把数据拦截下来然后加密在传过去的套路吗
确定加密位置
但是有一个问题,直接运行到return t没法逐步分析了,因为运行到最后所有都成了密文,很难找加密起始的位置,而且这里混淆了也看不出加密关键字,我们重新触发一下断点,上一文我们说尽量不要直接刷新网页,因为接口不同,现在跟我步骤:

这样就过掉了所有断点,然后刷新页面,并关掉'停用断点'(如果停在了xhr断点那里就启用'停用断电'过去然后再重新关闭'停用断点')现在是这个样子:

然后下拉页面触发断点(之前断点没删就一点一点运行进来就行):

然后逐步运行观察右侧t.url在哪变成密文

往下运行:

点了五次到这里,目前无密文,继续运行:

说明url中的密文在黄线上方产生(在黄线上的是准备运行的代码,黄线上面的是刚运行的代码),然后在刚执行的代码那行打上断点,重复上面步骤重新进入这个函数,然后直接运行,断在刚打断点这里即可,然后再分析这行代码(因为运行完可能会变):
javascript
-B == t[qt][O](v) && (t[qt] += (-B != t[qt][O](Bn) ? Nn : Bn) + v + B5 + R[V5](e))
首先有一个逻辑与:&&当左侧代码为true时运行右侧代码,然后后面有一个三元运算符:
t[qt] += (-B != t[qt][O] ? Nn : Bn
当-B != t[qt][O]成立时运行问号后代码,不成立运行冒号后代码,后面就没啥了,然后在浏览器分析:

前面是true,接着运行:

false运行冒号:

现在结果是:"/rank/release?"+... 继续运行:

结果为:"/rank/release?analysis="+... 继续运行:

R是window,调用window中的encodeURIComponent 方法,我们查一下这个方法:

意思就是转换一种编码,不用管,然后我们发现了:

下面就是确定真正的加密位置了,e肯定是被赋值的,一点一点往上找:

逗号表达式写出来就是这个样子(把零和括号删掉就能变清晰):
javascript
e = i[Jt](i[Qt](a, d))
然后i中调用了Jt函数,参数为i[Qt](a, d),然后i中又调用了Qt函数,参数为a和d,d不知道哪儿来的,但是a有来源(找赋值的地方):

在最开头的地方打上断点,重新将代码运行过来:

看看a怎么生成的:

就是和py中的lst.jion("")一样,将'列表'中的字符串拼接在一起,然后经过两行代码将a参数整理出来:

然后和d参数一起传入一个函数然后加密出e,其实到这里我们基本已经知道加密的核心代码了:

那就把它拿下来
扣代码
扣下核心代码
扣下核心代码并整理:

封装函数
将核心代码封装成函数,第一行的a是传入的,封装成函数之后为防止a混乱,将式子中的a换一个名字,然后将参数先写死,var一个变量接收:

然后调用函数运行一下看看:

报错了,说明缺东西,缺环境,下面就要补全这些环境
补环境
补环境就缺啥补啥就行,但是需要耐心和细心,上一幅图缺i,那我们回到浏览器看看i是啥:

为啥Jt是函数呢,记住一句话,括号前面是函数,括号里面是参数(个人总结不一定百分百对)还有一种就是你直接进对象i里找,这里混淆之后是Jt,鼠标放上是cv,你就找i中的cv就行:


打开cv,是个函数,点进去(有函数入口(FunctionLocation)是开发者写出来的函数,用的话要进去cv大法,没有的大概就是内置可以直接拿着函数名用的):

复制这一段:

粘贴下来,并改下函数名:

然后再运行:

我们需要先进入cv函数然后加上断点运行进来看看R是个啥:

发现R是个window对象,接着看:
V5(调用的是encodeURIComponent方法)

R[V5](这里没有FunctionLocation就是内置函数可以直接像我一样用)

t

T

直接把这些方法补进我们的代码中:

继续运行:

回原文继续改:

都打上断点,看看它走哪条路:

再运行:

回浏览器去看:

重新触发断点,都看看吧:
p

qt

T

t[Tt]

N

r(这里不是固定的,看是不是固定的可以过掉断点重新触发看看一不一样)

r生成逻辑(可以局部搜索Ctrl+ F或者先在本函数作用域找):


除了r都是固定值,直接补上(可以在控制台输出这些,到时候报错一个个补)再运行:

运行后会报出i未定义,进入浏览器打上断点,运行看i和i[Jt],i[Qt]:
i[Jt]

是cv欸,我们有救了(不就是我们刚写的改名为func_cv的函数么)
i[Qt]

是函数,进去Ctrl + C
将上面的补入代码:

运行一下:

进入浏览器打上断点,运行进来:

$5

补上:

下面开始参数d

那就只有一种可能,它的上一级作用域这个变量就有了,先看一下全局:

炒鸡多啊,大概看下没有,一般来说赋值其实不会太远,我们Ctrl + F局部搜索一下d =

发现这里挺像,离得很近,而且它所在的作用域在刚那个函数的上一级,下个断点,刷新触发断点(这里的数据是刷新页面时就加载好了的,通过翻页没法断下来,只有再刷新让他再加载一遍才能从这里过从而断下来):

控制台输出一下:

然后记住,重新刷新页面看看是不是固定的:

那就是固定的,把d填上,再运行:

看来还需要补N,找一下:

顺手看下I5

说明是join("")方法,补上:

后面就报错哪里就找哪里就行,大多补环境的方法已经讲过,总结一下:
TOP0:缺啥补啥,报错啥回去找到啥
1.显示是函数的就进函数cv大法,但有些是内置函数没有入口就直接复制函数名直接用就行
2.不确定参数变不变就多运行到断点几次看看一不一样
3.对于当前定义域中没有的变量就局部搜索Ctrl+F或者附近找找
4.看起来特殊点的可以全局搜索(和关键字搜索一样)Ctrl+Shift+F
注意:前面有些我是连着找连着补,尽量补一个运行一个,报错之后,再找再补再报错,再找再补再... ...
最后代码是这样的:
javascript
// 扣核心代码
var params_ = '2025-09-302025-09-302025-09-303364allcn'
function func_cv(paramsFromMain) {
t = encodeURIComponent(paramsFromMain)['replace'](/%([0-9A-F]{2})/g, function(n, t) {
return func_o("0x" + t)
});
try {
return btoa(t)
} catch (n) {
return R[W5][K5](t)[U5](Z5)
}
}
function func_o(n) {
t = "",
["66", "72", "6f", "6d", "43", "68", "61", '72', "43", "6f", "64", "65"].forEach(function(n) {
t += unescape("%u00" + n)
});
var t, e = t;
return String[e](n)
}
function func_oZ(n, t) {
t = t || u();
for (var e = (n = n["split"]("")).length, r = t.length, a = "charCodeAt", i = 0; i < e; i++)
n[i] = func_o(n[i][a](0) ^ t[(i + 10) % r][a](0));
return n.join("")
}
function main(params) {
a = func_cv(params) // 调用func_cv函数
a = (a += "@#" + "/rank/release".replace("https://api.qimai.cn", "")) + ("@#" + new Date - (-3 || 0) - 1661224081041) + ("@#" + 3)
e = func_cv(func_oZ(a, 'xyz517cda96efgh'))
return e
}
console.log(main(params_))
总结
逆向其实大方向就是:
1.确定加密位置
2.扣关键代码并封装
3.补环境(看着麻烦,实则有时候确实麻烦)
4.若py需要此接口密文,那就调用核心代码函数
这节课挺长,动手实践一下然后理解大方向即可,多做几个案例就会了,之后学习标准算法有时候就不需要这么麻烦了,若本文有哪些地方有误请及时纠正,或者交流讨论,加油加油