1242 字
3 分钟
补环境代理完整实现
补环境深度解析:从 Proxy 监控到防检测实现
一、为什么需要环境代理
1.1 问题背景
在 JS 逆向过程中,我们需要将浏览器环境中的代码移植到 Node.js 中运行。但直接运行会遇到两个核心问题:
问题一:环境缺失
// 浏览器中有但 Node.js 中没有typeof window // 浏览器: "object", Node.js: "undefined"typeof document // 浏览器: "object", Node.js: "undefined"typeof navigator // 浏览器: "object", Node.js: "undefined"问题二:环境检测
// 安全代码会检测环境真实性Object.prototype.toString.call(window) // 浏览器: "[object Window]" // Node.js: "[object global]"
'process' in window // 浏览器: false, Node.js: true
navigator.webdriver // 自动化工具: true, 正常浏览器: false1.2 代理的作用
环境代理的核心作用是:
- 监控访问:追踪代码对浏览器对象的所有访问
- 模拟行为:提供符合浏览器规范的对象和方法
- 隐藏特征:屏蔽 Node.js 特有的属性和行为
- 防检测:绕过各种环境检测手段
二、核心代理机制:Proxy 监控
2.1 Proxy 的基本用法
function watch(obj, name) { const loggedOperations = new Set(); // 去重记录
return new Proxy(obj, { get: function(target, property) { // 拦截属性读取 const value = target[property]; console.log(`[访问] ${name}.${String(property)}`); return value; }, set: function(target, property, newValue) { // 拦截属性设置 console.log(`[设置] ${name}.${String(property)} = ${newValue}`); return Reflect.set(target, property, newValue); }, has: function(target, property) { // 拦截 in 操作符 console.log(`[检测] '${property}' in ${name}`); return Reflect.has(target, property); }, ownKeys: function(target) { // 拦截属性枚举 const keys = Reflect.ownKeys(target); console.log(`[枚举] ${name} 的属性: ${keys}`); return keys; } });}2.2 不同类型属性的处理策略
2.2.1 Symbol 类型处理
get: function(target, property) { const value = target[property]; const type = typeof value;
if (type === "symbol") { const symbolDescription = property.description || 'no description'; logMessage = `对象=>${name},读取属性:${symbolDescription},这是一个 Symbol 类型的值`; operationId = `get:${name}:symbol:${symbolDescription}`; } // ...}为什么要特殊处理 Symbol?
- Symbol 是 ES6 引入的新类型,用于创建唯一标识符
- 浏览器对象大量使用 Symbol(如
Symbol.toStringTag、Symbol.toPrimitive) - 某些检测会检查特定 Symbol 是否存在
2.2.2 Function 类型处理
if (type === "function") { const functionName = value.name || 'anonymous'; logMessage = `对象=>${name},读取属性:${property.toString()},这是一个名为 ${functionName} 的函数`; operationId = `get:${name}:function:${property.toString()}:${functionName}`;}函数处理的关键点:
- 记录函数名便于调试
- 函数需要绑定正确的 this 上下文
- 某些检测会验证函数的 toString 输出
2.2.3 普通对象处理
else { logMessage = `对象=>${name},读取属性:${String(property)},值为:${displayValue},类型为:${type}`; operationId = `get:${name}:${String(property)}:${type}:${String(value)}`;}普通值的处理原则:
- 记录类型信息
- 截断过长的字符串(避免日志刷屏)
- 使用唯一 ID 去重,避免重复输出
2.3 去重机制的重要性
const loggedOperations = new Set();
// 检查是否已记录if (!loggedOperations.has(operationId)) { loggedOperations.add(operationId); logToConsole(logMessage);}为什么需要去重?
- 代码可能反复访问同一个属性(如循环中访问
window.document) - 不去重会导致日志爆炸,难以分析
- 使用 Set + 唯一 ID 实现高效去重
三、防检测策略
3.1 Function.toString 检测
3.1.1 问题分析
浏览器中的原生函数 toString() 返回特殊格式:
window.alert.toString()// "function alert() { [native code] }"
document.createElement.toString()// "function createElement() { [native code] }"而 Node.js 中的函数 toString() 返回实际代码:
(function(){}).toString()// "function(){}"3.1.2 解决方案
const safeFunction = (function () { // 保存原始的 call 方法 Function.prototype.$call = Function.prototype.call; const $toString = Function.toString;
// 创建一个 Symbol 作为标记 const myFunction_toString_symbol = Symbol('('.concat('', ')'));
// 自定义 toString 方法 const myToString = function myToString() { // 如果函数有标记,返回原生代码格式 return typeof this === 'function' && this[myFunction_toString_symbol] || $toString.$call(this); }
// 删除原始 toString,替换为自定义版本 delete Function.prototype['toString'];
// 使用 Object.defineProperty 设置(确保不可枚举) Object.defineProperty(Function.prototype, "toString", { "enumerable": false, "configurable": true, "writable": true, "value": myToString });
// 设置 toString 本身也是原生函数 Object.defineProperty(Function.prototype.toString, myFunction_toString_symbol, { value: "function toString() { [native code] }", enumerable: false, configurable: true, writable: true });
// 返回一个函数,用于为其他函数添加标记 return function (func) { Object.defineProperty(func, myFunction_toString_symbol, { value: "function" + (func.name ? " " + func.name : "") + "() { [native code] }", enumerable: false, configurable: true, writable: true }); }})();使用示例:
const myFunc = function test() { return 1; };safeFunction(myFunc);console.log(myFunc.toString());// "function test() { [native code] }"3.2 Symbol.toStringTag 检测
3.2.1 问题分析
浏览器对象的 toStringTag 有特定值:
Object.prototype.toString.call(window) // "[object Window]"Object.prototype.toString.call(document) // "[object HTMLDocument]"Object.prototype.toString.call(navigator) // "[object Navigator]"3.2.2 解决方案
// 在 createConstructor 中设置Object.defineProperty(this, Symbol.toStringTag, { value: constructorName, // 如 "Window" writable: false, enumerable: false, configurable: false});关键点:
- 使用
Object.defineProperty而非直接赋值 - 设置为不可枚举、不可写、不可配置(模拟浏览器行为)
- 值必须与浏览器一致(区分大小写)
3.3 Symbol.toPrimitive 检测
3.3.1 问题分析
某些检测会通过 Symbol.toPrimitive 检查对象类型:
// 浏览器中window[Symbol.toPrimitive]('string') // "[object Window]"document[Symbol.toPrimitive]('string') // "[object HTMLDocument]"3.3.2 解决方案
Object.defineProperty(this, Symbol.toPrimitive, { value: function (hint) { switch (hint) { case 'number': return this._element ? instancesData[this._element].toString().length : 0; case 'string': return `[${constructorName} Instance]`; default: return `[object ${constructorName}]`; } }, writable: false, enumerable: false, configurable: false});处理不同 hint 的策略:
'number':返回数字(通常是长度)'string':返回字符串表示- 默认:返回标准对象格式
3.4 Node.js 特征隐藏
3.4.1 必须删除的属性
// 删除 Node.js 特有属性delete global.Buffer;delete global.process;delete global.module;delete global.exports;delete global.require;delete global.__dirname;delete global.__filename;delete Error.prepareStackTrace;为什么不能用 = undefined?
// 错误做法global.Buffer = undefined;'Buffer' in global // 仍然返回 true!
// 正确做法delete global.Buffer;'Buffer' in global // 返回 false3.4.2 属性描述符检测
某些检测会使用 Object.getOwnPropertyDescriptor:
// 检测是否为 accessor 属性const desc = Object.getOwnPropertyDescriptor(window, 'Buffer');// 浏览器: undefined(属性不存在)// Node.js (delete后): undefined// Node.js (=undefined): { value: undefined, writable: true, ... }对策:必须使用 delete 删除,不能直接赋值为 undefined。
四、类构造函数模拟
4.1 创建构造函数工厂
function createConstructor(constructorName, enableStrictMode, propertiesList, prototypeMethods, parentConstructorName) { // 存储实例数据(使用 Symbol 作为 key) const instancesData = {};
const constructorFunction = function (element, propertySetter, validationToken) { // 严格模式下,只有传入正确的 token 才能创建实例 if (enableStrictMode && !(validationToken && validationToken === "fatdog")) { throw new Error("Illegal constructor"); }
// 设置 Symbol.toStringTag Object.defineProperty(this, Symbol.toStringTag, { value: constructorName, writable: false, enumerable: false, configurable: false });
// 设置 Symbol.toPrimitive Object.defineProperty(this, Symbol.toPrimitive, { value: function (hint) { /* ... */ }, writable: false, enumerable: false, configurable: false });
// 存储实例数据 const instanceProperties = element && typeof element === "object" ? element : {}; this._element = Symbol("_element"); instancesData[this._element] = instanceProperties; };
// 设置构造函数名称 Object.defineProperty(constructorFunction, 'name', {value: constructorName});
// 处理继承关系 if (parentConstructorName && window[parentConstructorName]) { const ParentConstructor = window[parentConstructorName]; constructorFunction.prototype = Object.create(ParentConstructor.prototype); Object.defineProperty(constructorFunction.prototype, 'constructor', { value: constructorFunction, writable: false, enumerable: false, configurable: false }); }
// 添加原型方法 Object.keys(prototypeMethods).forEach(methodName => { constructorFunction.prototype[methodName] = prototypeMethods[methodName]; if (typeof constructorFunction.prototype[methodName] === "function") { safeFunction(constructorFunction.prototype[methodName]); } });
// 保护构造函数 safeFunction(constructorFunction);
// 挂载到全局 window[constructorName] = constructorFunction; return constructorFunction;}4.2 原型链设置
// 创建继承链createConstructor('EventTarget', true, [], {});createConstructor('Node', true, [], {}, 'EventTarget');createConstructor('WindowProperties', true, [], {}, 'EventTarget');createConstructor('Window', true, [], {}, 'WindowProperties');
// 设置 window 的原型Object.setPrototypeOf(window, Window.prototype);原型链结构:
window -> Window.prototype -> WindowProperties.prototype -> EventTarget.prototype -> Object.prototype4.3 严格模式保护
if (enableStrictMode && !(validationToken && validationToken === "fatdog")) { throw new Error("Illegal constructor");}作用:
- 防止代码直接使用
new Window()创建实例 - 浏览器中某些构造函数是受保护的,不能直接实例化
- 只有传入正确的 token 才能创建实例
五、实战:构建完整的浏览器环境
5.1 步骤一:初始化全局对象
// 基础设置window = globalThis;
// 删除 Node.js 特有属性delete global.Buffer;delete global.process;// ...
// 创建构造函数链createConstructor('EventTarget', true, [], {});createConstructor('Node', true, [], {}, 'EventTarget');createConstructor('Window', true, [], {}, 'Node');Object.setPrototypeOf(window, Window.prototype);5.2 步骤二:创建核心对象
// 创建 documentconst fakeDocument = { createElement: function(tag) { const el = { tagName: tag.toUpperCase(), style: {} }; // 设置正确的 toStringTag Object.defineProperty(el, Symbol.toStringTag, { value: tag === 'canvas' ? 'HTMLCanvasElement' : `HTML${tag}Element`, enumerable: false }); return el; }, // ... 其他方法};safeFunction(fakeDocument.createElement);
// 创建 navigatorconst fakeNavigator = { userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', webdriver: false, // ... 其他属性};
// 使用 Proxy 监控window.document = watch(fakeDocument, 'document');window.navigator = watch(fakeNavigator, 'navigator');5.3 步骤三:测试与迭代
// 加载目标代码require('./target.js');
// 测试签名函数console.log('签名函数:', typeof window.signFunction);
// 根据日志补充缺失的属性// 如果看到 "[访问] document.createElement('canvas')"// 就需要为 canvas 添加 getContext 方法六、高级防检测技巧
6.1 方法调用追踪
// 在 watch 函数中添加对函数调用的追踪get: function(target, property) { const value = target[property];
if (typeof value === 'function') { // 返回包装后的函数,记录调用 return function(...args) { console.log(`[调用] ${name}.${property}(${args.map(a => truncateValue(a)).join(', ')})`); const result = value.apply(target, args); console.log(`[返回] ${name}.${property} -> ${truncateValue(result)}`); return result; }; }
return value;}6.2 属性描述符模拟
// 模拟浏览器属性的描述符Object.defineProperty(fakeNavigator, 'userAgent', { get: () => 'Mozilla/5.0...', enumerable: true, // 可枚举 configurable: true, // 可配置 writable: false // 只读});为什么要设置描述符?
某些检测会检查属性是否可写:
const desc = Object.getOwnPropertyDescriptor(navigator, 'userAgent');if (desc.writable) { // 可能是伪造的!}6.3 动态属性生成
// 延迟生成属性,只在访问时创建const fakeNavigator = {};
Object.defineProperty(fakeNavigator, 'plugins', { get: function() { // 第一次访问时生成 const plugins = { length: 0, item: () => null }; // 缓存结果 Object.defineProperty(fakeNavigator, 'plugins', { value: plugins, enumerable: true, configurable: false }); return plugins; }, enumerable: true, configurable: true});优势:
- 减少初始化时间
- 避免创建永远不会被访问的对象
- 更接近浏览器的惰性初始化行为
七、常见问题与解决方案
7.1 TypeError: Illegal constructor
// 问题:代码尝试直接实例化受保护的构造函数new Window(); // Error: Illegal constructor
// 解决方案:在 createConstructor 中设置 enableStrictMode = false// 或者在调用时传入正确的 tokenconst win = new Window(null, null, "fatdog");7.2 toString 返回值不正确
// 问题:函数的 toString 返回的是源码而非 "[native code]"myFunc.toString(); // "function myFunc() { ... }"
// 解决方案:使用 safeFunction 包装safeFunction(myFunc);myFunc.toString(); // "function myFunc() { [native code] }"7.3 属性不存在错误
// 问题:代码访问了未定义的属性window.someProperty.someMethod(); // TypeError
// 解决方案:使用 Proxy 的 get 拦截器提供默认值get: function(target, property) { if (!(property in target)) { console.log(`[警告] ${name}.${property} 不存在,返回空对象`); return {}; // 或 null,根据实际情况 } return target[property];}7.4 原型链检测失败
// 问题:instanceof 检测失败window instanceof Window // false
// 解决方案:正确设置原型链Object.setPrototypeOf(window, Window.prototype);window instanceof Window // true7.5完整代理与反检测
借鉴大佬开源的模板
!(() => { const origin_log = console.log; logToConsole = function (){ return origin_log(...arguments) }})();//环境代理function watch(obj, name) { // 用于存储已记录的操作,实现去重 const loggedOperations = new Set();
// 辅助函数:限制字符串长度,最长20个字符 const truncateValue = (value) => { const str = String(value); return str.length > 20 ? str.substring(0, 20) + '...' : str; };
return new Proxy(obj, { get: function (target, property) { const value = target[property]; const type = typeof value;
// 生成操作唯一标识和日志消息 let operationId; let logMessage; // 处理要显示的值(截断处理) const displayValue = truncateValue(value);
if (type === "symbol") { const symbolDescription = property.description || 'no description'; logMessage = `对象=>${name},读取属性:${symbolDescription},这是一个 Symbol 类型的值`; operationId = `get:${name}:symbol:${symbolDescription}`; } else if (type === "function") { const functionName = value.name || 'anonymous'; // 函数名也进行长度控制 const displayFunctionName = truncateValue(functionName); logMessage = `对象=>${name},读取属性:${property.toString()},这是一个名为 ${displayFunctionName} 的函数`; operationId = `get:${name}:function:${property.toString()}:${functionName}`; } else { logMessage = `对象=>${name},读取属性:${String(property)},值为:${displayValue},类型为:${type}`; operationId = `get:${name}:${String(property)}:${type}:${String(value)}`; }
// 检查是否已记录,如果没有则记录并输出 if (!loggedOperations.has(operationId)) { loggedOperations.add(operationId); logToConsole(logMessage); } // 关键修复:如果是函数,用 bind 绑定正确的 this // 或者直接返回 value,让 receiver 自动处理 this if (type === "function") { // 方式1: 使用 bind 绑定到 target // return value.bind(target);
// 方式2: 使用 Reflect.get 获取,保持 receiver 上下文(更推荐) return Reflect.get(target, property, receiver); } return value; }, set: (target, property, newValue, receiver) => { const valueType = typeof newValue;
// 生成操作唯一标识和日志消息 let operationId; let logMessage; // 处理要显示的新值(截断处理) const displayNewValue = truncateValue(newValue);
if (valueType === "symbol") { const symbolDescription = newValue.description || 'no description'; logMessage = `对象=>${name},设置属性:${String(property)},这是一个 Symbol 类型的新值, 描述为: ${symbolDescription}`; operationId = `set:${name}:${String(property)}:symbol:${symbolDescription}`; } else { logMessage = `对象=>${name},设置属性:${String(property)},值为:${displayNewValue},类型为:${valueType}`; operationId = `set:${name}:${String(property)}:${valueType}:${String(newValue)}`; }
// 检查是否已记录,如果没有则记录并输出 if (!loggedOperations.has(operationId)) { loggedOperations.add(operationId); logToConsole(logMessage); }
return Reflect.set(target, property, newValue, receiver); }, // 监听 in 操作符(检查属性是否存在) has: (target, property) => { let operationId; let logMessage;
// 处理 Symbol 类型的属性 if (typeof property === 'symbol') { const symbolDescription = property.description || 'no description'; logMessage = `对象=>${name},检查属性存在:${symbolDescription} (Symbol), 使用 in 操作符`; operationId = `has:${name}:symbol:${symbolDescription}`; } else { logMessage = `对象=>${name},检查属性存在:${String(property)}, 使用 in 操作符`; operationId = `has:${name}:${String(property)}`; }
if (!loggedOperations.has(operationId)) { loggedOperations.add(operationId); logToConsole(logMessage); }
return Reflect.has(target, property); }, // 监听属性枚举操作(如 Object.keys、for...in 等) ownKeys: (target) => { const keys = Reflect.ownKeys(target); // 截断过长的键列表显示 const displayKeys = truncateValue(keys.map(k => typeof k === 'symbol' ? `Symbol(${k.description || ''})` : String(k) ).join(', '));
const operationId = `ownKeys:${name}:${keys.length}`; const logMessage = `对象=>${name},枚举属性,共 ${keys.length} 个属性:${displayKeys}`;
if (!loggedOperations.has(operationId)) { loggedOperations.add(operationId); logToConsole(logMessage); }
return keys; }, // 拦截 Object.getOwnPropertyDescriptor getOwnPropertyDescriptor: (target, property) => { const desc = Reflect.getOwnPropertyDescriptor(target, property); if (desc) { logToConsole(`[描述符] ${name}.${String(property)}: configurable=${desc.configurable}, enumerable=${desc.enumerable}, writable=${desc.writable}`); } return desc; }, // 拦截 delete deleteProperty: (target, property) => { logToConsole(`[删除] ${name}.${String(property)}`); return Reflect.deleteProperty(target, property); }, // 拦截 Object.getPrototypeOf() 和 __proto__ getPrototypeOf: (target) => { const proto = Reflect.getPrototypeOf(target); const protoName = proto ? (proto.name || proto[Symbol.toStringTag] || 'Object') : 'null'; const operationId = `getPrototypeOf:${name}:${protoName}`; const logMessage = `[原型] ${name}.getPrototypeOf() = [${protoName}]`;
if (!loggedOperations.has(operationId)) { loggedOperations.add(operationId); logToConsole(logMessage); }
return proto; }, // 拦截 Object.setPrototypeOf() setPrototypeOf: (target, proto) => { const protoName = proto ? (proto.name || proto[Symbol.toStringTag] || 'Object') : 'null'; const operationId = `setPrototypeOf:${name}:${protoName}`; const logMessage = `[原型] 设置 ${name}.prototype = [${protoName}]`;
if (!loggedOperations.has(operationId)) { loggedOperations.add(operationId); logToConsole(logMessage); }
return Reflect.setPrototypeOf(target, proto); }, });}
//安全函数const safeFunction = (function () { //处理安全函数 Function.prototype.$call = Function.prototype.call; const $toString = Function.toString; const myFunction_toString_symbol = Symbol('('.concat('', ')'));
const myToString = function myToString() { return typeof this === 'function' && this[myFunction_toString_symbol] || $toString.$call(this); }
const set_native = function set_native(func, key, value) { Object.defineProperty(func, key, { "enumerable": false, "configurable": true, "writable": true, "value": value }); }
delete Function.prototype['toString']; set_native(Function.prototype, "toString", myToString); set_native(Function.prototype.toString, myFunction_toString_symbol, "function toString() { [native code] }");
return function (func) { set_native(func, myFunction_toString_symbol, "function" + (func.name ? " " + func.name : "") + "() { [native code] }"); }})();
//类构造函数function createConstructor(constructorName, enableStrictMode, propertiesList, prototypeMethods, parentConstructorName) { const instancesData = {}; const constructorFunction = function (element, propertySetter, validationToken) { if (enableStrictMode && !(validationToken && validationToken === "fatdog")) { throw new Error("Illegal constructor"); }
// 为实例添加Symbol.toStringTag Object.defineProperty(this, Symbol.toStringTag, { value: constructorName, writable: false, enumerable: false, configurable: false });
// 为实例添加Symbol.toPrimitive Object.defineProperty(this, Symbol.toPrimitive, { value: function (hint) { switch (hint) { case 'number': return this._element ? instancesData[this._element].toString().length : 0; case 'string': return `[${constructorName} Instance]`; default: return `[object ${constructorName}]`; } }, writable: false, enumerable: false, configurable: false });
if (propertySetter && typeof propertySetter === "function") { propertySetter(this, instancesData[this._element]); } const instanceProperties = element && typeof element === "object" ? element : {}; this._element = Symbol("_element"); instancesData[this._element] = instanceProperties; if (element && typeof element === "object") { Object.keys(element).forEach(key => { if (!this[key]) { this[key] = element[key]; } }); } };
// 设置构造函数名称 Object.defineProperty(constructorFunction, 'name', {value: constructorName});
// 处理继承关系 if (parentConstructorName && window[parentConstructorName]) { const ParentConstructor = window[parentConstructorName]; constructorFunction.prototype = Object.create(ParentConstructor.prototype); Object.defineProperty(constructorFunction.prototype, 'constructor', { value: constructorFunction, writable: false, enumerable: false, configurable: false }); }
// 为构造函数本身添加Symbol.toStringTag Object.defineProperty(constructorFunction, Symbol.toStringTag, { value: constructorName, writable: false, enumerable: false, configurable: false });
// 为构造函数本身添加Symbol.toPrimitive Object.defineProperty(constructorFunction, Symbol.toPrimitive, { value: function (hint) { switch (hint) { case 'number': return constructorName.length; case 'string': return `[Constructor ${constructorName}]`; default: return constructorName; } }, writable: false, enumerable: false, configurable: false });
// 添加原型方法 Object.keys(prototypeMethods).forEach(methodName => { constructorFunction.prototype[methodName] = prototypeMethods[methodName]; if (typeof constructorFunction.prototype[methodName] === "function") { safeFunction(constructorFunction.prototype[methodName]); } });
// 保护构造函数 safeFunction(constructorFunction);
// 挂载到全局 window[constructorName] = constructorFunction; return constructorFunction;};八、总结
8.1 核心要点
| 检测类型 | 解决方案 |
|---|---|
Function.toString | 使用 safeFunction 包装 |
Symbol.toStringTag | 通过 defineProperty 设置 |
Symbol.toPrimitive | 定义转换函数 |
| Node.js 特征 | 使用 delete 删除 |
| 属性描述符 | 使用 defineProperty 精确控制 |
| 原型链 | 使用 setPrototypeOf 设置 |
8.2 设计原则
- 最小化原则:只模拟被实际访问的属性,避免过度实现
- 真实性原则:属性值和行为要尽可能接近真实浏览器
- 隐蔽性原则:隐藏所有 Node.js 特征
- 可监控原则:使用 Proxy 追踪所有访问,便于调试
8.3 工具链推荐
- Proxy:用于监控和拦截属性访问
- Object.defineProperty:用于精确控制属性描述符
- Symbol:用于创建唯一标识符和特殊属性
- Set:用于去重日志记录
通过以上方法,我们可以构建一个高度仿真的浏览器环境,成功绕过各种检测,让加密代码在 Node.js 中正常运行。
部分信息可能已经过时









