10-七麦js扣代码
我们来用一个案例来解释js扣代码:qimai数据
寻找加密位置
下xhr断点
打开网站,打开开发者工具,抓取翻页数据包(多取几个,对比数据)发现**analysis**是加密数据,尝试搜索关键字:
全都没找到数据,很可能是混淆了,别白白费力气了,这个参数也不特殊,本人不知道怎么hook,所以我选择xhr
下面来xhr定位:
粘贴到xhr断点的地方然后关闭开发者工具,因为我们之前为了对比数据包中的参数把翻页翻完了,数据加载过了,没办法重新触发断点,所以要重新刷新网页,再打开开发者工具,触发翻页接口,打上xhr断点
看参数
我们直接看最外层那个函数的传参:我们所在的栈是作用域跟到的位置(就是后面有黄色字体的地方),而我们看的是所在栈的外层函数,只有调用了外层函数才能进来内层,所以外层也是程序运行经过的地方,自然放入了栈堆,正好是在所在栈的前一栈,所以相当与我们直接看的是前一个栈,而这个栈参数是加密的,所在栈就不用看了。我的意思是,既然这种可以,(栈多的时候)我们也可以在右边直接二分法跳栈分析,前一个栈都是加密的,那后一栈就不用看了,节省时间
然后我们看所在栈的上上一个栈:
然后再继续跟发现跟不过去(作用域不在上个栈了)
这是因为异步调用无法跟栈,异步标志:
跟进异步
我们可以在异步执行之前的代码中打上断点:然后下拉页面触发断点:
然后看加密参数是否是加密的,结果是正常的,没有加密参数,url也是正常的:
那可能加密逻辑就在这个函数中,但是我们看整个函数的逻辑也没有加密的逻辑,然后异步回调之后的e就有了加密参数:
说明加密逻辑藏在异步中,我们来着重看一下这段代码(在js中这段代码会很常见):
首先是一个for循环,然后看for后面那个括号后一半括号在哪儿,是在t.length之后,然后t.length前面是分号,根据for循环语法:
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):
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中的密文在黄线上方产生(在黄线上的是准备运行的代码,黄线上面的是刚运行的代码),然后在刚执行的代码那行打上断点,重复上面步骤重新进入这个函数,然后直接运行,断在刚打断点这里即可,然后再分析这行代码(因为运行完可能会变):
-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肯定是被赋值的,一点一点往上找:
逗号表达式写出来就是这个样子(把零和括号删掉就能变清晰):
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
注意:前面有些我是连着找连着补,尽量补一个运行一个,报错之后,再找再补再报错,再找再补再… …
最后代码是这样的:
// 扣核心代码
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需要此接口密文,那就调用核心代码函数
这节课挺长,动手实践一下然后理解大方向即可,多做几个案例就会了,之后学习标准算法有时候就不需要这么麻烦了,若本文有哪些地方有误请及时纠正,或者交流讨论,加油加油