5

如何检测 JavaScript 原生函数是否被打过猴子补丁

 1 year ago
source link: https://www.fly63.com/article/detial/12129
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

更新日期: 2022-09-16阅读: 27标签: 函数分享

扫一扫分享

简单讲:如何确定 JavaScript 的 原生函数有没有被重写过呢? 我们没法做到,或者说判定结果的可信度并不会特别高。我们有很多方法可以检查,但是无法保证万无一失。

JavaScript 中的原生函数

在 JavaScript 中,“原生函数”(Native function) 是那些源代码被编译为原生机器码的函数。我们可以在 JavaScript 标准内置对象 中找到原生函数(诸如 eval(),parseInt()) ,或者在 浏览器 Web api 找到(诸如 fetch(),localStorage.getItem())。

由于 JavaScript 的动态特性,开发者可以覆盖浏览器暴露出的原生函数。这种技巧被我们称作 猴子补丁(Monkey patching ) 。

猴子补丁主要用于修改浏览器内置 API 和原生函数的默认行为。这通常是添加特定功能、polyfill 特性、hook 到 API 的唯一方法,因为我们没法直接对这些 API 进行访问。

例如,像是 Bugsnag 这样的监测工具,重写了 Fetch 和 XMLHttpRequest 的 API 来获取由 JavaScript 代码触发的网络连接相关信息。

猴子补丁是个强大而危险的技巧,因为你没法控制那些被你覆盖的代码:未来 JavaScript 引擎的更新可能会打破你在补丁中做出的一些假设,并导致严重的 Bug。

另外,对那些并非由你负责的代码打猴子补丁,可能会覆盖一些被其他开发者加入的猴子补丁,引入潜在的冲突。

由于种种原因,有时需要确定给定函数是否是原生函数,是否被打了猴子补丁,但是我们能做到吗?

用 toString() 来检查函数上的猴子补丁

检查一个函数是否 “干净”(没有猴子补丁) 最常用的方式那就是检查函数的 toString() 输出。

默认情况下,原生函数的 toString() 返回这么一行 "function fetch() { [native code] }"

6323f20ea9428.jpg

依照运行 JavaScript 引擎的不同,输出结果会略有不同。不过,在大多数浏览器中,还是可以很安全的假定返回的字符串中会包含 "[native code]" 。

打过猴子补丁的原生函数,它的 toString() 将不会返回包含 "[native code]" 的字符串,而是会返回字符串化的函数体。

所以说,想要知道函数是否仍是原生的,我们可以通过检测 toString() 输出是否包含 "[native code]" 来简单判断。

基本的检测方式如下:

function isNativeFunction(f) {
  return f.toString().includes("[native code]");
}

isNativeFunction(window.fetch); // → true

// 对 fetch API 打猴子补丁
(function () {
  const { fetch: originalFetch } = window;
  window.fetch = function fetch(...ƒargs) {
    console.log("Fetch call intercepted:", ...args);
    return originalFetch(...args);
  };
})();

window.fetch.toString(); // → "function fetch(...args) {\n console.log("Fetch...

isNativeFunction(window.fetch); // → false

这种方式在大多数场景下都能正常生效。然而,你得清楚,很多伎俩可以让函数绕过这个检测。无论是出于恶意目的(注入恶意代码)还是说你不希望自己的覆盖行为被发现,有几种方法可以让函数看起来很 “原生”。

比如,可以添加一些包含 "[native code]" 的代码(甚至是一条注释!)在函数体里:

(function () {
  const { fetch: originalFetch } = window;
  window.fetch = function fetch(...args) {
    // function fetch() { [native code] }
    console.log("Fetch call intercepted:", ...args);
    return originalFetch(...args);
  };
})();

window.fetch.toString(); // → "function fetch(...args) {\n // function fetch...

isNativeFunction(window.fetch); // → true

… 或者,可以重写 toString() 方法,返回包含 "[native code]" 的字符串:

(function () {
  const { fetch: originalFetch } = window;
  window.fetch = function fetch(...args) {
    console.log("Fetch call intercepted:", ...args);
    return originalFetch(...args);
  };
})();

window.fetch.toString = function toString() {
  return `function fetch() { [native code] }`;
};

window.fetch.toString(); // → "function fetch() { [native code] }"

isNativeFunction(window.fetch); // → true

… 或者,可以用 bind 创建猴子补丁函数,这会生成一个原生函数:

(function () {
  const { fetch: originalFetch } = window;
  window.fetch = function fetch(...args) {
    console.log("Fetch call intercepted:", ...args);
    return originalFetch(...args);
  }.bind(window.fetch); // :point_left:
})();

window.fetch.toString(); // → "function fetch() { [native code] }"

isNativeFunction(window.fetch); // → true

… 或者,可以通过 ES6 的 Proxy 来捕获 apply() 调用,这样一来,从外部来看,函数完全是原生的:

