
在前端逆向、爬虫对抗与安全测试中Hook XMLHttpRequest简称 XHR是最基础也最实用的技术之一。无论是定位加密参数、拦截接口数据还是篡改请求 / 响应内容掌握 XHR Hook 都能大幅提升调试效率。本文从原理到实战一步步拆解完整的 Hook 方案并给出可直接复用的代码模板。一、为什么要 Hook XMLHttpRequestXMLHttpRequest 是浏览器原生的 AJAX 核心对象几乎所有传统网页的异步请求都基于它实现。当页面 JavaScript 发起请求时数据会经过 XHR 对象的生命周期open → setRequestHeader → send → onreadystatechange/onload → response。Hook 的本质就是在这个生命周期中插入我们的代理逻辑实现拦截所有请求 URL、请求头、请求体捕获接口返回的原始响应数据篡改请求参数或响应内容定位加密函数的调用时机监控页面异常请求行为常见应用场景前端加密逆向、接口数据采集、广告拦截、安全审计、自动化测试。二、XHR 核心生命周期与可 Hook 点在动手写代码之前先明确 XHR 对象的关键方法和属性这是所有 Hook 的切入点。表格方法 / 属性作用Hook 价值open(method, url, async)初始化请求捕获请求方法与完整 URLsetRequestHeader(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 和请求体都会被打印出来。这是最基础的入门版本但它有两个明显缺陷只能看请求拿不到响应数据无法拦截setRequestHeader设置的自定义头不支持篡改响应四、完整版 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劫持responseText和response属性。(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 script在document_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; }); };八、使用建议与注意事项注入时机Hook 代码必须在业务请求发起之前执行否则已经创建的 XHR 实例不会被代理。保留原生引用务必把原始方法保存到变量中不要覆盖后丢失引用否则会造成死循环。作用域隔离使用立即执行函数IIFE包裹代码避免污染全局变量。异常处理解析响应时加 try/catch避免非 JSON 响应导致 Hook 代码报错进而影响原页面。性能影响不要在 Hook 回调中写耗时操作频繁的大 JSON 打印会拖慢页面。总结Hook XMLHttpRequest 是前端逆向和调试的基本功核心逻辑并不复杂代理原型方法 → 监听状态变化 → 读写请求 / 响应数据。在此基础上配合属性劫持、调用栈回溯、反检测绕过可以应对绝大多数站点的接口分析需求。实际项目中通常会把 XHR Hook、Fetch Hook、Cookie Hook、WebSocket Hook 封装成一套统一的请求监控工具作为逆向调试的基础注入脚本。掌握了本文的思路你就可以在此之上扩展出适合自己的调试工具箱。这是一篇完整的技术文章如果你需要补充更多实战案例比如具体站点的 Hook 演示、油猴脚本封装、浏览器扩展注入方式或者调整文章的侧重点都可以告诉我。