Skip to content

补环境框架设计方案

这个框架的目标是建立一个高仿真的浏览器沙箱,能够通过大厂(如瑞数、Akamai)的环境检测。

0. 项目阶段规划与进度汇总

核心目标:构建高仿真浏览器沙箱,通过瑞数/Akamai等大厂风控检测。

关键难点Proxy检测绕过、原生(Native)行为模拟、图形/渲染指纹对齐。

📅 阶段一:骨架搭建与核心机制 (Phase 1: Core) - ✅ 已完成

目标:建立沙箱隔离环境,实现“可观测”与“可伪装”。

  • [x] 沙箱架构:确定 Node.js + VM2 隔离方案。
  • [x] 上帝视角:实现 Global Proxy,递归拦截所有属性访问,输出调试日志。
  • [x] 身份伪装:实现 Native Protection (Hook toString),让模拟函数显示 [native code]
  • [x] 原型链构建:实现 Prototype Builder,还原 instanceof 类继承关系。

🔨 阶段二:环境填充与API模拟 (Phase 2: BOM/DOM) - 🚧 进行中

目标:填充 windowdocument 等基础对象,跑通简单的反爬脚本。

  • [x] BOM 核心:实现 Navigator (Plugins/MimeType), Screen, History, Location
  • [x] DOM 基础:实现 Document.createElement, Node.appendChild, Element.getBoundingClientRect
  • [x] 事件系统:模拟 EventTarget 及简单的事件冒泡机制。

🎨 阶段三:指纹对抗与细节打磨 (Phase 3: Fingerprinting) - 📅 待启动

目标:通过 Canvas、WebGL、Font 等高级指纹检测。

  • [x] 渲染指纹:Canvas 噪点注入,WebGL 参数随机化。
  • [ ] 环境一致性:确保 JS 执行耗时、报错堆栈(Stack Trace)与真实浏览器一致。
  • [x] Node特征清洗:彻底隐藏 process, Buffer 等 Node.js 特有变量。

⚔️ 阶段四:实战验证 (Phase 4: Production) - 📅 待启动

  • [ ] 目标攻坚:针对特定站点(如某数 4/5/6 代)进行逆向测试。
  • [ ] 自动化测试:建立 CI/CD,对比真机与沙箱的日志差异(Log Diff)。

1. 核心架构设计

采用 Proxy 监听 + 懒加载 + 深度原型链 的策略。

jsx
/env-framework
├── /config             # 补丁与UA配置
├── /core               # 核心引擎
│   ├── vm-sandbox.js   # 纯净沙箱封装
│   ├── proxy-trap.js   # 递归代理拦截器(核心)
│   └── event-loop.js   # 事件循环模拟
├── /browser            # 浏览器对象库
│   ├── /bom            # window, location, navigator
│   ├── /dom            # document, element, events
│   └── /crypto         # subtle crypto 模拟
├── /tools              # 辅助工具
│   ├── protection.js   # toString 保护
│   └── logger.js       # 调试日志
└── main.js             # 启动入口

2. 核心模块详解

2.1 全局日志代理 (The "Eye")

这是补环境中最关键的一步。我们需要知道目标代码到底访问了哪些环境特征。通过 Proxy 递归拦截 window 下的所有操作。

核心逻辑实现:

jsx
// core/proxy-trap.js
const logger = require('../tools/logger');

// 防止死循环:记录已代理的对象
const proxiedMap = new WeakMap();

function createProxy(target, name) {
    if (proxiedMap.has(target)) return proxiedMap.get(target);

    const p = new Proxy(target, {
        get(target, prop, receiver) {
            // 排除 Symbol 和自身属性访问,减少噪声
            if (prop !== Symbol.toPrimitive && prop !== 'toJSON') {
                // 记录读取操作,包括返回值类型,方便调试
                const value = Reflect.get(target, prop, receiver);
                logger.log('GET', `${name}.${String(prop)}`, value);
                
                // 递归代理:如果获取的是对象且不为空,继续套上一层 Proxy
                if (typeof value === 'object' && value !== null) {
                    return createProxy(value, `${name}.${String(prop)}`);
                }
                return value;
            }
            return Reflect.get(target, prop, receiver);
        },
        set(target, prop, value, receiver) {
            logger.log('SET', `${name}.${String(prop)}`, value);
            return Reflect.set(target, prop, value, receiver);
        },
        // 关键:拦截 `instanceof` 检查,防止 Proxy 被识破
        getPrototypeOf(target) {
             return Reflect.getPrototypeOf(target);
        }
    });
    
    proxiedMap.set(target, p);
    return p;
}

