在Web安全研究和爬虫开发领域,我们经常会遇到需要模拟浏览器环境的情况。特别是在处理那些采用了复杂反爬机制的网站时,单纯使用请求库已经无法满足需求。这时候就需要通过JavaScript逆向技术来"补环境"——即模拟浏览器运行时的各种属性和方法。
最近我在分析一个电商平台的反爬系统时,发现它通过检测navigator、window、document等对象的原型链属性来判断环境真实性。传统的补环境方法只覆盖了表层属性,导致很容易被识别为自动化脚本。这促使我深入研究原型链层次的属性方法伪造技术。
现代反爬系统通常会检查以下几个关键点:
window.outerWidth、navigator.userAgent等Document.prototype.querySelector的实现window.document === document是否成立performance.now()的返回值模式以检测navigator对象为例,高级反爬系统不仅会检查userAgent,还会验证:
javascript复制Object.getOwnPropertyDescriptor(Navigator.prototype, 'userAgent').get.toString()
如果返回的是原生函数字符串,就会包含[native code]标记。而简单的属性覆盖无法模拟这一点。
要实现完美的环境模拟,我们需要从三个层次入手:
以下是一个完整的navigator对象模拟实现:
javascript复制const fakeNavigator = {};
// 1. 定义属性描述符
const propertyDescriptor = {
get: function() {
return 'Mozilla/5.0...';
},
enumerable: true,
configurable: false
};
// 2. 修改原型链
Object.defineProperty(Navigator.prototype, 'userAgent', propertyDescriptor);
// 3. 创建不可检测的实例
const navigator = Object.create(Navigator.prototype);
Object.defineProperty(window, 'navigator', {
value: navigator,
writable: false,
configurable: false
});
在伪造方法时,需要特别注意:
例如伪造document.querySelector:
javascript复制const originalQuerySelector = Document.prototype.querySelector;
Document.prototype.querySelector = function(selector) {
// 添加随机延迟模拟真实环境
const start = Date.now();
while(Date.now() - start < Math.random() * 5) {}
return originalQuerySelector.call(this, selector);
};
// 保持toString输出一致
Document.prototype.querySelector.toString = function() {
return 'function querySelector() { [native code] }';
};
一个完整的环境模拟系统应包含以下模块:
code复制/env
/core
window.js # 窗口对象模拟
document.js # 文档对象模拟
/prototypes
dom.js # DOM原型方法
bom.js # BOM原型方法
/utils
perf.js # 性能模拟
relation.js # 对象关系维护
window对象模拟示例:
javascript复制// core/window.js
const createWindow = () => {
const window = {};
// 1. 定义基础属性
Object.defineProperties(window, {
innerWidth: {
get: () => 1920,
set: () => {},
configurable: false
},
// ...其他属性
});
// 2. 建立对象引用关系
window.window = window;
window.self = window;
// 3. 原型链处理
Object.setPrototypeOf(window, Window.prototype);
return window;
};
| 检测类型 | 检测方法示例 | 应对方案 |
|---|---|---|
| 原型链检测 | Object.getPrototypeOf(navigator) === Navigator.prototype |
正确设置原型链 |
| 函数特征检测 | document.querySelector.toString().includes('[native code]') |
重写toString方法 |
| 性能特征检测 | performance.now()差值分析 |
添加随机延迟 |
| 对象关系检测 | window.document === document |
维护引用一致性 |
最近遇到一个检测Function.prototype.toString调用的案例:
javascript复制const isNative = (fn) => {
const str = Function.prototype.toString.call(fn);
return str.includes('[native code]') &&
str === fn.toString();
}
破解方案:
javascript复制// 保存原生方法
const originalToString = Function.prototype.toString;
// 重写Function.prototype.toString
Function.prototype.toString = function() {
if(this === document.querySelector) {
return 'function querySelector() { [native code] }';
}
return originalToString.call(this);
};
// 确保Function.prototype.toString自身也是native
Object.defineProperty(Function.prototype, 'toString', {
value: Function.prototype.toString,
writable: false,
configurable: false
});
在完成环境模拟后,建议检查以下项目:
javascript复制console.log(Object.getPrototypeOf(yourFakeNavigator) === Navigator.prototype);
javascript复制console.log(document.querySelector.toString());
javascript复制const start = performance.now();
document.querySelector('div');
console.log(performance.now() - start);
javascript复制console.log(Object.getOwnPropertyDescriptor(Navigator.prototype, 'userAgent'));
在修改原型方法时,可能会意外影响其他代码。解决方案:
javascript复制// 使用沙箱环境
const iframe = document.createElement('iframe');
document.body.appendChild(iframe);
const sandbox = iframe.contentWindow;
// 在沙箱中修改原型
sandbox.eval(`
Document.prototype.querySelector = function() {
// 自定义实现
};
`);
长时间运行的脚本可能导致内存泄漏。预防措施:
对于更高级的场景,可以实现动态环境模拟:
javascript复制class EnvironmentSimulator {
constructor() {
this.originalPrototypes = new Map();
}
// 备份原始原型
backup(ctor) {
this.originalPrototypes.set(ctor, {
prototype: Object.assign({}, ctor.prototype),
methods: {}
});
Object.getOwnPropertyNames(ctor.prototype).forEach(name => {
if(typeof ctor.prototype[name] === 'function') {
this.originalPrototypes.get(ctor).methods[name] =
ctor.prototype[name].toString();
}
});
}
// 恢复原始原型
restore(ctor) {
const backup = this.originalPrototypes.get(ctor);
Object.setPrototypeOf(ctor.prototype, null);
Object.assign(ctor.prototype, backup.prototype);
Object.entries(backup.methods).forEach(([name, body]) => {
ctor.prototype[name] = new Function(
body.substring(body.indexOf('(') + 1, body.indexOf(')')),
body.substring(body.indexOf('{') + 1, body.lastIndexOf('}'))
);
});
}
// 模拟环境
simulate() {
this.backup(Navigator);
this.backup(Document);
// 应用模拟逻辑...
}
}
在实施JS逆向和环境模拟时,需要注意:
在实际项目中,我通常会设置以下防护措施: