1056 字
5 分钟
记一场酣畅淋漓的前端解密

前情提要#

有点标题党了,不过问题不大,这几天有个著名的漫画网站终于不堪其忧,大改了自己的前端页面,名字不好说,怕被爬虫搜到,反正 6 月 13 号这几天吧,如果你漫画阅读器用不了了,章节数显示不出来了,那么我们的漫画源就是一样的。

他新的反爬手段是用接口获取章节数,这一改动主要为了阻止的漫画阅读器的爬虫,一些比较简单的阅读器,他就只会加载静态页面,用 selector 去查找元素显示,他无法处理接口请求(至少在规则上无法描述)。

过程#

我一开始没注意,就 F12 在那试,发现所有的 selector 写法都没效果,我还在想是不是阅读器用的移动端 UA 访问,后来突然意识到是接口。

他接口是这样的: alt text

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 个变量: alt text

其中有插件注入的,这个不用管,只找“可疑”的,如果分不清可以打印出他们的值:

temp0.map(k => window[k])

alt text

这里可疑的有一个 key,一个 Base64,于是我猜测密文是 Base64 加密的,但解密出来是乱码,所以这一步不太对。

那么就应该从 key 开始,找到 key 对应的键,叫 dio,打开调试器,在里面搜索 dio 这个变量: alt text

结果有两个,但这个是函数,所以从这里入手。

从上方可以看出,这部分应该是对接口结果进行处理,上方先判断响应是否成功,成功就执行 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 中的,于是我查了一下,应该没错:

alt text

所以 _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 的一些部分计算来的:

  1. 头部用于计算出 iv
  2. 身体包含了章节信息

key 由外部传入,就是挂在 window 上那个,知道了解密流程,就可以对 results 进行解析了,丢一张图吧:

alt text

总结#

感觉这次运气成分比较多,正好遇上懒狗程序员把东西挂在 window 上,让我能从 window 入手,要不然得认真看源码了。

调试器中的 SourceXXXX 从内容上看是页面运行时的代码/变量,关键是运行时,所以打不了断点,或者说打了会重置,这点比较麻烦,不知道正确的调试方式是什么。

记一场酣畅淋漓的前端解密
https://blog.erio.work/posts/记一场酣畅淋漓的前端解密/
作者
Dupfioire
发布于
2025-06-13
许可协议
CC BY-NC-SA 4.0