module.exports = createProxy;

难点解决

  • 递归死循环:使用 WeakMap 缓存已代理对象。
  • 检测规避:某些反爬会检测 Proxy 特征。在生产环境(非调试模式)下,应关闭此 Proxy 功能,直接使用真实模拟对象,或者使用更底层的 V8 引擎修改(如魔改 Node 源码)。

2.2 Native 保护机制 (The "Mask")

所有模拟的函数(如 window.atob),其 toString() 必须返回 [native code],否则会被秒杀。

实现 Hook 方案:

jsx
// tools/protection.js
(() => {
    // 1. 保存原生的 toString
    const _toString = Function.prototype.toString;
    
    // 2. 定义 hook 逻辑
    // 使用 Symbol 标记哪些函数是我们模拟的
    const $safe = Symbol('safe_func');

    Function.prototype.toString = function() {
        // A. 如果是我们标记过的模拟函数,返回标准 Native 字符串
        if (this[$safe]) {
            return `function ${this.name}() { [native code] }`;
        }
        // B. 如果是 Proxy 对象,可能需要特殊处理(视具体情况)
        
        // C. 否则调用原生逻辑
        return _toString.call(this);
    };

    // 3. 导出工具函数,用于包装模拟函数
    module.exports = {
        protect: (func, name) => {
            // 修复 name 属性
            Object.defineProperty(func, 'name', { value: name || func.name });
            // 标记为“原生”
            func[$safe] = true;
            return func;
        }
    };
})();

测试用例

jsx
// 在沙箱中执行:
console.log(window.atob.toString()); 
// 输出必须是: "function atob() { [native code] }"
// 而不是: "function atob() { ... code ... }"

2.3 深度原型链构建 (The "Skeleton")

简单的 obj = {} 无法通过 instanceof 检测。必须还原完整的类继承结构。

通用构建器:

jsx
// core/prototype-builder.js
const protection = require('../tools/protection');

function buildClass(className, parentClass, instanceProperties = {}) {
    // 1. 定义类构造函数
    const Cls = function() {
        // 模拟 Illegal constructor 调用保护(部分浏览器对象不允许直接 new)
        throw new TypeError(`Illegal constructor: ${className}`);
    };
    
    // 2. 建立继承关系:Cls -> Parent -> ... -> Object
    Object.setPrototypeOf(Cls, parentClass); // 静态属性继承
    Cls.prototype = Object.create(parentClass.prototype); // 原型链继承
    
    // 3. 修复 constructor 和 toStringTag
    Object.defineProperty(Cls.prototype, 'constructor', { 
        value: Cls, 
        enumerable: false,
        writable: true,
        configurable: true
    });
    Object.defineProperty(Cls.prototype, Symbol.toStringTag, { 
        value: className, 
        configurable: true 
    });

    // 4. 保护类本身
    protection.protect(Cls, className);
    
    return Cls;
}

2.4 BOM 对象模拟 (The "Face")

BOM 对象是环境检测的重灾区,特别是 NavigatorPluginArray

关键点:PluginArray 的特殊行为

在浏览器中,navigator.plugins 是一个 PluginArray,它既可以通过数字索引访问,也可以通过插件名访问。普通的 Array 无法模拟这种行为。

jsx
// browser/bom/plugin-array.js
const prototypeBuilder = require('../../core/prototype-builder');

// 1. 定义 Plugin 类
const Plugin = prototypeBuilder.buildClass('Plugin', Object, {
    description: '',
    filename: '',
    name: '',
    length: 0
});

