1. 为什么我们需要一个“通用”的补环境框架?
做JS逆向的朋友,估计都经历过这样的场景:好不容易找到一个关键的加密函数,兴冲冲地把它抠出来,放到Node.js里一跑,结果直接给你抛出一堆“ReferenceError: window is not defined”或者“TypeError: Cannot read property 'getContext' of null”。那一刻的心情,就像大夏天被泼了一盆冰水,透心凉。
这就是环境差异带来的问题。浏览器里,JavaScript运行在一个非常“富”的环境里,有window、document、navigator、canvas,甚至还有各种稀奇古怪的插件。而我们的Node.js环境,本质上是一个服务器端的JavaScript运行时,它很“干净”,干净到连最基本的window对象都没有。所以,当你把依赖浏览器环境的JS代码直接拿过来用,它当然会“水土不服”。
传统的补环境,就像打补丁。缺window,我就造一个window对象;缺document,我就造一个document。但问题是,每个网站的JS需要的环境补丁都不一样。A网站可能只检测navigator.userAgent,B网站却要检查canvas指纹,C网站更狠,连你系统里装了哪些字体都要查一遍。这就导致我们每分析一个新目标,就得重新写一套补环境的代码,重复劳动不说,还容易遗漏,头发就是这么一根根掉光的。
所以,我就在想,能不能搞一个“通用”的框架?它就像一个万能适配器,或者一个虚拟的浏览器沙盒。我们只需要把这个框架引入,它就能自动模拟出一个近乎完整的浏览器环境,让那些依赖特定浏览器特性的JS代码,能在Node.js里“无感”运行。我们不再需要为每个目标单独写补丁,只需要关注核心的加密逻辑本身。这,就是我折腾这个框架的初衷。
2. 框架的核心设计思路:像搭积木一样构建环境
要模拟一个完整的浏览器环境,听起来是个浩大的工程。但别怕,我们可以把它拆解成一个个独立的模块,像搭积木一样组合起来。我的设计思路主要围绕以下几个核心原则:
第一,分层与模块化。 浏览器环境不是铁板一块,它是有清晰层次的。最底层是JavaScript语言本身和ECMAScript标准对象(比如Object, Array, Function)。往上一层是BOM(浏览器对象模型),比如window、navigator、location、screen。再往上是DOM(文档对象模型),比如document、Element、Event。此外,还有各种Web API,比如Canvas、WebGL、AudioContext、WebRTC等等。我们的框架也按这个层次来构建,每个模块只负责模拟自己那一层的东西,这样结构清晰,也方便维护和扩展。
第二,黑盒与透明。 我们的目标是“黑盒”过检测。也就是说,对于运行在框架里的JS代码,它不应该感知到自己是在一个模拟环境里。它调用navigator.userAgent,就应该返回一个合理的、像真实浏览器的字符串;它调用canvas.toDataURL(),就应该生成一个看起来正常的、带有指纹特征的图像数据。框架要做的,就是让这些API的输入输出行为,与真实浏览器高度一致,从而骗过环境检测代码。
第三,可配置与可插拔。 不是所有网站都需要全套环境模拟。有些可能只检测简单的userAgent,有些则对WebGL渲染器指纹有严苛校验。因此,框架的各个模块应该是可配置、可开关的。你可以像配置插件一样,选择只启用navigator和canvas模块,而关闭复杂的字体枚举模拟,以提升性能。这种灵活性对于应对不同场景至关重要。
第四,性能与内存考量。 模拟一个完整环境是有代价的。比如,为了模拟字体,你可能需要加载一个庞大的字体列表;模拟canvas指纹,可能需要真的在内存中绘制图像。框架在设计时就需要权衡模拟的逼真度和运行效率。我的做法是提供“精度”选项,在调试阶段可以开启高精度模拟以通过复杂检测,在生产环境或批量任务中则可以降低精度以换取速度。
基于这些思路,整个框架的架构图大致是这样的:一个核心的BrowserEmulator类作为总控,它下面挂载着BOMProvider、DOMProvider、CanvasProvider、FontProvider、PluginProvider等多个“供应商”(Provider)。每个Provider负责生成和管理自己领域内的模拟对象和API。核心类负责协调这些Provider,并最终导出一个完整的、可注入到JS运行环境中的全局对象。
3. 关键模块实现详解:从BOM到Canvas指纹
理论说再多,不如看看代码怎么实现。我们来拆解几个最核心、也最常被检测的模块。
3.1 BOM(浏览器对象模型)模拟:打造你的“虚拟身份”
BOM是JS与浏览器窗口交互的基础,也是反爬虫最先检查的地方。window对象是全局对象的化身,在浏览器里,var a = 1 实际上等于 window.a = 1。在Node.js里,我们需要先创建一个对象来扮演window,并让它成为新的全局对象。
// 核心:创建一个伪造的window对象,并使其成为全局对象
const vm = require('vm'); // 使用Node.js的vm模块创建沙盒
class BOMProvider {
constructor(config) {
this.config = config; // 包含userAgent, viewport大小等配置
this.window = {};
this.initWindow();
}
initWindow() {
// 1. 模拟window自身属性
this.window = Object.create(global); // 继承Node.js全局对象的一些方法
this.window.self = this.window; // window.self指向自身
this.window.window = this.window; // window.window也指向自身
// 2. 模拟navigator对象 - 重中之重
this.window.navigator = {
userAgent: this.config.userAgent || 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ...',
platform: 'Win32',
language: 'zh-CN',
languages: ['zh-CN', 'zh'],
hardwareConcurrency: navigator.hardwareConcurrency || 8, // 小心!这里用到了真实的navigator,在生产环境需要伪造
deviceMemory: 8,
// 模拟插件 - 很多检测会遍历navigator.plugins
plugins: this.simulatePlugins(),
mimeTypes: this.simulateMimeTypes(),
// 关键:让navigator.toString()返回合理值
toString: () => '[object Navigator]'
};
// 3. 模拟location对象


1万+

被折叠的 条评论
为什么被折叠?



