Hook XMLHttpRequest 实战

在前端逆向、爬虫对抗与安全测试中,Hook XMLHttpRequest(简称 XHR) 是最基础也最实用的技术之一。无论是定位加密参数、拦截接口数据,还是篡改请求 / 响应内容,掌握 XHR Hook 都能大幅提升调试效率。本文从原理到实战,一步步拆解完整的 Hook 方案,并给出可直接复用的代码模板。

一、为什么要 Hook XMLHttpRequest

XMLHttpRequest 是浏览器原生的 AJAX 核心对象,几乎所有传统网页的异步请求都基于它实现。当页面 JavaScript 发起请求时,数据会经过 XHR 对象的生命周期:open → setRequestHeader → send → onreadystatechange/onload → response

Hook 的本质就是在这个生命周期中插入我们的代理逻辑,实现:

  • 拦截所有请求 URL、请求头、请求体
  • 捕获接口返回的原始响应数据
  • 篡改请求参数或响应内容
  • 定位加密函数的调用时机
  • 监控页面异常请求行为

常见应用场景:前端加密逆向、接口数据采集、广告拦截、安全审计、自动化测试。

二、XHR 核心生命周期与可 Hook 点

在动手写代码之前,先明确 XHR 对象的关键方法和属性,这是所有 Hook 的切入点。

表格

方法 / 属性 作用 Hook 价值
open(method, url, async) 初始化请求 捕获请求方法与完整 URL
setRequestHeader(header, value) 设置请求头 抓取自定义请求头(如签名)
send(body) 发送请求 拦截 POST 请求体
onreadystatechange 状态变化回调 捕获响应原始数据
onload 请求成功回调 读取响应结果
response / responseText 响应内容 获取 / 篡改返回数据
status / statusText 状态码 判断请求结果

一个完整的 Hook 方案,需要同时覆盖请求发出前响应返回后两个阶段。

三、最简版 XHR Hook:拦截所有请求

先从最简单的版本入手,原理是保存原生构造函数,重写原型方法

复制代码
// 保存原生 open 和 send
const originalOpen = XMLHttpRequest.prototype.open;
const originalSend = XMLHttpRequest.prototype.send;

// Hook open 方法,捕获 method 和 url
XMLHttpRequest.prototype.open = function (method, url) {
    console.log('[XHR Hook] 请求:', method, url);
    // 保存到当前实例上,后续 send 可用
    this._method = method;
    this._url = url;
    return originalOpen.apply(this, arguments);
};

// Hook send 方法,捕获请求体
XMLHttpRequest.prototype.send = function (body) {
    console.log('[XHR Hook] 请求体:', body);
    return originalSend.apply(this, arguments);
};

把这段代码在控制台执行后,页面后续所有 XHR 请求的 URL 和请求体都会被打印出来。这是最基础的入门版本,但它有两个明显缺陷:

  1. 只能看请求,拿不到响应数据
  2. 无法拦截 setRequestHeader 设置的自定义头
  3. 不支持篡改响应

四、完整版 Hook:同时拦截请求与响应

要拿到响应数据,核心思路是劫持回调函数 。当 readyState === 4 时,响应已经就绪,此时可以读取 responseText

