各位好,我是今天的主讲人,很高兴和大家一起聊聊一个很有意思,但也经常让人头疼的话题:JS Proxy 的检测与反检测。这玩意儿就像猫鼠游戏,你绞尽脑汁去用 Proxy 实现一些高级功能,沙箱或者恶意代码分析引擎就千方百计地想把它揪出来。
咱们今天就来深入探讨一下,Proxy 究竟是怎么被检测的,以及我们又有哪些反制手段。准备好了吗? Let’s dive in!
第一部分:为什么要检测 Proxy?
在深入技术细节之前,我们先来明确一个根本问题:为什么要检测 Proxy? 简单来说,Proxy 赋予了 JavaScript 极强的元编程能力,它可以拦截并修改对象的各种操作,包括属性访问、赋值、函数调用等等。这在某些场景下非常有用,但也给安全带来了挑战。
- 沙箱环境: 沙箱通常会使用
Proxy来限制代码的行为,例如阻止访问敏感 API、限制内存使用等。检测Proxy可以帮助沙箱确定代码是否正在试图绕过限制。 - 恶意代码分析: 恶意代码可能会利用
Proxy来隐藏其真实意图,例如,通过拦截属性访问来动态加载恶意代码。检测Proxy可以帮助分析引擎识别潜在的威胁。 - 调试与监控: 一些调试工具或监控系统会使用
Proxy来追踪对象的行为,检测Proxy可以帮助确定代码是否正在被监控。
第二部分:Proxy 的常见检测方法
既然知道了为什么要检测 Proxy,接下来我们就来看看常见的检测方法有哪些。
-
instanceof Proxy: 最简单直接的方法就是使用instanceof运算符。如果一个对象是Proxy的实例,那么obj instanceof Proxy将返回true。const obj = {}; const proxy = new Proxy(obj, {}); console.log(proxy instanceof Proxy); // true console.log(obj instanceof Proxy); // false缺点: 这种方法过于简单粗暴,很容易被绕过,我们后面会讲到。
-
Proxy原型链检查:Proxy对象的原型链上会有Proxy构造函数的原型对象。我们可以通过遍历原型链来判断一个对象是否是Proxy。function isProxy(obj) { let proto = Object.getPrototypeOf(obj); while (proto) { if (proto === Proxy.prototype) { return true; } proto = Object.getPrototypeOf(proto); } return false; } const obj = {}; const proxy = new Proxy(obj, {}); console.log(isProxy(proxy)); // true console.log(isProxy(obj)); // false缺点: 这种方法比
instanceof稍微复杂一些,但仍然容易被绕过,例如通过修改原型链。 -
getOwnPropertyDescriptor与ownKeys:Proxy的一个重要特性是它可以拦截getOwnPropertyDescriptor和ownKeys操作。我们可以通过检查这些操作是否被拦截来判断一个对象是否是Proxy。function isProxyByDescriptor(obj) { const originalGetOwnPropertyDescriptor = Object.getOwnPropertyDescriptor; const originalOwnKeys = Reflect.ownKeys; try { // 尝试获取自身属性描述符,如果抛出异常,则可能是 Proxy originalGetOwnPropertyDescriptor(obj, "nonExistentProperty"); // 尝试获取自身属性键,如果抛出异常,则可能是 Proxy originalOwnKeys(obj); return false; // 没有抛出异常,不是 Proxy } catch (e) { return true; // 抛出异常,可能是 Proxy } finally { // 恢复原始方法,防止影响其他代码 Object.getOwnPropertyDescriptor = originalGetOwnPropertyDescriptor; Reflect.ownKeys = originalOwnKeys; } } const obj = {}; const proxy = new Proxy(obj, { getOwnPropertyDescriptor(target, prop) { throw new Error("Intercepted!"); }, ownKeys(target) { throw new Error("Intercepted!"); }, }); console.log(isProxyByDescriptor(proxy)); // true console.log(isProxyByDescriptor(obj)); // false缺点: 这种方法依赖于
Proxy是否拦截了getOwnPropertyDescriptor和ownKeys操作。如果Proxy没有拦截这些操作,或者拦截后没有抛出异常,就无法检测到。另外,这种方法可能会影响性能,因为它需要执行一些额外的操作。 -
检查 handler 是否存在:
Proxy的核心在于其 handler 对象。如果一个对象具有 handler,并且该 handler 包含特定的 trap 方法(例如get、set等),那么它很可能是一个Proxy。function hasProxyHandler(obj) { return typeof obj === 'object' && obj !== null && typeof obj.handler === 'object' && obj.handler !== null; } const obj = {}; const proxy = new Proxy(obj, { get(target, prop) { return target[prop]; } }); proxy.handler = { get: function(){} }; // 模拟 handler 属性 console.log(hasProxyHandler(proxy)); // true console.log(hasProxyHandler(obj)); // false缺点: 这个方法依赖于检查对象是否具有名为
handler的属性,并且该属性是一个对象。这种方法很容易被绕过,因为我们可以简单地不使用handler这个属性名,或者使用其他方式来存储 trap 方法。 而且这种方式很容易误判,因为一个普通的对象也可能拥有一个名为handler的属性。 -
通过调用
toString方法:Proxy对象的toString方法通常会返回"[object Proxy]"。我们可以通过调用toString方法来判断一个对象是否是Proxy。const obj = {}; const proxy = new Proxy(obj, {}); console.log(Object.prototype.toString.call(proxy)); // "[object Proxy]" console.log(Object.prototype.toString.call(obj)); // "[object Object]"缺点: 这种方法同样很容易被绕过,因为我们可以修改
Proxy对象的toString方法。
第三部分:Proxy 的反检测技巧
既然知道了 Proxy 是如何被检测的,接下来我们就来看看如何反检测,让我们的 Proxy 对象更难被发现。
-
修改
instanceof行为: 我们可以通过修改Proxy对象的Symbol.hasInstance属性来改变instanceof运算符的行为。const obj = {}; const proxy = new Proxy(obj, { [Symbol.hasInstance](instance) { return false; // 始终返回 false,让 instanceof Proxy 返回 false } }); console.log(proxy instanceof Proxy); // false原理:
instanceof运算符实际上会调用对象的Symbol.hasInstance方法。我们可以通过修改这个方法来改变instanceof的行为。 -
伪造原型链: 我们可以修改
Proxy对象的原型链,使其看起来像一个普通对象。const obj = {}; const proxy = new Proxy(obj, {}); Object.setPrototypeOf(proxy, Object.prototype); // 将 proxy 的原型设置为 Object.prototype function isProxy(obj) { let proto = Object.getPrototypeOf(obj); while (proto) { if (proto === Proxy.prototype) { return true; } proto = Object.getPrototypeOf(proto); } return false; } console.log(isProxy(proxy)); // false原理:
isProxy函数通过遍历原型链来判断一个对象是否是Proxy。通过修改Proxy对象的原型链,我们可以让isProxy函数无法找到Proxy.prototype。 -
避免抛出异常: 如果
Proxy拦截了getOwnPropertyDescriptor和ownKeys操作,但没有抛出异常,那么isProxyByDescriptor函数就无法检测到Proxy。const obj = {}; const proxy = new Proxy(obj, { getOwnPropertyDescriptor(target, prop) { return Object.getOwnPropertyDescriptor(target, prop); // 返回原始描述符,不抛出异常 }, ownKeys(target) { return Reflect.ownKeys(target); // 返回原始键,不抛出异常 }, }); function isProxyByDescriptor(obj) { const originalGetOwnPropertyDescriptor = Object.getOwnPropertyDescriptor; const originalOwnKeys = Reflect.ownKeys; try { originalGetOwnPropertyDescriptor(obj, "nonExistentProperty"); originalOwnKeys(obj); return false; } catch (e) { return true; } finally { Object.getOwnPropertyDescriptor = originalGetOwnPropertyDescriptor; Reflect.ownKeys = originalOwnKeys; } } console.log(isProxyByDescriptor(proxy)); // false原理:
isProxyByDescriptor函数通过检查getOwnPropertyDescriptor和ownKeys操作是否抛出异常来判断一个对象是否是Proxy。通过让Proxy不抛出异常,我们可以绕过isProxyByDescriptor函数的检测。 -
隐藏 handler: 我们可以使用闭包或者 WeakMap 来隐藏
Proxy的 handler 对象,避免被检测到。const createHiddenProxy = (target, handler) => { const privateHandler = new WeakMap(); privateHandler.set(target, handler); return new Proxy(target, { get(target, prop) { const handler = privateHandler.get(target); if (handler && handler.get) { return handler.get(target, prop); } return target[prop]; }, // 其他 trap 方法... }); }; const obj = {}; const handler = { get(target, prop) { return target[prop]; } }; const proxy = createHiddenProxy(obj, handler); function hasProxyHandler(obj) { return typeof obj === 'object' && obj !== null && typeof obj.handler === 'object' && obj.handler !== null; } console.log(hasProxyHandler(proxy)); // false原理:
hasProxyHandler函数通过检查对象是否具有名为handler的属性来判断一个对象是否是Proxy。通过使用闭包或者 WeakMap 来隐藏Proxy的 handler 对象,我们可以绕过hasProxyHandler函数的检测。 -
修改
toString方法: 我们可以修改Proxy对象的toString方法,使其返回一个看起来像普通对象的字符串。const obj = {}; const proxy = new Proxy(obj, {}); proxy.toString = function() { return "[object Object]"; }; console.log(Object.prototype.toString.call(proxy)); // "[object Object]"原理:
Object.prototype.toString.call(proxy)通常会返回"[object Proxy]"。通过修改Proxy对象的toString方法,我们可以让它返回"[object Object]",从而隐藏Proxy的身份。
第四部分:更高级的反检测技巧:利用 ES Module 的隔离性
ES Module 具有天然的隔离性,这为我们提供了一种更高级的反检测手段。我们可以将 Proxy 的创建和使用封装在一个 ES Module 中,然后将该 Module 导入到其他代码中。这样,其他代码就无法直接访问 Proxy 对象,也无法直接检测到 Proxy 的存在。
例如,我们可以创建一个名为 proxy-module.js 的文件:
// proxy-module.js
const createHiddenProxy = (target, handler) => {
const privateHandler = new WeakMap();
privateHandler.set(target, handler);
return new Proxy(target, {
get(target, prop) {
const handler = privateHandler.get(target);
if (handler && handler.get) {
return handler.get(target, prop);
}
return target[prop];
},
// 其他 trap 方法...
});
};
const obj = {};
const handler = {
get(target, prop) {
return target[prop];
}
};
const proxy = createHiddenProxy(obj, handler);
export default proxy;
然后,在其他代码中导入该 Module:
<!DOCTYPE html>
<html>
<head>
<title>Proxy 反检测示例</title>
</head>
<body>
<script type="module">
import proxy from './proxy-module.js';
function isProxy(obj) {
let proto = Object.getPrototypeOf(obj);
while (proto) {
if (proto === Proxy.prototype) {
return true;
}
proto = Object.getPrototypeOf(proto);
}
return false;
}
console.log(isProxy(proxy)); // 仍然是 false,因为原型链可能已经被修改
console.log(proxy.someProperty); // 可以正常访问 proxy 的属性
// 尝试直接访问 proxy 的 handler 属性,会失败
try {
console.log(proxy.handler);
} catch (e) {
console.log("无法访问 proxy 的 handler 属性");
}
</script>
</body>
</html>
原理: ES Module 的隔离性确保了 proxy-module.js 中定义的 proxy 对象只能在该 Module 内部访问。其他代码只能通过 export 导出的接口来访问 proxy 对象,而无法直接访问其内部属性和方法。这使得检测 Proxy 变得更加困难。
第五部分:总结与展望
Proxy 的检测与反检测是一个持续演进的过程。随着技术的不断发展,新的检测方法和反检测技巧也会不断涌现。
| 检测方法 | 反检测技巧 | 优点 | 缺点 |
|---|---|---|---|
instanceof Proxy |
修改 Symbol.hasInstance 属性 |
简单易懂 | 容易被绕过 |
| 原型链检查 | 伪造原型链 | 比 instanceof 稍微复杂 |
仍然容易被绕过 |
getOwnPropertyDescriptor |
避免抛出异常 | 可以检测到拦截 getOwnPropertyDescriptor 的 Proxy |
依赖于 Proxy 是否抛出异常 |
| 检查 handler 是否存在 | 隐藏 handler | 可以检测到具有 handler 属性的 Proxy |
容易误判,容易被绕过 |
toString 方法 |
修改 toString 方法 |
可以检测到未修改 toString 的 Proxy |
容易被绕过 |
| ES Module 隔离 | 将 Proxy 封装在 ES Module 中 |
隔离性强,难以直接访问 Proxy |
需要使用 ES Module,可能增加代码复杂度 |
作为开发者,我们需要不断学习和掌握新的技术,才能更好地应对各种安全挑战。
希望今天的分享对大家有所帮助。谢谢大家!