在前端逆向、爬虫对抗与安全测试中,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 和请求体都会被打印出来。这是最基础的入门版本,但它有两个明显缺陷:
- 只能看请求,拿不到响应数据
- 无法拦截
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 演示、油猴脚本封装、浏览器扩展注入方式),或者调整文章的侧重点,都可以告诉我。