补环境完全指南
"补环境"是 JS 逆向工程中的核心高级技术,主要用于应对瑞数(RuiShu)、Akamai、F5 等强对抗型反爬系统。其核心逻辑是在 Node.js 等非浏览器环境中,手动模拟出浏览器特有的对象(BOM、DOM)和行为,欺骗服务器端的检测脚本。
一、核心原理
1.1 为什么需要补环境?
浏览器执行 JS 的上下文(window)与 Node.js 执行 JS 的上下文(global)差异巨大。反爬 JS 代码会检查这些差异来判断当前环境是否为真实浏览器。
1.2 神器:Recursive Proxy (递归代理)
这是补环境最关键的调试工具。通过 Proxy 拦截所有对象属性的读取,我们可以准确知道反爬代码到底检测了什么。
javascript
// 简易递归代理框架,用于捕获缺少的环境
function getProxy(obj, name) {
return new Proxy(obj, {
get: function(target, prop) {
if (typeof prop === 'symbol' || prop === 'inspect') {
return target[prop];
}
console.log(`[读] ${name}.${String(prop)}`);
const val = target[prop];
if (typeof val === 'object' && val !== null) {
return getProxy(val, `${name}.${String(prop)}`);
}
return val;
},
set: function(target, prop, value) {
console.log(`[写] ${name}.${String(prop)} = ${value}`);
target[prop] = value;
return true;
}
});
}
window = getProxy({}, "window");二、常见问题与挑战
2.1 缺失的对象与属性
- 报错中断:JS 代码访问不存在的变量会直接抛出异常
- 隐式检测:通过
typeof window === 'undefined'判断环境
2.2 原型链与继承关系
- instanceof 检测:需要构建完整的类和原型链结构
- toString 保护:
Object.prototype.toString.call(window)必须返回[object Window]
2.3 Native Code 检测
浏览器内置函数的 toString() 会显示 [native code],需要 Hook Function.prototype.toString 进行伪装。
2.4 浏览器指纹与渲染差异
- Canvas/WebGL 指纹:Node.js 模拟结果与真实浏览器有微小差异
- DOM 布局属性:
div.offsetWidth等属性在 Node.js 中无法正确计算
2.5 Node.js 特有指纹
需要隐藏 process、Buffer、global 等 Node.js 特有变量。
三、BOM 模拟重点
3.1 Navigator
javascript
const Navigator = {
get webdriver() { return undefined; },
platform: 'Win32',
vendor: 'Google Inc.',
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
languages: ['zh-CN', 'zh'],
hardwareConcurrency: 8,
plugins: pluginArrayInstance
};3.2 Location
javascript
const Location = {
href: 'https://www.example.com/path?query=1',
protocol: 'https:',
host: 'www.example.com',
pathname: '/path',
search: '?query=1',
hash: ''
};3.3 Screen
javascript
const Screen = {
width: 1920,
height: 1080,
availWidth: 1920,
availHeight: 1040,
colorDepth: 24,
pixelDepth: 24
};四、DOM 模拟重点
4.1 Document 对象
- Cookie 钩子:必须实现 getter/setter
- createElement:需要返回正确的构造函数实例
4.2 事件系统
javascript
const EventTarget = {
addEventListener: function(type, listener) {
if (!this._listeners) this._listeners = {};
if (!this._listeners[type]) this._listeners[type] = [];
this._listeners[type].push(listener);
},
removeEventListener: function(type, listener) {
if (!this._listeners || !this._listeners[type]) return;
const idx = this._listeners[type].indexOf(listener);
if (idx > -1) this._listeners[type].splice(idx, 1);
},
dispatchEvent: function(event) {
if (!this._listeners || !this._listeners[event.type]) return true;
this._listeners[event.type].forEach(fn => fn.call(this, event));
return !event.defaultPrevented;
}
};五、高级对抗
5.1 Native 函数伪装
javascript
(() => {
const _toString = Function.prototype.toString;
const $safe = Symbol('safe_func');
Function.prototype.toString = function() {
if (this[$safe]) {
return `function ${this.name}() { [native code] }`;
}
return _toString.call(this);
};
module.exports = {
protect: (func, name) => {
Object.defineProperty(func, 'name', { value: name || func.name });
func[$safe] = true;
return func;
}
};
})();5.2 Canvas 指纹噪点
javascript
const originalToDataURL = HTMLCanvasElement.prototype.toDataURL;
HTMLCanvasElement.prototype.toDataURL = function(type, quality) {
const context = this.getContext('2d');
if (context) {
const imageData = context.getImageData(0, 0, this.width, this.height);
const data = imageData.data;
for (let i = 0; i < 5; i++) {
const index = Math.floor(Math.random() * data.length);
data[index] = data[index] + (Math.random() > 0.5 ? 1 : -1);
}
context.putImageData(imageData, 0, 0);
}
return originalToDataURL.call(this, type, quality);
};5.3 堆栈伪装
javascript
const originalPrepare = Error.prepareStackTrace;
Error.prepareStackTrace = function(error, stackTraces) {
const cleanStack = stackTraces.filter(frame => {
const filename = frame.getFileName() || '';
return !filename.includes('node_modules') &&
!filename.startsWith('internal/');
});
if (originalPrepare) {
return originalPrepare(error, cleanStack);
}
return error.toString() + '\n' + cleanStack.map(frame => ' at ' + frame).join('\n');
};六、实战案例
6.1 瑞数 (RuiShu)
- 流程:请求首页 -> 返回 HTML -> 浏览器执行 JS (生成 cookie) -> 携带 Cookie 二次请求
- 逆向思路:Hook
document.cookie或window.eval,推荐补环境方案
6.2 Akamai
- 核心:收集传感器数据 (sensor_data),生成
_abckCookie - 关键:鼠标/键盘轨迹模拟,环境一致性
6.3 Cloudflare 5秒盾
- 核心痛点:TLS 指纹校验 + JS 算力挑战
- 解决方案:使用
tls_client、curl_cffi等库模拟浏览器 TLS 指纹
七、工具与框架
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| JSDOM | 成熟,API 全 | 特征指纹多 | 简单反爬 |
| vm2 + 手动补环境 | 纯净,可控性高 | 开发成本极高 | 瑞数/Akamai |
| 浏览器自动化 | 真实环境 | 效率低,易被检测 | 少量数据抓取 |
| RPC 方案 | 环境 100% 真实 | 并发低 | 极高难度对抗 |
八、建议
补环境本质上是一场"猫鼠游戏",维护成本极高。如果遇到极难补的环境,通常建议转向 RPC 方案或使用自动化工具配合指纹浏览器。