// 2. 定义 PluginArray 类
// 注意:PluginArray 在 Chrome 中实际上并不是 Array 的子类,它的原型直接是 Object (或特定内部类)
// 但为了方便,我们可以模拟 Array 的行为,同时修改原型链
const PluginArray = prototypeBuilder.buildClass('PluginArray', Object, {
    length: 0,
    item: function(index) { return this[index]; },
    namedItem: function(name) { return this[name]; },
    refresh: function() {}
});

// 3. 实现“名+索引”双重访问的魔法
function createPluginArray(pluginsData) {
    const pluginArray = new PluginArray();
    const plugins = pluginsData.map(data => {
        const p = new Plugin();
        Object.assign(p, data);
        return p;
    });

    plugins.forEach((p, index) => {
        pluginArray[index] = p;         // 索引访问
        pluginArray[p.name] = p;        // 名称访问
    });
    
    pluginArray.length = plugins.length;
    return pluginArray;
}

2.5 DOM 基础模拟 (The "Body")

DOM 的难点在于节点关系的维护(parentNode, childNodes)和递归创建。

HTMLDivElement 模拟示例:

jsx
// browser/dom/html-div-element.js
const prototypeBuilder = require('../../core/prototype-builder');
const HTMLElement = require('./html-element');

const HTMLDivElement = prototypeBuilder.buildClass('HTMLDivElement', HTMLElement, {
    align: '',
    // 难点:getBoundingClientRect
    // 浏览器会根据 CSS 计算真实位置。沙箱中通常只能返回 0 或伪造值。
    getBoundingClientRect: function() {
        return {
            x: 0, y: 0, 
            width: 0, height: 0, 
            top: 0, right: 0, bottom: 0, left: 0,
            toJSON: function() { return this; }
        };
    }
});

2.6 更多 BOM 对象细节 (The "Details")

除了 Plugins,NavigatorScreen 也是指纹检测的重点。

Navigator 关键属性对抗:

jsx
// browser/bom/navigator.js
const prototypeBuilder = require('../../core/prototype-builder');

// 重点:webdriver 属性
// 在 Puppeteer/Selenium 中,该属性默认为 true。
// 在正常浏览器中,该属性通常为 undefined (非自动化) 或 false。
// 简单的 delete navigator.webdriver 可能无效,需要模拟 getter。

const Navigator = prototypeBuilder.buildClass('Navigator', Object, {
    // 模拟 getter,同时确保 toString 保护
    get webdriver() { return undefined; }, 
    platform: 'Win32',
    vendor: 'Google Inc.',
    userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
    languages: ['zh-CN', 'zh'],
    // 硬件并发数,通常为 4, 8, 16
    hardwareConcurrency: 8,
    // 引用之前创建的 pluginArray
    plugins: { get: () => pluginArrayInstance } 
});

Screen 对象一致性:

Screen 的尺寸需要与 window.innerWidth/innerHeight 甚至 navigator.userAgent 中的设备类型逻辑上保持一致。

jsx
// browser/bom/screen.js
const Screen = prototypeBuilder.buildClass('Screen', Object, {
    width: 1920, 
    height: 1080,
    availWidth: 1920, 
    availHeight: 1040, // 减去任务栏高度
    colorDepth: 24, 
    pixelDepth: 24
});

2.7 DOM 构建工厂 (Document Factory)

document.createElement 是 DOM 操作的入口,必须实现为“工厂模式”,根据标签名返回正确的构造函数实例。

jsx
// browser/dom/document.js
// 引入各个具体的元素类
const HTMLDivElement = require('./html-div-element');
const HTMLCanvasElement = require('./html-canvas-element');
const HTMLAnchorElement = require('./html-anchor-element');

const Document = prototypeBuilder.buildClass('Document', Node, {
    // Cookie 管理:核心功能,需要模拟存储
    get cookie() { return this._cookieJar || ''; },
    set cookie(val) { 
        // 简单的解析逻辑,实际应处理 domain/path/expiry
        this._cookieJar = val; 
    },
    
    // 核心工厂方法
    createElement: function(tagName) {
        if (!tagName) return null;
        tagName = tagName.toLowerCase();
        
        switch(tagName) {
            case 'div': return new HTMLDivElement();
            case 'canvas': return new HTMLCanvasElement();
            case 'a': return new HTMLAnchorElement();
            // ... 补全常用标签 ...
            default: return new HTMLUnknownElement();
        }
    },
    
    getElementById: function(id) { 
        // 简单模拟:实际需要遍历 DOM 树
        return null; 
    }
});

2.8 事件系统 (EventTarget)

几乎所有的 DOM 节点都继承自 EventTarget。有些反爬会检测 addEventListener 是否存在,或者尝试绑定事件并触发来检测回调是否执行。

jsx
// core/event-target.js
const prototypeBuilder = require('../prototype-builder');

const EventTarget = prototypeBuilder.buildClass('EventTarget', Object, {
    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 指向和参数传递
        this._listeners[event.type].forEach(fn => fn.call(this, event));
        return !event.defaultPrevented;
    }
});

2.9 剩余核心对象 (Location, History, Window)

Location 对象:

很多反爬脚本会检查 location.href 甚至对其赋值来触发跳转。

jsx
// browser/bom/location.js
const Location = prototypeBuilder.buildClass('Location', Object, {
    href: 'https://www.example.com/path?query=1',
    protocol: 'https:',
    host: 'www.example.com',
    hostname: 'www.example.com',
    port: '',
    pathname: '/path',
    search: '?query=1',
    hash: '',
    
    reload: function() { /* log reload */ },
    replace: function(url) { this.href = url; /* log replace */ },
    assign: function(url) { this.href = url; /* log assign */ }
});

History 对象:

需要模拟 back, forward, go 以及 HTML5 的 pushState/replaceState

jsx
// browser/bom/history.js
const History = prototypeBuilder.buildClass('History', Object, {
    length: 1,
    state: null,
    scrollRestoration: 'auto',
    
    back: function() {},
    forward: function() {},
    go: function() {},
    pushState: function(state, title, url) { this.state = state; },
    replaceState: function(state, title, url) { this.state = state; }
});

Window 循环引用 (The "Loop"):

浏览器中 window.window === windowwindow.self === window,[window.top](http://window.top) === window。这是极易被忽略的检测点。

jsx
// core/vm-sandbox.js (补充)
// 在创建沙箱上下文时:
const window = createProxy(windowInstance, 'window');

// 关键:手动建立循环引用
window.window = window;
window.self = window;
window.top = window;
window.parent = window; // 如果没有 iframe,parent 也是自己

// 注入到 VM 上下文
const vm = new VM({
    sandbox: window
});

2.10 启动入口 (main.js)

将所有模块组装起来,加载目标代码并运行。

jsx
// main.js
const fs = require('fs');
const { VM } = require('vm2');
const createProxy = require('./core/proxy-trap');

// 引入我们构建的各个浏览器对象实例
const windowInstance = require('./browser/bom/window');
// 注意:其他对象如 document, navigator 应该已经挂载在 windowInstance 上

// 读取目标 JS 代码(例如瑞数 JS)
const targetCode = fs.readFileSync('./target/target-script.js', 'utf8');

// 准备沙箱全局对象
// 关键:使用 Proxy 包裹 window,开启“上帝视角”
const proxyWindow = createProxy(windowInstance, 'window');

// 修复循环引用(在 Proxy 之后)
proxyWindow.window = proxyWindow;
proxyWindow.self = proxyWindow;
proxyWindow.top = proxyWindow;

const vm = new VM({
    timeout: 10000, // 防止死循环
    sandbox: {
        ...proxyWindow, // 将 window 属性展开到全局
        window: proxyWindow,
        // 补齐常用全局函数
        console: { log: () => {}, error: () => {} }, // 生产环境建议静默
        setTimeout: setTimeout,
        clearTimeout: clearTimeout,
        setInterval: setInterval,
        clearInterval: clearInterval,
        // 彻底隐藏 Node 特征:不传入 process, Buffer 等
    }
});

console.log('🚀 开始执行目标代码...');
try {
    const result = vm.run(targetCode);
    console.log('✅ 执行完成,结果:', result);
} catch (e) {
    console.error('❌ 发生错误:', e.message);
    // 可以在这里分析堆栈,看是哪个模拟函数出了问题
}

3. 进阶指纹对抗 (The "Fingerprint")

当基础环境补全后,大厂会通过图形学差异来识别机器人。

3.1 Canvas 指纹噪点 (Noise Injection)

相同的 Canvas 绘图指令在不同设备上生成的 Base64 应该是不同的(受显卡驱动影响)。如果在所有机器上都返回一样的值,会被判定为伪造。

jsx
// browser/dom/html-canvas-element.js
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;
        
        // 注入微量噪点:随机修改几个像素的 RGB 值
        // 这种修改肉眼不可见,但会改变最终的 Hash
        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);
    }
    // 调用原生方法生成 Base64
    return originalToDataURL.call(this, type, quality);
};

