Hook WebSocket 实战

📅 2026/8/13 9:49:06
Hook WebSocket 实战
在前端逆向、数据采集与页面调试中WebSocket 是最常见的实时通信协议。大量行情、聊天、游戏、协同编辑类应用都会通过 WebSocket 双向推送数据而传统的 HTTP Hook 手段对其无效。本文从原生 API 原理出发完整讲解 WebSocket Hook 的核心思路、可落地代码与进阶对抗方案读完即可直接用于实际逆向场景。一、为什么要 Hook WebSocketHTTP 请求可以通过XMLHttpRequest、fetch拦截器轻松捕获但 WebSocket 基于长连接数据以帧的形式双向传输不走常规请求生命周期。如果我们需要实时查看页面收发的所有 WS 消息修改、伪造或重放业务数据分析二进制 / 加密消息的编解码逻辑绕过前端校验直接注入指令记录完整连接生命周期用于复现那么对 WebSocket 全局对象进行 Hook 就是最直接、最高效的手段。二、WebSocket 原生 API 与 Hook 切入点浏览器中 WebSocket 的核心能力集中在几个关键属性与方法上这也是我们的主要拦截点表格API作用Hook 价值new WebSocket(url, protocols)建立连接捕获连接地址、子协议、注入代理WebSocket.prototype.send(data)发送消息拦截 / 篡改上行数据onmessage/addEventListener(message)接收消息拦截 / 篡改下行数据onopen/onclose/onerror生命周期事件记录连接状态readyState/url/protocol实例属性获取连接元信息Hook 的核心思想在页面业务代码执行前重写全局 WebSocket 构造函数与原型方法用代理对象包裹真实实例对外保持接口完全一致内部完成消息劫持。三、基础版全局 Hook 收发消息这是最通用的实现兼容onmessage赋值和addEventListener两种事件绑定方式能打印所有收发的文本消息。(function hookWebSocket() { // 保存原生构造函数 const OriginalWebSocket window.WebSocket; // 重写全局 WebSocket window.WebSocket function (url, protocols) { // 创建真实实例 const ws protocols ? new OriginalWebSocket(url, protocols) : new OriginalWebSocket(url); console.log([WS Hook] 建立连接:, url); // 1. 拦截 send 方法上行 const originalSend ws.send.bind(ws); ws.send function (data) { console.log([WS Hook] 发送:, data); return originalSend(data); }; // 2. 拦截 onmessage 属性赋值下行 let userOnMessage null; Object.defineProperty(ws, onmessage, { get: () userOnMessage, set: (handler) { userOnMessage (event) { console.log([WS Hook] 接收:, event.data); handler.call(ws, event); }; } }); // 3. 拦截 addEventListener 方式绑定的 message 事件 const originalAddEventListener ws.addEventListener.bind(ws); ws.addEventListener function (type, listener, options) { if (type message) { const wrappedListener (event) { console.log([WS Hook] 接收:, event.data); return listener.call(ws, event); }; return originalAddEventListener(type, wrappedListener, options); } return originalAddEventListener(type, listener, options); }; return ws; }; // 原型链保持一致避免 instanceof 检测 window.WebSocket.prototype OriginalWebSocket.prototype; window.WebSocket.CONNECTING OriginalWebSocket.CONNECTING; window.WebSocket.OPEN OriginalWebSocket.OPEN; window.WebSocket.CLOSING OriginalWebSocket.CLOSING; window.WebSocket.CLOSED OriginalWebSocket.CLOSED; })();将这段代码在页面加载前注入Tampermonkeydocument-start、代理注入、控制台提前执行均可页面后续所有 WebSocket 连接的收发消息都会被打印到控制台。四、进阶实战消息篡改与二进制处理4.1 修改发送内容很多场景下我们不只是看还要改。比如修改上报的坐标、金额、操作指令等。只需要在send的代理中修改data再转发。ws.send function (data) { // 示例如果是 JSON 文本修改其中的字段 if (typeof data string) { try { const obj JSON.parse(data); if (obj.type heartbeat) { obj.timestamp Date.now(); // 篡改时间戳 data JSON.stringify(obj); } } catch (e) {} } return originalSend(data); };4.2 处理二进制消息ArrayBuffer / Blob大量高频场景会使用二进制传输行情、游戏帧直接打印看不到内容需要结合解码逻辑。// 接收时解析二进制 const wrappedListener (event) { if (event.data instanceof ArrayBuffer) { const view new DataView(event.data); // 示例读取前 4 字节作为指令码 const opcode view.getInt32(0, true); console.log([WS Hook] 二进制消息, 指令码:, opcode, 长度:, event.data.byteLength); } else if (event.data instanceof Blob) { event.data.arrayBuffer().then(buf { console.log([WS Hook] Blob 消息长度:, buf.byteLength); }); } return listener.call(ws, event); };如果对方使用 Protobuf、MessagePack 等序列化协议可以在 Hook 点把原始二进制保存下来配合反编译得到的 proto 文件做离线解析。4.3 主动向连接注入消息Hook 之后我们不仅能被动监听还能主动向已有连接注入假消息用于测试前端渲染逻辑。// 保存全局 ws 实例引用 window._wsInstances window._wsInstances || []; window._wsInstances.push(ws); // 控制台手动注入 // _wsInstances[0].send({cmd:refresh,id:1001});五、对抗场景绕过反 Hook 检测成熟的站点会检测 WebSocket 是否被篡改常见检测手段与绕过方式如下5.1 检测toString原生代码页面可能执行if (WebSocket.toString() ! [object WebSocketConstructor]) { // 判定被 Hook }绕过重写 Hook 后构造函数的toStringwindow.WebSocket.toString () function WebSocket() { [native code] };5.2 检测原型链与instanceof如果重写不当ws instanceof WebSocket会返回 false。前面基础版代码中已经做了原型指向保持可绕过大部分检测。5.3 页面缓存原生引用如果业务代码在你的 Hook 执行前就把const WS window.WebSocket保存了下来后续全部用缓存的构造函数建连Hook 就会失效。解决方案使用浏览器扩展在document_start阶段尽早注入使用代理层如 Charles、mitmproxy在响应中把 Hook 脚本插入到 HTML 最顶部针对 Worker 中的 WebSocket需要单独注入Worker构造函数 Hook5.4 Web Worker 中的 WebSocket很多站点把 WS 放在 Worker 里跑避开主线程 Hook。这时需要连 Worker 一起代理const OriginalWorker window.Worker; window.Worker function (scriptURL, options) { const worker new OriginalWorker(scriptURL, options); // 通过 postMessage 无法直接 Hook Worker 内部 // 更彻底的方式使用 Blob 改写 worker 脚本在内部注入 WS Hook return worker; };六、实用工具化可复用的 Hook 模板在实际逆向工作中可以把下面这段作为通用模板按需开启打印、篡改、保存、重放功能。const WsHook { instances: [], enableLog: true, onSend: null, // 自定义发送回调 onRecv: null, // 自定义接收回调 install() { const OrigWS window.WebSocket; const self this; window.WebSocket function (url, protocols) { const ws protocols ? new OrigWS(url, protocols) : new OrigWS(url); self.instances.push(ws); // 拦截 send const origSend ws.send.bind(ws); ws.send (data) { if (self.enableLog) console.log([WS ↑], data); if (typeof self.onSend function) { const modified self.onSend(data, ws); if (modified ! undefined) data modified; } return origSend(data); }; // 拦截 message 事件 const wrap (fn) (e) { if (self.enableLog) console.log([WS ↓], e.data); if (typeof self.onRecv function) { self.onRecv(e.data, ws, e); } return fn.call(ws, e); }; // onmessage 属性 let userHandler null; Object.defineProperty(ws, onmessage, { get: () userHandler, set: (fn) { userHandler wrap(fn); } }); // addEventListener const origAdd ws.addEventListener.bind(ws); ws.addEventListener (type, fn, opts) { if (type message) fn wrap(fn); return origAdd(type, fn, opts); }; return ws; }; window.WebSocket.prototype OrigWS.prototype; Object.assign(window.WebSocket, OrigWS); window.WebSocket.toString () function WebSocket() { [native code] }; } }; // 使用 WsHook.install(); WsHook.onSend (data) { /* 自定义处理 */ };七、适用边界与补充方案Hook WebSocket 虽然强大但也有其边界无法拦截加密帧本身Hook 拿到的是浏览器解密后的数据WSS 加密在传输层不影响应用层 Hook帧分片、压缩扩展浏览器会自动处理我们拿到的始终是完整消息强混淆 动态代码如果页面频繁动态创建 WebSocket 并立即销毁需要结合调用栈定位业务代码更底层的需求如果需要分析原始帧、做流量重放建议结合 Charles、Wireshark 或 mitmproxy 做代理层抓包。写在最后WebSocket Hook 是前端逆向的基础技能之一本质上是对浏览器原生 API 的代理与封装。掌握它之后你可以用同样的思路去 Hookfetch、XHR、Canvas、WebAudio等各类浏览器接口形成一套完整的前端调试与逆向工具箱。在实际项目中建议先上基础版看明文遇到二进制再上解码遇到反检测再逐步加固伪装层分层递进效率最高。