复制代码
(function () {
    const originalOpen = XMLHttpRequest.prototype.open;
    const originalSend = XMLHttpRequest.prototype.send;
    const originalSetRequestHeader = XMLHttpRequest.prototype.setRequestHeader;

    // 存储自定义请求头
    XMLHttpRequest.prototype.open = function (method, url) {
        this._url = url;
        this._method = method;
        this._headers = {};
        return originalOpen.apply(this, arguments);
    };

    // 拦截请求头
    XMLHttpRequest.prototype.setRequestHeader = function (header, value) {
        this._headers[header] = value;
        return originalSetRequestHeader.apply(this, arguments);
    };

    // 拦截 send 并挂载响应监听
    XMLHttpRequest.prototype.send = function (body) {
        const self = this;

        // 监听 readyState 变化
        function onReadyStateChange() {
            if (self.readyState === 4) {
                console.log('========== XHR 请求信息 ==========');
                console.log('URL:', self._url);
                console.log('方法:', self._method);
                console.log('请求头:', self._headers);
                console.log('请求体:', body);
                console.log('状态码:', self.status);
                console.log('响应内容:', self.responseText);
                console.log('==================================');
            }
        }

        // 兼容 onreadystatechange 和 addEventListener 两种写法
        if (this.onreadystatechange) {
            const originalCallback = this.onreadystatechange;
            this.onreadystatechange = function () {
                onReadyStateChange();
                return originalCallback.apply(this, arguments);
            };
        } else {
            this.addEventListener('readystatechange', onReadyStateChange);
        }

        return originalSend.apply(this, arguments);
    };
})();

这个版本已经可以完整抓取一条 XHR 从请求到响应的全部信息。但在真实站点中,很多代码会使用 onload 事件,甚至通过 addEventListener('load') 监听,因此还需要进一步兼容。

五、进阶:篡改响应数据

仅仅看数据还不够,很多场景下我们需要修改接口返回值 ,比如绕过校验、替换数据。思路是使用 Object.defineProperty 劫持 responseTextresponse 属性。

复制代码
(function () {
    const originalOpen = XMLHttpRequest.prototype.open;
    const originalSend = XMLHttpRequest.prototype.send;

    XMLHttpRequest.prototype.open = function (method, url) {
        this._url = url;
        return originalOpen.apply(this, arguments);
    };

    XMLHttpRequest.prototype.send = function () {
        const self = this;

        function hijackResponse() {
            if (self.readyState === 4 && self.status === 200) {
                // 只针对特定接口篡改
                if (self._url.includes('/api/user/info')) {
                    try {
                        const originalData = JSON.parse(self.responseText);
                        // 修改返回数据
                        originalData.data.vip = true;
                        originalData.data.balance = 99999;

                        const fakeResponse = JSON.stringify(originalData);

                        // 重写 responseText 和 response
                        Object.defineProperty(self, 'responseText', {
                            writable: true,
                            value: fakeResponse
                        });
                        Object.defineProperty(self, 'response', {
                            writable: true,
                            value: fakeResponse
                        });
                    } catch (e) {
                        console.warn('响应解析失败', e);
                    }
                }
            }
        }

        this.addEventListener('readystatechange', hijackResponse);
        return originalSend.apply(this, arguments);
    };
})();

这段代码会在 /api/user/info 接口返回后,把响应里的 vip 字段改成 true,余额改成 99999。页面后续代码读取 xhr.responseText 时,拿到的就是我们篡改后的数据。

注意 :部分站点会使用 responseType = 'json',此时直接读取 response 属性(对象形式),需要同时劫持 response 属性才生效。

六、实战场景:定位签名参数

逆向中最常见的需求:页面请求头里有一个 sign 参数,每次都变,想找到它的生成函数。

用 XHR Hook 可以快速定位:在 setRequestHeader 里打断点。

复制代码
const originalSetHeader = XMLHttpRequest.prototype.setRequestHeader;
XMLHttpRequest.prototype.setRequestHeader = function (header, value) {
    // 当设置 sign 头时,自动断住
    if (header.toLowerCase() === 'sign') {
        console.log('捕获到 sign:', value);
        debugger; // 调用栈里就能看到生成 sign 的函数
    }
    return originalSetHeader.apply(this, arguments);
};

执行后,一旦页面设置 sign 请求头,浏览器会自动断在 debugger 处。此时查看调用栈(Call Stack),向上回溯就能找到生成签名的业务代码。

七、常见对抗与绕过

1. 检测原型是否被篡改

部分站点会检测 XMLHttpRequest.prototype.open.toString() 是否包含 [native code],以此判断是否被 Hook。

绕过方式:重写 toString 方法,返回原生代码字符串。

复制代码
XMLHttpRequest.prototype.open.toString = function () {
    return 'function open() { [native code] }';
};

2. 使用 new XMLHttpRequest() 缓存原生对象

有些脚本会在页面最开头保存一份原生 XHR 引用,后续都用这份引用发起请求,此时原型 Hook 会失效。

应对方法:

  • 尽可能早地注入 Hook 脚本(在业务代码执行之前)
  • 使用浏览器扩展的 content scriptdocument_start 时机注入
  • 配合 Proxy 代理构造函数本身

3. Fetch API 的普及

现代站点越来越多地使用 fetch 替代 XHR。完整的接口拦截方案需要同时 Hook XHR 和 fetch。fetch 的 Hook 思路类似,重写 window.fetch 即可:

复制代码
const originalFetch = window.fetch;
window.fetch = function (url, options) {
    console.log('[Fetch Hook]', url, options);
    return originalFetch.apply(this, arguments).then(response => {
        const clone = response.clone();
        clone.text().then(data => console.log('[Fetch 响应]', data));
        return response;
    });
};

八、使用建议与注意事项

  1. 注入时机:Hook 代码必须在业务请求发起之前执行,否则已经创建的 XHR 实例不会被代理。
  2. 保留原生引用:务必把原始方法保存到变量中,不要覆盖后丢失引用,否则会造成死循环。
  3. 作用域隔离:使用立即执行函数(IIFE)包裹代码,避免污染全局变量。
  4. 异常处理:解析响应时加 try/catch,避免非 JSON 响应导致 Hook 代码报错,进而影响原页面。
  5. 性能影响:不要在 Hook 回调中写耗时操作,频繁的大 JSON 打印会拖慢页面。

总结

Hook XMLHttpRequest 是前端逆向和调试的基本功,核心逻辑并不复杂:代理原型方法 → 监听状态变化 → 读写请求 / 响应数据。在此基础上,配合属性劫持、调用栈回溯、反检测绕过,可以应对绝大多数站点的接口分析需求。

实际项目中,通常会把 XHR Hook、Fetch Hook、Cookie Hook、WebSocket Hook 封装成一套统一的请求监控工具,作为逆向调试的基础注入脚本。掌握了本文的思路,你就可以在此之上扩展出适合自己的调试工具箱。


这是一篇完整的技术文章,如果你需要补充更多实战案例(比如具体站点的 Hook 演示、油猴脚本封装、浏览器扩展注入方式),或者调整文章的侧重点,都可以告诉我。

相关推荐
quantdash_cc16 小时前
告别自建 Requests/BS4 网页爬虫:基于 QuantDash 搭建零维保的高性能量化行情流水线
开发语言·爬虫·python·pandas·量化·quantdash
Patrick在香港1 天前
Python爬虫框架实战:优雅抓取香港政府公开API数据的通用方案
java·开发语言·爬虫·python·ai编程·数据可视化·可用性测试
鬼手点金2 天前
与LLM结合的主流智能爬虫框架
爬虫·python·llm·post·request·firecrawl·crawl4ai
天启HTTP3 天前
爬虫频繁弹验证码?解析网站反爬检测机制
网络·爬虫·tcp/ip
深蓝电商API3 天前
如何快速定位加密函数?
爬虫·加密函数
oh,huoyuyan4 天前
网页公开数据采集工具推荐
人工智能·爬虫·火车采集器
崔子末4 天前
某影视库剧集列表以及查询接口逆向
爬虫·python
雪山青木4 天前
全国微博签到数据201912-202004
大数据·爬虫·python·数据挖掘·数据分析·新浪微博
TlSfoward4 天前
DLL 指纹库 采集、检索 TLSFOWARD tls指纹库
爬虫·网络协议·https·自动化