浏览器环境模拟是JS逆向工程中最关键的环节之一。去年我在分析某电商平台加密逻辑时,发现其前端代码对运行环境进行了多达37处特征检测。传统的方法补丁式Hook已经难以应对这种级别的环境校验,必须建立完整的原型链体系。
环境模拟的核心在于还原浏览器原生对象的完整行为特征。以最常见的window对象为例,它不仅是一个全局对象,更是整个JS运行环境的根基。现代前端框架会通过以下方式检测环境真实性:
javascript复制if (window.self !== window.top
|| window.document.constructor.name !== 'HTMLDocument'
|| Object.getPrototypeOf(navigator) !== Navigator.prototype) {
throw new Error('Environment validation failed');
}
直接修改__proto__是最危险的方案,会触发现代浏览器的原型污染防护机制。我推荐使用Proxy递归代理方案:
javascript复制function createHierarchy(original) {
const handler = {
get(target, prop) {
// 特殊处理constructor避免循环引用
if (prop === 'constructor') {
return Object.create(original.constructor.prototype);
}
const val = Reflect.get(...arguments);
return typeof val === 'object' && val !== null
? new Proxy(val, handler)
: val;
}
};
return new Proxy(original, handler);
}
const fakeWindow = createHierarchy(realWindow);
关键点:必须保留原始原型链的不可枚举属性和Symbol属性,这是大多数检测脚本的重点检查项
bind()保持正确的this指向例如模拟document.createElement:
javascript复制const originalCreateElement = Document.prototype.createElement;
Document.prototype.createElement = function(tagName, options) {
// 参数校验
if (typeof tagName !== 'string') {
throw new TypeError("Failed to execute 'createElement'");
}
// 创建真实元素
const element = originalCreateElement.call(this, tagName, options);
// 添加虚拟属性
Object.defineProperty(element, '__virtual__', {
value: true,
configurable: false,
enumerable: false
});
return element;
};
现代反爬系统会检测超过50项navigator属性。这是我总结的关键检测点表格:
| 检测属性 | 处理方案 | 注意事项 |
|---|---|---|
| userAgent | 动态生成匹配版本号 | 需同步更新appVersion |
| platform | 保持与userAgent一致 | MacIntel/Win32区分大小写 |
| hardwareConcurrency | 随机生成2/4/8 | 需与设备内存大小逻辑匹配 |
| deviceMemory | 与concurrency保持合理比例 | 通常为2的n次方 |
| webdriver | 必须设为undefined | 直接delete会触发异常 |
实现示例:
javascript复制const fakeNavigator = {};
Object.defineProperties(fakeNavigator, {
'userAgent': {
get: () => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
configurable: false
},
'hardwareConcurrency': {
value: 4,
writable: false
}
});
// 原型链修正
Object.setPrototypeOf(fakeNavigator, Navigator.prototype);
不同方案的性能影响对比:
| 方案 | 内存占用 | CPU开销 | 检测难度 | 适用场景 |
|---|---|---|---|---|
| 原生重写 | 低 | 低 | 易 | 简单环境 |
| Proxy拦截 | 中 | 中 | 中 | 中等检测强度 |
| WASM替换 | 高 | 高 | 难 | 强对抗场景 |
| 虚拟机注入 | 极高 | 极高 | 极难 | 专业逆向工程 |
推荐的中等强度实现:
javascript复制const originalSetTimeout = window.setTimeout;
window.setTimeout = function(callback, delay, ...args) {
// 随机添加1-10ms抖动
const jitter = Math.floor(Math.random() * 10);
return originalSetTimeout.call(
window,
callback,
delay + jitter,
...args
);
};
某些安全框架会检查函数toString()结果。应对方案:
javascript复制function createNativeLikeFunction(original) {
const handler = {
apply(target, thisArg, args) {
return Reflect.apply(target, thisArg, args);
}
};
const proxy = new Proxy(original, handler);
// 关键:保持原生函数toString格式
Object.defineProperty(proxy, 'toString', {
value: () => `function ${original.name}() { [native code] }`,
writable: false
});
return proxy;
}
通过ArrayBuffer制造真实内存分布:
javascript复制function createMemoryFake() {
const buffer = new ArrayBuffer(1024);
const view = new DataView(buffer);
// 写入特征值
view.setUint32(0, 0x11223344, true);
view.setFloat64(8, Math.PI, true);
return {
buffer,
get [Symbol.toStringTag]() {
return 'Memory';
}
};
}
javascript复制Object.defineProperty(window, 'chrome', {
value: {
runtime: {
lastError: null,
id: 'mock_extension'
},
devtools: {
inspectedWindow: {
tabId: Math.floor(Math.random() * 1000)
},
network: {
getHAR: () => ({ entries: [] })
}
}
},
configurable: false,
enumerable: true
});
javascript复制const originalNow = performance.now;
performance.now = function() {
// 添加微小随机偏移
const offset = Math.random() * 0.1;
return originalNow.call(performance) + offset;
};
// 保持时间连续性
let cumulativeTime = 0;
performance.mark = function(name) {
cumulativeTime += 5;
PerformanceObserver.supportedEntryTypes.includes('mark');
};
| 错误类型 | 根本原因 | 解决方案 |
|---|---|---|
| Illegal invocation | 上下文丢失 | 使用bind绑定正确this |
| Invalid calling object | 原型链断裂 | 递归修复原型关系 |
| Permission denied | 属性不可配置 | 使用Object.defineProperty |
| Function not cloneable | 内置方法保护 | 创建代理函数包裹 |
javascript复制const lazyHandler = {
get(target, prop) {
if (!(prop in target)) {
if (prop === 'webkitStorageInfo') {
target[prop] = createStorageMock();
}
}
return target[prop];
}
};
在最近一个金融项目的逆向中,这套方案成功绕过了包括FingerprintJS在内的三重检测。核心经验是:环境模拟不是简单的属性复制,而是需要构建完整的对象生命周期行为模型。特别是在处理WebGL和AudioContext等高级API时,必须注意硬件加速层的行为一致性。