window.fetch = new Proxy(window.fetch, {
  apply: function (target, thisArg, argumentsList) {
    console.log("Fetch call intercepted:", ...argumentsList);
    Reflect.apply(...arguments);
  },
});

window.fetch.toString(); // → "function fetch() { [native code] }"

isNativeFunction(window.fetch); // → true

好了,我就不举例子了。

我主要想强调的是: 开发者可以轻易地绕开你的 toString() 检测。

我觉得大多数情况下,不需要太在意上面那些边缘情况。但是如果你想的话,还是可以用一些额外检测来覆盖上面的用例。

  • 可以使用一次性的 iframe 来获取 “干净” 的toString() 值,再做严格匹配;
  • 可以多次调用.toString().toString() 确保toString() 不被重写;
  • 使用元编程技巧,对Proxy 构造函数自身来打个猴子补丁,以此来确定原生函数是否被代理过了(因为依照规范,无法察觉到什么东西是Proxy)

这完全取决于你想在 toString() 这个兔子洞里钻多深。

但是这真的值得吗?我们能够覆盖所有的边缘情况吗?

从 iframe 获取干净的函数

如果你需要调用一个 “干净” 的函数,而不是去检查原生函数是不是被打过猴子补丁,那么我们可以从同源的 iframe 中获取:

// 创建一个同源的 iframe
// 你可能需要添加一些样式先隐藏 iframe,稍后再从 dom 中彻底删除
const iframe = document.createElement("iframe");
document.body.appendChild(iframe);
// 新的 iframe 会创建它自身的 “干净” window 对象,这样你就可以从这里拿到你想要的函数了
const cleanFetch = iframe.contentWindow.fetch;

尽管,我觉得这种方式比调用 toString() 去做验证要好,但也会有一些局限性;

  • iframe 有时会由于强 CSP 或者 你的代码没有通过浏览器运行 而导致不可用。
  • 尽管不太现实,但第三方可以给 iframe API 上猴子补丁。所以还是不能 100% 信任生成 iframe 的 window 对象。
  • 修改或调用 DOM 的原生函数(比如document.createElement)没法使用这种方法,因为它们会指向 iframe 的 DOM 而不是顶层的 DOM。

通过判断引用是否相等来检查函数上的猴子补丁

如果安全是你首要考虑的因素,我认为你可以选择一种不同的方法:长期存储一个 “干净” 的原生函数引用,然后,用它来和可能的猴子补丁函数进行比较:

<html>
  <head>
    <script>
      // 在其他脚本修改原生函数之前,保存 “干净” 原生函数的原始引用。
      // 在这个例子中,我们保存了 fetch API 的原始引用
      // 并把它保存在闭包里。如果你无法预先决定要检查什么 API,
      // 那可以存储多个 window 对象。
      (function () {
        const { fetch: originalFetch } = window;
        window.__isFetchMonkeyPatched = function () {
          return window.fetch !== originalFetch;
        };
      })();
      // 现在开始,你可以调用 window.__isFetchMonkeyPatched()
      // 来检查 fetch API 是不是被打了猴子补丁
      //
      // 例如:
      window.fetch = new Proxy(window.fetch, {
        apply: function (target, thisArg, argumentsList) {
          console.log("Fetch call intercepted:", ...argumentsList);
          Reflect.apply(...arguments);
        },
      });
      window.__isFetchMonkeyPatched(); // → true
    </script>
  </head>
</html>

通过严格的引用检查,我们可以避免所有的 toString() 漏洞。甚至这种方式也能应用于Proxy,因为 Proxy 没法捕获相等性比较 。

这种方法最大的问题在于有点不切实际。它需要在运行任何 app 中其他代码之前,保存函数的原始引用,以确保函数没有被动过手脚。但我们有时根本没法做到这一点(比如,你构建的是库)。

那么,如何确定 JavaScript 原生函数是否被重写过呢?

我 需要 检查函数上猴子补丁的次数,用一只手都能数得过来。

不过我对这个问题很感兴趣,我认为对于很多场景,不存在真正万无一失的判定方法。

  • 如果你能控制整个网页,可以预先在函数都还是 “干净” 的时候存储它们,之后再进行比较。
  • 不然,你可以使用 iframe,创建一次性的 iframe 并从中获取 “干净” 的函数。但你要明白你还是无法 100% 确定 iframe API 是否被动了手脚。
  • 再者,由于 JavaScript 的动态特性,你可以简单使用toString().includes("[native code])" 来检查(但恶意代码很容易绕过这种检测)。你还可以增加大量的安全检测来覆盖大多数(没法做到全部)的边缘情况。
译者:@Tapir
译文:https://zhuanlan.zhihu.com/p/564194625
作者:@Mazzarolo Matteo
原文:https://mmazzarolo.com/blog/2022-07-30-checking-if-a-javascript-native-function-was-monkey-patched/

链接: https://www.fly63.com/article/detial/12129


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK