前情提要
有点标题党了,不过问题不大,这几天有个著名的漫画网站终于不堪其忧,大改了自己的前端页面,名字不好说,怕被爬虫搜到,反正 6 月 13 号这几天吧,如果你漫画阅读器用不了了,章节数显示不出来了,那么我们的漫画源就是一样的。
他新的反爬手段是用接口获取章节数,这一改动主要为了阻止的漫画阅读器的爬虫,一些比较简单的阅读器,他就只会加载静态页面,用 selector 去查找元素显示,他无法处理接口请求(至少在规则上无法描述)。
过程
我一开始没注意,就 F12 在那试,发现所有的 selector 写法都没效果,我还在想是不是阅读器用的移动端 UA 访问,后来突然意识到是接口。
他接口是这样的:
results
很明显已经加密了,好在是前端,可以在页面中捣鼓,反正一定包含了所需的密钥。
第一步先猜测,要敢于去猜,然后再找证据尝试推翻猜测,推不翻就没问题。
我猜他们程序员比较懒狗,变量清理不会太干净,可能把一些信息保存在了全局变量 window
上,所以我用以下代码检查:
(function getExtraVars () {
const keys = []
const iframe = document.createElement('iframe')
iframe.onload = () => {
const iframeKeys = Object.keys(iframe.contentWindow)
Object.keys(window).forEach(key => {
if (!iframeKeys.includes(key)) {
keys.push(key)
}
})
document.body.removeChild(iframe)
console.log(keys)
}
iframe.src = 'about:blank'
document.body.appendChild(iframe)
})()
列出了大概 28 个变量:
其中有插件注入的,这个不用管,只找“可疑”的,如果分不清可以打印出他们的值:
temp0.map(k => window[k])
这里可疑的有一个 key
,一个 Base64,于是我猜测密文是 Base64 加密的,但解密出来是乱码,所以这一步不太对。
那么就应该从 key
开始,找到 key
对应的键,叫 dio
,打开调试器,在里面搜索 dio
这个变量:
结果有两个,但这个是函数,所以从这里入手。
从上方可以看出,这部分应该是对接口结果进行处理,上方先判断响应是否成功,成功就执行 function (_0x55a4d4)
,所以我估计这里的参数 _0x55a4d4
就是响应结果中的密文 results
。
接着往下走,下面都是混淆后的变量,这里我没什么办法,只能对着 window
内已暴露出的变量去对照,这里的 _0x4e8a
是一个函数,暴露在了 window
上,所以可以尝试去执行,然后慢慢还原代码全貌。(为什么不用断点,因为这是浏览器解析出来的临时代码,不是源码,所以无法打断点调试)
先从 dio
所在行入手:
0x3def88 = _0x2e033c[_0x4e8a('0x59')][_0x4e8a('0x8')][_0x4e8a('0x5c')](dio)
这里还原之后是:
0x3def88 = _0x2e033c['enc']['Utf8']['parse'](dio)
这段 API 很眼熟,推测是 CryptoJS 中的,于是我查了一下,应该没错:
所以 _0x2e033c
的值是 Crypto
,顺带解析其他 _0x4e8a
的函数、美化变量名,结果变成了:
function (encryptedText) {
var Crypto = /* Crypto 对象*/,
headPart = encryptedText['substring'](0, 16),
bodyPart = encryptedText['substring'](16, encryptedText['length']),
dioResult = Crypto['enc']['Utf8']['parse'](dio),
headResult = Crypto['enc']['Utf8']['parse'](headPart),
_0x2fa5a9 = function (bodyPartStr) {
var hexResult = Crypto['enc']['Hex']['parse'](bodyPartStr),
base64Result = Crypto['enc']['Base64']['stringify'](hexResult);
return Crypto['AES']['decrypt'](
base64Result,
dioResult,
{
'iv': headResult,
'mode': Crypto['mode']['CBC'],
'padding': Crypto['pad']['Pkcs7']
}
) ['toString'](Crypto['enc']['Utf8']) ['toString']()
}(bodyPart)
0x14dbee = JSON[_0x4e8a('0x5c')](_0x2fa5a9),
// ...省略
}
到了这一步,已经可以确定 _0x2fa5a9
就是要找的解密函数了,很明显用到的加密就是 AES,只不过 AES 需要的参数都是由 results
的一些部分计算来的:
- 头部用于计算出
iv
- 身体包含了章节信息
key
由外部传入,就是挂在 window
上那个,知道了解密流程,就可以对 results
进行解析了,丢一张图吧:
总结
感觉这次运气成分比较多,正好遇上懒狗程序员把东西挂在 window
上,让我能从 window
入手,要不然得认真看源码了。
调试器中的 SourceXXXX
从内容上看是页面运行时的代码/变量,关键是运行时,所以打不了断点,或者说打了会重置,这点比较麻烦,不知道正确的调试方式是什么。