1

检查原生 JavaScript 函数是否被覆盖 - chuckQu

 2 years ago
source link: https://www.cnblogs.com/chuckQu/p/16630509.html
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.
neoserver,ios ssh client

你如何确定一个JavaScript原生函数是否被覆盖? 你不能--或者至少无法可靠地确定。有一些检测方法很接近,但你不能完全相信它们。

JavaScript原生函数

在JavaScript中,原生函数指的是其源代码已经被编译进原生机器码的函数。原生函数可以在JavaScript 标准内置对象(比如说eval()parseInt()等等),以及浏览器Web API(比如说fetch()localStorage.getItem()等等)中找到。

由于JavaScript的动态特性,开发者可以覆盖浏览器暴露的原生函数。这种技术被称为"猴子补丁"。

猴子补丁主要用于修改浏览器内置API和原生函数的默认行为。这通常是添加特定功能、垫片功能或连接你无法访问的API的唯一途径。

比如说,诸如Bugsnag等监控工具覆盖了FetchXMLHttpRequest APIs,以获得对由JavaScript代码触发的网络连接的可见性。

猴子补丁是非常强大,但也是非常危险的技术。因为你所覆盖的代码不受你的控制:未来对JavaScript引擎的更新可能会打破你的补丁中的一些假设,从而导致严重的bug。

此外,通过对不属于你的代码进行猴子补丁,你可能会覆盖一些已经被其他开发者猴子补丁过的代码,从而引入潜在的冲突。

基于此,有时你可能需要测试一个给定的函数是否为原生函数,或者它是否被猴子补丁过...但你能做到吗?

使用toString()检查

检查一个函数是否仍然是 "干净的"(如未被猴子补丁)的最常用方法是检查其toString()的输出。

默认情况下,原生函数的toString()会返回类似于 "function fetch() { [native code] }"的内容。

fetch-native-code.png

这个字符串可能略有不同,这取决于运行的是什么JavaScript引擎。不过,在大多数浏览器中,你可以安全地认为这个字符串将包括"[native code]"子串。

通过对原生函数进行猴子补丁,它的toString()将停止返回"[native code]"字符串,而是返回字符串化的函数体。

因此,检查一个函数是否仍然是原生的一个简单方法是,检查其toString()输出是否包含"[native code]"字符串。

初步检查可能是这样的:

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

isNativeFunction(window.fetch); // → true

// Monkey patch the 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); // 👈
})();

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

isNativeFunction(window.fetch); // → true

或者,你可以用ES6代理来捕获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
const iframe = document.createElement("iframe");
document.body.appendChild(iframe);
// 新的iframe将创建自己的"干净"window对象,
// 所以你可以从那里抓取你感兴趣的函数。
const cleanFetch = iframe.contentWindow.fetch;

虽然我认为这种方法仍然比使用toString()验证一个函数好,但它仍然有一些明显的局限性:

  • 无论是因为强大的Content Security Policy (CSP),还是因为你的代码没有在浏览器中运行,有时iframes可能无法使用。
  • 虽然有点不切实际,但第三方可以对iframe的API进行猴子补丁。因此,你仍然不能100%地信任生成的iframewindow对象。
  • 改变或使用DOM的原生函数(如document.createElement)将无法使用这种方法,因为它们的目标是iframe的DOM,而不是顶层的。

使用全等检查

如果安全是你首要考虑的因素,我认为你应该采用不同的方法:持有一个"干净"原生函数的引用,稍后用潜在的猴子补丁函数与它进行比较。

<html>
  <head>
    <script>
    // 在任何其他脚本有机会修改原始的原生函数之前,存储一个引用。
    // 在这种情况下,我们只是持有一个原始fetchAPI的引用,并将其隐藏在一个闭包后面。
    // 如果你事先不知道你要检查什么API,你可能需要存储多个window对象的引用。
      (function () {
        const { fetch: originalFetch } = window;
        window.__isFetchMonkeyPatched = function () {
          return window.fetch !== originalFetch;
        };
      })();
      // 从现在开始,你可以通过调用window.__isFetchMonkeyPatched()
      // 来检查fetch API是否已经被猴子补丁过。
      //
      // Example:
      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()的漏洞。它甚至适用于代理,因为它们不能捕获相等比较。

这种方法的主要缺点是,它可能不切实际。它要求在运行应用程序中的任何其他代码之前存储原始函数引用(以确保它仍然未被触及),有时你将无法做到这一点(例如,你正在构建一个库)。

可能有一些方法可以打破这种方法,但在写这篇文章的时候,我还不知道这种方法。如果我遗漏了什么,请让我知晓。

如何确定是否被覆盖

我对这个问题的看法(或者更好的说法是 "猜测")是,根据不同的使用情况,可能没有一种失败的证明方法来确定它。

  • 如果你能控制整个网页,当它们仍然是"干净的"时候,你可以通过存储你想检查的函数的引用,来提前设置你的代码,然后再进行比较。
  • 否则,如果你能使用iframe,你可以创建一个隐藏的一次性iframe,并从那里抓取一个"干净 "的函数--要知道你仍然不能100%确定iframe的API没有被猴子补丁过。
  • 否则,考虑到JavaScript的动态性质,你可以使用简单的toString().includes("[native code]")检查,或者添加大量的安全检查来覆盖大多数(但不是全部)边缘情况。

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK