补环境框架设计方案
这个框架的目标是建立一个高仿真的浏览器沙箱,能够通过大厂(如瑞数、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) - 🚧 进行中
目标:填充 window、document 等基础对象,跑通简单的反爬脚本。
- [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 监听 + 懒加载 + 深度原型链 的策略。
/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 下的所有操作。
核心逻辑实现:
// 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 方案:
// 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;
}
};
})();测试用例:
// 在沙箱中执行:
console.log(window.atob.toString());
// 输出必须是: "function atob() { [native code] }"
// 而不是: "function atob() { ... code ... }"2.3 深度原型链构建 (The "Skeleton")
简单的 obj = {} 无法通过 instanceof 检测。必须还原完整的类继承结构。
通用构建器:
// 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 对象是环境检测的重灾区,特别是 Navigator 和 PluginArray。
关键点:PluginArray 的特殊行为
在浏览器中,navigator.plugins 是一个 PluginArray,它既可以通过数字索引访问,也可以通过插件名访问。普通的 Array 无法模拟这种行为。
// 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 模拟示例:
// 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,Navigator 和 Screen 也是指纹检测的重点。
Navigator 关键属性对抗:
// 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 中的设备类型逻辑上保持一致。
// 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 操作的入口,必须实现为“工厂模式”,根据标签名返回正确的构造函数实例。
// 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 是否存在,或者尝试绑定事件并触发来检测回调是否执行。
// 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 甚至对其赋值来触发跳转。
// 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。
// 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 === window,window.self === window,[window.top](http://window.top) === window。这是极易被忽略的检测点。
// 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)
将所有模块组装起来,加载目标代码并运行。
// 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 应该是不同的(受显卡驱动影响)。如果在所有机器上都返回一样的值,会被判定为伪造。
// 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_WEBGL和UNMASKED_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) 环境中。
// 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")
这是解决“为什么我的环境跑不过”的终极武器。
- Browser Log: 使用 Puppeteer 注入 Proxy 脚本,在真实浏览器中运行目标 JS,记录所有
GET/SET操作序列。 - Sandbox Log: 在我们的 Node 沙箱中运行同一段 JS,记录操作序列。
- Diff: 逐行对比。第一个出现差异的地方,就是我们需要补的缺口。
4.2 RPC 降级方案 (The "Fallback")
针对极高难度的环境(如瑞数 6 代、Akamai 高级版),如果纯补环境成本过高,可以无缝切换到 RPC 模式。
- 原理:在真实浏览器(由 Puppeteer 启动)中注入 WebSocket 客户端。
- 流程:NodeServer 收到请求 -> 发送指令给 Browser -> Browser 执行加密函数 -> 返回 Token -> NodeServer 转发请求。
- 优点:环境 100% 真实。
- 缺点:并发低,需要维护浏览器池。
4.3 自动化流水线 (CI/CD)
建立测试用例库:
tests/cases/ruishu4.jstests/cases/akamai.jstests/cases/5s-shield.js
每次修改框架核心代码后,自动运行所有用例,确保没有引入回归(Regression)。