From 240892c9b5e726f29cb34b09463fdd1754fa36ec Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Sun, 11 Oct 2026 20:54:38 +0800 Subject: [PATCH] =?UTF-8?q?fix(mirror):=20ws=20=E6=8A=93=E5=8C=85=E6=94=B9?= =?UTF-8?q?=E4=B8=BA=E5=8E=9F=E5=9E=8B=E7=BA=A7=20hook=EF=BC=88=E4=B8=8D?= =?UTF-8?q?=E5=8A=A8=20window.WebSocket=20=E6=9E=84=E9=80=A0=E5=87=BD?= =?UTF-8?q?=E6=95=B0=EF=BC=89=EF=BC=8C=E9=81=BF=E5=85=8D=E8=A2=AB=E6=B8=B8?= =?UTF-8?q?=E6=88=8F=E5=AF=9F=E8=A7=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实测替换构造函数会让 WebSocket.toString() 不再是 native code → 进不去战场;改为只包 WebSocket.prototype 的 send / addEventListener / onmessage|onopen|onclose 访问器 - window.WebSocket 的名字/长度/toString/instanceof/静态常量全部保持原生 --- sockets/ws_hook.js | 115 +++++++++++++++++++++++++++++++-------------- 1 file changed, 79 insertions(+), 36 deletions(-) diff --git a/sockets/ws_hook.js b/sockets/ws_hook.js index b570153..b61c371 100644 --- a/sockets/ws_hook.js +++ b/sockets/ws_hook.js @@ -4,10 +4,16 @@ // 目的:在不改变「服务端看到的连接」的前提下,把浏览器真实的 ws 帧抓下来, // 用于与「中继转存」逐帧对比,定位「直连能进、中继不能进」的差异。 // -// 挂载:serve.js 提供 /ws_hook.js;web/index.html 在主脚本之前引入(先于游戏创建 WebSocket)。 -// 开关:**默认不启用**;URL hash 加 #hook 才抓(#nohook 可强制关)。 +// 关键设计(避免被游戏察觉): +// **不替换 window.WebSocket 构造函数**,只在原型上补: +// WebSocket.prototype.send / addEventListener / onmessage|onopen|onclose 访问器 +// → WebSocket.name / .length / .toString() / instanceof / 静态常量 全部保持原生。 +// (替换构造函数会让 WebSocket.toString() 不再是 "[native code]",实测会进不去战场。) +// +// 挂载:serve.js 提供 /ws_hook.js;web/index.html 在主脚本之前引入。 +// 开关:**默认不启用**;URL hash 加 #hook 才抓(#nohook 强制关)。 // 产物: -// window.__ws_capture__ —— 内存里的抓包(connections[].messages[] = {d,t,n,b}, +// window.__ws_capture__ —— 内存抓包(connections[].messages[] = {d,t,n,b}, // d=c2s/s2c,n=字节数,b=base64 原始字节,顺序即收发顺序) // window.__ws_save__() —— 手动保存;离开页面时自动 sendBeacon 再存一份 // 落盘位置:serve.js 的 POST /__dump__/ → 3DTank/names/ @@ -16,7 +22,6 @@ if (window.__ws_hooked__) return; if (!/#hook/.test(location.hash) || /#nohook/.test(location.hash)) { console.log('[WS Hook] 未启用(默认关闭;URL 加 #hook 开启抓包)'); - // 兜底:即使没启用也定义,调用时提示怎么开,避免"__ws_save__ 不存在" window.__ws_save__ = function () { console.warn('[WS Hook] 未启用:请在 URL 末尾加 #hook(如 http://localhost:8080/#hook)后整页刷新,再调用 __ws_save__()'); return Promise.resolve(null); @@ -27,7 +32,8 @@ var Capture = { startedAt: Date.now(), connections: [] }; window.__ws_capture__ = Capture; - var Original = window.WebSocket; + var Proto = window.WebSocket.prototype; // 只动原型,window.WebSocket 本体保持原生 + var ByWs = new WeakMap(); function ToBytes(Data) { if (Data instanceof ArrayBuffer) return new Uint8Array(Data); @@ -37,7 +43,7 @@ } function ToBase64(Bytes) { var S = ''; - // 注意:String.fromCharCode.apply 会把参数数组整包展开成实参,块太大必然爆栈 + // 注意:String.fromCharCode.apply 会把参数数组整包展开成实参,块太大必爆栈 //(V8 实参上限约 6.5 万)。这里每块 8192 字节,远低于上限,且比逐字节拼接快得多。 var Chunk = 0x2000; for (var I = 0; I < Bytes.length; I += Chunk) { @@ -46,41 +52,78 @@ return btoa(S); } - function Hooked(Url, Protocols) { - var Ws = Protocols ? new Original(Url, Protocols) : new Original(Url); - var Info = { id: Capture.connections.length, url: String(Url), openTime: null, closeCode: null, messages: [] }; - Capture.connections.push(Info); - console.log('[WS Hook] #' + Info.id + ' 连接 ' + Info.url); - - function Record(Dir, Data) { - var Bytes = ToBytes(Data); - Info.messages.push({ d: Dir, t: Date.now(), n: Bytes.length, b: ToBase64(Bytes) }); + function Info(Ws) { + var I = ByWs.get(Ws); + if (!I) { + I = { id: Capture.connections.length, url: '', openTime: null, closeCode: null, messages: [] }; + ByWs.set(Ws, I); + Capture.connections.push(I); } - - var OrigSend = Ws.send; - Ws.send = function (Data) { - Record('c2s', Data); - return OrigSend.apply(Ws, arguments); - }; - Ws.addEventListener('message', function (Ev) { Record('s2c', Ev.data); }); - Ws.addEventListener('open', function () { Info.openTime = Date.now(); }); - Ws.addEventListener('close', function (Ev) { Info.closeCode = Ev.code; }); - - return Ws; + if (!I.url && Ws.url) I.url = String(Ws.url); + return I; } - Hooked.CONNECTING = Original.CONNECTING; - Hooked.OPEN = Original.OPEN; - Hooked.CLOSING = Original.CLOSING; - Hooked.CLOSED = Original.CLOSED; - Hooked.prototype = Original.prototype; - window.WebSocket = Hooked; + function Record(Ws, Dir, Data) { + var I = Info(Ws); + var B = ToBytes(Data); + I.messages.push({ d: Dir, t: Date.now(), n: B.length, b: ToBase64(B) }); + } + + // ① send(原型级) + var OrigSend = Proto.send; + Proto.send = function (Data) { + try { Record(this, 'c2s', Data); } catch (E) {} + return OrigSend.apply(this, arguments); + }; + + // ② addEventListener(原型级,只包 message/open/close) + var OrigAdd = Proto.addEventListener; + Proto.addEventListener = function (Type, Listener, Opts) { + if (typeof Listener !== 'function') return OrigAdd.call(this, Type, Listener, Opts); + var Self = this; + var Wrapped; + if (Type === 'message') { + Wrapped = function (Ev) { try { Record(Self, 's2c', Ev.data); } catch (E) {} return Listener.call(this, Ev); }; + } else if (Type === 'open') { + Wrapped = function (Ev) { try { Info(Self).openTime = Date.now(); } catch (E) {} return Listener.call(this, Ev); }; + } else if (Type === 'close') { + Wrapped = function (Ev) { try { Info(Self).closeCode = Ev && Ev.code; } catch (E) {} return Listener.call(this, Ev); }; + } else { + return OrigAdd.call(this, Type, Listener, Opts); + } + Wrapped.__hbOrig = Listener; + return OrigAdd.call(this, Type, Wrapped, Opts); + }; + + // ③ onmessage / onopen / onclose 属性访问器(原型级) + function WrapAccessor(Name, OnEvent) { + var D = Object.getOwnPropertyDescriptor(Proto, Name); + if (!D || !D.set || !D.get) return; + Object.defineProperty(Proto, Name, { + configurable: true, + enumerable: D.enumerable, + get: function () { + var F = D.get.call(this); + return F && F.__hbOrig ? F.__hbOrig : F; // 还原本来的处理函数,保持 `ws.onmessage === fn` 成立 + }, + set: function (Fn) { + if (typeof Fn !== 'function') return D.set.call(this, Fn); + var Self = this; + var Wrapped = function (Ev) { try { OnEvent(Self, Ev); } catch (E) {} return Fn.call(this, Ev); }; + Wrapped.__hbOrig = Fn; + return D.set.call(this, Wrapped); + }, + }); + } + WrapAccessor('onmessage', function (W, Ev) { Record(W, 's2c', Ev.data); }); + WrapAccessor('onopen', function (W) { Info(W).openTime = Date.now(); }); + WrapAccessor('onclose', function (W, Ev) { Info(W).closeCode = Ev && Ev.code; }); function Stamp() { return new Date().toISOString().replace(/[:.]/g, '-'); } - function Save(Prefix) { + function Save() { var Json = JSON.stringify(Capture); - var Url = '/__dump__/' + (Prefix || 'ws_capture_') + Stamp() + '.json'; + var Url = '/__dump__/ws_capture_' + Stamp() + '.json'; return fetch(Url, { method: 'POST', body: Json }) .then(function (R) { console.log('[WS Hook] 已保存 ' + Url + '(' + Json.length + ' 字节,' + Capture.connections.length + ' 个连接)'); @@ -88,7 +131,7 @@ }) .catch(function (E) { console.warn('[WS Hook] 保存失败: ' + (E && E.message)); }); } - window.__ws_save__ = function () { return Save('ws_capture_'); }; + window.__ws_save__ = Save; window.addEventListener('beforeunload', function () { try { @@ -97,5 +140,5 @@ } catch (E) {} }); - console.log('[WS Hook] 已挂载:window.__ws_capture__(内存)/ __ws_save__()(存到 3DTank/names/)'); + console.log('[WS Hook] 已挂载(原型级,不改构造函数):__ws_save__() 存到 3DTank/names/'); })();