JS `Hooking Browser APIs` (`XMLHttpRequest`, `fetch`, `localStorage`) 监听行为

各位靓仔靓女,今天咱们来聊点刺激的:JS Hooking Browser APIs,也就是“扒浏览器API的底裤”。放心,咱们不违法犯罪,只是为了更好地理解和控制我们的代码。

先来个友好的提醒:Hooking 是一把双刃剑,用好了可以降妖除魔,用不好可能引火烧身。所以,务必遵守法律法规,尊重他人隐私,仅用于学习和安全研究目的。

一、为什么要Hook?

想象一下,你正在做一个安全审计工具,需要监控网站的所有网络请求,或者你想调试一个第三方库,看看它到底往 localStorage 里塞了什么乱七八糟的东西。这时候,Hooking 就派上用场了。

简单来说,Hooking 就是在函数调用前后插入我们的代码,就像在高速公路上设置一个检查站,拦截每一辆经过的车,检查乘客信息。

二、Hooking 的几种姿势

咱们主要关注 XMLHttpRequestfetchlocalStorage 这三大金刚。

  1. XMLHttpRequest (XHR)

    XMLHttpRequest 可是个老家伙了,但依然活跃在前端舞台上。Hooking 它,可以监控所有的 AJAX 请求。

    • 原理: 替换原生的 XMLHttpRequest 对象,创建一个代理对象,重写 opensend 等方法。

    • 代码示例:

    (function() {
      var originalXHR = window.XMLHttpRequest;
    
      function MyXHR() {
        var xhr = new originalXHR();
        this.xhr = xhr; // 保存原始的XHR对象,方便调用
    
        // 重写 open 方法
        var originalOpen = xhr.open;
        xhr.open = function(method, url, async, user, password) {
          console.log("XHR Hook: open - Method:", method, "URL:", url);
          this.myURL = url; // 保存URL,方便后面使用
          return originalOpen.apply(xhr, arguments);
        };
    
        // 重写 send 方法
        var originalSend = xhr.send;
        xhr.send = function(data) {
          console.log("XHR Hook: send - Data:", data);
          this.myRequestData = data; // 保存请求数据
    
          // 监听请求状态变化
          xhr.addEventListener("readystatechange", function() {
            if (xhr.readyState === 4) {
              console.log("XHR Hook: readyStateChange - Status:", xhr.status, "Response:", xhr.responseText);
              // 在这里可以做一些处理,比如记录日志、修改响应数据等
              if(xhr.status === 200 && this.myURL.includes('api/sensitive')){
                  console.warn('发现敏感API调用,请注意!');
              }
            }
          });
    
          return originalSend.apply(xhr, arguments);
        };
    
        return xhr; // 返回代理的XHR对象
      }
    
      window.XMLHttpRequest = MyXHR; // 替换原生的XMLHttpRequest对象
    
      // 验证 Hook 是否生效
      var xhr = new XMLHttpRequest();
      xhr.open("GET", "https://api.example.com/data", true);
      xhr.send();
    })();
    • 代码解释:

      1. originalXHR = window.XMLHttpRequest;:保存原生的 XMLHttpRequest 对象,以便后续调用。
      2. MyXHR:自定义的构造函数,用于创建代理的 XMLHttpRequest 对象。
      3. xhr.open = function(...):重写 open 方法,在实际调用 open 之前,记录请求方法和 URL。
      4. xhr.send = function(...):重写 send 方法,在实际调用 send 之前,记录请求数据。
      5. xhr.addEventListener("readystatechange", function() { ... }):监听 readystatechange 事件,在请求完成时,记录状态码和响应数据。
      6. window.XMLHttpRequest = MyXHR;:将原生的 XMLHttpRequest 对象替换为我们自定义的 MyXHR 对象。
    • 注意事项:

      • apply 方法:用于改变 this 指向,确保在调用原生方法时,this 指向的是原始的 XMLHttpRequest 对象。
      • arguments 对象:包含了传递给 opensend 方法的所有参数。
      • readyState 4: 代表请求完成。
  2. fetch API

    fetch 是个时髦的家伙,基于 Promise,比 XMLHttpRequest 更加简洁易用。Hooking 它稍微复杂一点,因为 fetch 返回的是 Promise。

    • 原理: 替换全局的 fetch 函数,创建一个代理函数,在调用 fetch 前后插入我们的代码。

    • 代码示例:

    (function() {
      var originalFetch = window.fetch;
    
      window.fetch = function(url, options) {
        console.log("Fetch Hook: URL:", url, "Options:", options);
        this.myURL = url; // 保存URL
        this.myOptions = options; // 保存options
    
        return originalFetch.apply(this, arguments)
          .then(response => {
            console.log("Fetch Hook: Response Status:", response.status);
            // 克隆 response 对象,因为 response.body只能读取一次
            const clonedResponse = response.clone();
            return clonedResponse.text().then(body => {
              console.log("Fetch Hook: Response Body:", body);
              if(response.status === 200 && this.myURL.includes('api/sensitive')){
                  console.warn('发现敏感API调用,请注意!');
              }
              return response; // 返回原始的 response 对象
            });
          })
          .catch(error => {
            console.error("Fetch Hook: Error:", error);
            throw error; // 抛出错误,避免影响程序的正常运行
          });
      };
    
      // 验证 Hook 是否生效
      fetch("https://api.example.com/data")
        .then(response => response.json())
        .then(data => console.log("Fetch Data:", data));
    })();
    • 代码解释:

      1. originalFetch = window.fetch;:保存原生的 fetch 函数。
      2. window.fetch = function(url, options) { ... }:替换原生的 fetch 函数为我们自定义的函数。
      3. originalFetch.apply(this, arguments):调用原生的 fetch 函数,并传递参数。
      4. .then(response => { ... }):处理 Promise 的 resolve 状态,即请求成功。
      5. .catch(error => { ... }):处理 Promise 的 reject 状态,即请求失败。
      6. response.clone():克隆 response 对象,因为 response.body 只能读取一次。
      7. clonedResponse.text().then(body => { ... }):读取响应的文本内容。
    • 注意事项:

      • Promisefetch 返回的是 Promise 对象,需要使用 .then().catch() 来处理异步操作。
      • response.clone():由于 response.body 只能读取一次,如果需要多次使用响应数据,需要克隆 response 对象。
      • throw error:在 catch 块中,需要抛出错误,避免影响程序的正常运行。
  3. localStorage

    localStorage 是个存储数据的仓库,Hooking 它可以监控数据的写入和读取。

    • 原理: 替换 localStorage 对象的 setItemgetItemremoveItemclear 方法。

    • 代码示例:

    (function() {
      var originalSetItem = localStorage.setItem;
      var originalGetItem = localStorage.getItem;
      var originalRemoveItem = localStorage.removeItem;
      var originalClear = localStorage.clear;
    
      localStorage.setItem = function(key, value) {
        console.log("localStorage Hook: setItem - Key:", key, "Value:", value);
        return originalSetItem.apply(this, arguments);
      };
    
      localStorage.getItem = function(key) {
        var value = originalGetItem.apply(this, arguments);
        console.log("localStorage Hook: getItem - Key:", key, "Value:", value);
        return value;
      };
    
      localStorage.removeItem = function(key) {
        console.log("localStorage Hook: removeItem - Key:", key);
        return originalRemoveItem.apply(this, arguments);
      };
    
      localStorage.clear = function() {
        console.log("localStorage Hook: clear");
        return originalClear.apply(this, arguments);
      };
    
      // 验证 Hook 是否生效
      localStorage.setItem("name", "John Doe");
      var name = localStorage.getItem("name");
      localStorage.removeItem("name");
      localStorage.clear();
    })();
    • 代码解释:

      1. originalSetItem = localStorage.setItem;:保存原生的 setItem 方法。
      2. localStorage.setItem = function(key, value) { ... }:替换原生的 setItem 方法为我们自定义的函数。
      3. originalSetItem.apply(this, arguments):调用原生的 setItem 方法,并传递参数。
      4. getItemremoveItemclear 方法同理。
    • 注意事项:

      • this 指向:在 apply 方法中,this 指向的是 localStorage 对象。

