JS `Proxy` 检测与反检测:对抗沙箱与代码分析

各位好,我是今天的主讲人,很高兴和大家一起聊聊一个很有意思,但也经常让人头疼的话题:JS Proxy 的检测与反检测。这玩意儿就像猫鼠游戏,你绞尽脑汁去用 Proxy 实现一些高级功能,沙箱或者恶意代码分析引擎就千方百计地想把它揪出来。

咱们今天就来深入探讨一下,Proxy 究竟是怎么被检测的,以及我们又有哪些反制手段。准备好了吗? Let’s dive in!

第一部分:为什么要检测 Proxy

在深入技术细节之前,我们先来明确一个根本问题:为什么要检测 Proxy? 简单来说,Proxy 赋予了 JavaScript 极强的元编程能力,它可以拦截并修改对象的各种操作,包括属性访问、赋值、函数调用等等。这在某些场景下非常有用,但也给安全带来了挑战。

  1. 沙箱环境: 沙箱通常会使用 Proxy 来限制代码的行为,例如阻止访问敏感 API、限制内存使用等。检测 Proxy 可以帮助沙箱确定代码是否正在试图绕过限制。
  2. 恶意代码分析: 恶意代码可能会利用 Proxy 来隐藏其真实意图,例如,通过拦截属性访问来动态加载恶意代码。检测 Proxy 可以帮助分析引擎识别潜在的威胁。
  3. 调试与监控: 一些调试工具或监控系统会使用 Proxy 来追踪对象的行为,检测 Proxy 可以帮助确定代码是否正在被监控。

第二部分:Proxy 的常见检测方法

既然知道了为什么要检测 Proxy,接下来我们就来看看常见的检测方法有哪些。

  1. 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

    缺点: 这种方法过于简单粗暴,很容易被绕过,我们后面会讲到。

  2. 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 稍微复杂一些,但仍然容易被绕过,例如通过修改原型链。

  3. getOwnPropertyDescriptorownKeys: Proxy 的一个重要特性是它可以拦截 getOwnPropertyDescriptorownKeys 操作。我们可以通过检查这些操作是否被拦截来判断一个对象是否是 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 是否拦截了 getOwnPropertyDescriptorownKeys 操作。如果 Proxy 没有拦截这些操作,或者拦截后没有抛出异常,就无法检测到。另外,这种方法可能会影响性能,因为它需要执行一些额外的操作。

  4. 检查 handler 是否存在: Proxy 的核心在于其 handler 对象。如果一个对象具有 handler,并且该 handler 包含特定的 trap 方法(例如 getset 等),那么它很可能是一个 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 的属性。

  5. 通过调用 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 对象更难被发现。

  1. 修改 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 的行为。

  2. 伪造原型链: 我们可以修改 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

  3. 避免抛出异常: 如果 Proxy 拦截了 getOwnPropertyDescriptorownKeys 操作,但没有抛出异常,那么 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 函数通过检查 getOwnPropertyDescriptorownKeys 操作是否抛出异常来判断一个对象是否是 Proxy。通过让 Proxy 不抛出异常,我们可以绕过 isProxyByDescriptor 函数的检测。

  4. 隐藏 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 函数的检测。

  5. 修改 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 避免抛出异常 可以检测到拦截 getOwnPropertyDescriptorProxy 依赖于 Proxy 是否抛出异常
检查 handler 是否存在 隐藏 handler 可以检测到具有 handler 属性的 Proxy 容易误判,容易被绕过
toString 方法 修改 toString 方法 可以检测到未修改 toStringProxy 容易被绕过
ES Module 隔离 Proxy 封装在 ES Module 中 隔离性强,难以直接访问 Proxy 需要使用 ES Module,可能增加代码复杂度

作为开发者,我们需要不断学习和掌握新的技术,才能更好地应对各种安全挑战。

希望今天的分享对大家有所帮助。谢谢大家!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注