3.2 WebGL 参数伪装

Node-canvas 通常没有真实的 GPU 环境。我们需要 Hook WebGLRenderingContext

  • getParameter: 拦截 UNMASKED_VENDOR_WEBGLUNMASKED_RENDERER_WEBGL
  • 目标值: 不要返回 "Google SwiftShader" (纯软件渲染),要返回真实的显卡型号,如 "Intel Inc.", "Intel(R) Iris(TM) Plus Graphics 640"。

3.3 Node.js 特征清洗 (De-Nodeify)

vm2 虽然隔离了环境,但有时仍会残留痕迹。

  • 清理全局变量: 确保沙箱内无法访问 process, global, Buffer, setImmediate
  • 堆栈伪装: 此时 Node 抛出的 Error stack 会带有 /node_modules/vm2/... 的路径。高级对抗需要重写 Error.prepareStackTrace,过滤掉这些敏感路径。

3.4 环境一致性 (Consistency)

堆栈伪装 (Stack Trace Scrubbing)

这是非常隐蔽的检测点。当代码报错(try-catch)时,error.stack 会暴露当前是在 Node.js (vm2) 环境中。

jsx
// tools/stack-protection.js
const originalPrepare = Error.prepareStackTrace;

Error.prepareStackTrace = function(error, stackTraces) {
    // 过滤掉所有包含 node_modules 或 internal 的栈帧
    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');
};

时序与性能 (Timing)

某些反爬会检测特定操作(如 eval)的耗时。如果沙箱执行太慢(因为 Proxy 开销),可能会被标记。

  • 策略:在关键函数(如解密算法)上,尽量移除不必要的日志 Proxy,换回原生性能。
  • 精度:对齐 [performance.now](http://performance.now)() 的精度与 Chrome 一致(通常是微秒级,但在某些指纹防护下会降低精度)。

4. 实战验证与生产化 (Phase 4: Production)

补环境不是一次性的,而是一个持续对抗的过程。我们需要工具来辅助迭代。

4.1 Log Diff 系统 (The "Debugger")

这是解决“为什么我的环境跑不过”的终极武器。

  1. Browser Log: 使用 Puppeteer 注入 Proxy 脚本,在真实浏览器中运行目标 JS,记录所有 GET/SET 操作序列。
  2. Sandbox Log: 在我们的 Node 沙箱中运行同一段 JS,记录操作序列。
  3. Diff: 逐行对比。第一个出现差异的地方,就是我们需要补的缺口。

4.2 RPC 降级方案 (The "Fallback")

针对极高难度的环境(如瑞数 6 代、Akamai 高级版),如果纯补环境成本过高,可以无缝切换到 RPC 模式。

  • 原理:在真实浏览器(由 Puppeteer 启动)中注入 WebSocket 客户端。
  • 流程:NodeServer 收到请求 -> 发送指令给 Browser -> Browser 执行加密函数 -> 返回 Token -> NodeServer 转发请求。
  • 优点:环境 100% 真实。
  • 缺点:并发低,需要维护浏览器池。

4.3 自动化流水线 (CI/CD)

建立测试用例库:

  • tests/cases/ruishu4.js
  • tests/cases/akamai.js
  • tests/cases/5s-shield.js

每次修改框架核心代码后,自动运行所有用例,确保没有引入回归(Regression)。

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