各位好,我是今天的主讲人,很高兴和大家一起聊聊一个很有意思,但也经常让人头疼的话题: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,可能增加代码复杂度 |
作为开发者,我们需要不断学习和掌握新的技术,才能更好地应对各种安全挑战。
希望今天的分享对大家有所帮助。谢谢大家!