三、更高级的玩法

  1. 使用 Proxy 对象 (ES6)

    Proxy 对象可以更优雅地实现 Hooking,避免直接修改原生对象。

    const handler = {
      get: function(target, prop, receiver) {
        console.log(`Getting ${prop}`);
        return Reflect.get(...arguments);
      },
      set: function(target, prop, value, receiver) {
        console.log(`Setting ${prop} to ${value}`);
        return Reflect.set(...arguments);
      }
    };
    
    const proxy = new Proxy(window, handler);
    
    // 现在,任何对 window 对象的属性访问或修改都会被 handler 拦截
    proxy.myVariable = "Hello World";
    console.log(proxy.myVariable);

    这种方式更加灵活,可以拦截更多的操作,比如属性的读取、删除等。

  2. 使用 MutationObserver API

    MutationObserver 可以监听 DOM 树的变化,包括属性的修改、节点的增删等。可以用来 Hook 一些基于 DOM 的操作。

    const observer = new MutationObserver(function(mutations) {
      mutations.forEach(function(mutation) {
        console.log("Mutation:", mutation);
      });
    });
    
    const config = { attributes: true, childList: true, subtree: true };
    
    observer.observe(document.body, config);
    
    // 现在,任何对 document.body 的修改都会被 observer 监听
    document.body.setAttribute("data-test", "123");

    这种方式适用于需要监听 DOM 变化的场景,比如监控第三方广告代码的执行。

四、Hooking 的应用场景

  • 安全审计: 监控网站的网络请求、数据存储等行为,发现潜在的安全漏洞。
  • 性能分析: 记录 API 的调用次数、耗时等信息,优化代码性能。
  • 调试工具: 拦截 API 调用,修改参数或返回值,模拟不同的场景。
  • 自动化测试: 模拟用户操作,验证代码的正确性。
  • 恶意代码分析: 分析恶意代码的行为,了解其攻击方式。

五、Hooking 的风险

  • 兼容性问题: Hooking 可能会破坏网站的正常功能,导致兼容性问题。
  • 性能问题: Hooking 会增加代码的执行时间,影响网站的性能。
  • 安全问题: Hooking 可能会被恶意代码利用,窃取用户数据或进行其他攻击。
  • 法律风险: Hooking 可能会违反法律法规,侵犯他人隐私。

六、总结

Hooking 是一项强大的技术,可以让我们更好地理解和控制代码的行为。但是,Hooking 也是一把双刃剑,需要谨慎使用。在使用 Hooking 时,务必遵守法律法规,尊重他人隐私,仅用于学习和安全研究目的。

最后,记住:能力越大,责任越大! 希望大家都能成为一名负责任的开发者,用技术改变世界,而不是破坏世界。

今天的讲座就到这里,希望大家有所收获! 如果以后有机会,咱们可以再聊聊更深入的 Hooking 技巧,比如如何 Hook Native Code,如何绕过 Anti-Hooking 机制等等。

祝大家编程愉快!

发表回复

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