Skip to content

补环境完全指南

"补环境"是 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 特有指纹

需要隐藏 processBufferglobal 等 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.cookiewindow.eval,推荐补环境方案

6.2 Akamai

  • 核心:收集传感器数据 (sensor_data),生成 _abck Cookie
  • 关键:鼠标/键盘轨迹模拟,环境一致性

6.3 Cloudflare 5秒盾

  • 核心痛点:TLS 指纹校验 + JS 算力挑战
  • 解决方案:使用 tls_clientcurl_cffi 等库模拟浏览器 TLS 指纹

七、工具与框架

方案优点缺点适用场景
JSDOM成熟,API 全特征指纹多简单反爬
vm2 + 手动补环境纯净,可控性高开发成本极高瑞数/Akamai
浏览器自动化真实环境效率低,易被检测少量数据抓取
RPC 方案环境 100% 真实并发低极高难度对抗

八、建议

补环境本质上是一场"猫鼠游戏",维护成本极高。如果遇到极难补的环境,通常建议转向 RPC 方案或使用自动化工具配合指纹浏览器。

评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v3.7.1