// ============================================================ // 3DTank WebSocket 伪造客户端 // 手动构造 Hash Request 并发送,捕获服务器响应 // 用法: node forge.js [session_name] // ============================================================ const WebSocket = require('ws'); const fs = require('fs'); const path = require('path'); // ============================================================ // 协议编码器 // ============================================================ // 变长整数编码 (对应 g7g) // n < 0x80: 1 byte // n < 0x4000: 2 bytes [0x80|(n>>8), n&0xFF] // n < 0x400000: 3 bytes [0xC0|(n>>16), (n>>8)&0xFF, n&0xFF] function encodeVarint(n) { if (n < 0x80) { return Buffer.from([n & 0x7F]); } else if (n < 0x4000) { const i = 0x8000 | (n & 0x3FFF); return Buffer.from([(i >> 8) & 0xFF, i & 0xFF]); } else { const r = 0xC00000 | (n & 0x3FFFFF); return Buffer.from([(r >> 16) & 0xFF, (r >> 8) & 0xFF, r & 0xFF]); } } // 变长整数解码 (对应 h7g) function decodeVarint(buf, offset) { const b0 = buf[offset]; if ((b0 & 0x80) === 0) { return { value: b0 & 0x7F, nextOffset: offset + 1 }; } const b1 = buf[offset + 1]; if ((b0 & 0x40) === 0) { // 2 bytes return { value: ((b0 & 0x3F) << 8) | b1, nextOffset: offset + 2 }; } // 3 bytes const b2 = buf[offset + 2]; return { value: ((b0 & 0x3F) << 16) | (b1 << 8) | b2, nextOffset: offset + 3 }; } // UTF-8 字符串编码 (对应 Qt.c7g) function encodeString(str) { const utf8 = Buffer.from(str, 'utf8'); return Buffer.concat([encodeVarint(utf8.length), utf8]); } // 字符串数组编码 (对应 Wt.c7g) function encodeStringArray(arr) { const parts = [encodeVarint(arr.length)]; for (const s of arr) { parts.push(encodeString(s)); } return Buffer.concat(parts); } // 命令 ID 编码 (对应 nn.c7g — 1 字节 int8) // 注意: 命令 ID 在此协议中是 1 字节, 不是 8 字节 int64! function encodeCommandId(cmdId) { return Buffer.from([cmdId & 0xFF]); } // Nullmap 编码 (对应 w7g) // 对于 0 个可选字段: 1 byte = 0x00 function encodeNullmap(bitCount, bitmap) { if (bitCount === 0) { return Buffer.from([0x00]); } if (bitCount <= 5) { // 1 byte: bits 在高位 (bit 3-7) const byte0 = bitmap[0] >>> 3; return Buffer.from([byte0 & 0xFF]); } if (bitCount <= 13) { // 2 bytes: flag 0x20 in first byte const b0 = ((bitmap[0] >>> 3) + 0x20) & 0xFF; const b1 = ((bitmap[1] >>> 3) + ((bitmap[0] << 5) & 0xFF)) & 0xFF; return Buffer.from([b0, b1]); } if (bitCount <= 21) { // 3 bytes: flag 0x40 in first byte const b0 = ((bitmap[0] >>> 3) + 0x40) & 0xFF; const b1 = ((bitmap[1] >>> 3) + ((bitmap[0] << 5) & 0xFF)) & 0xFF; const b2 = ((bitmap[2] >>> 3) + ((bitmap[1] << 5) & 0xFF)) & 0xFF; return Buffer.from([b0, b1, b2]); } // 对于更大的 nullmap, 使用扩展格式 const byteCount = Math.ceil(bitCount / 8); if (bitCount <= 504) { // 1 length byte (with 0x80 flag) + data const lenByte = (byteCount + 0x80) & 0xFF; return Buffer.concat([Buffer.from([lenByte]), Buffer.from(bitmap.slice(0, byteCount))]); } // 3 length bytes (with 0xC00000 flag) + data const lenVal = byteCount + 0xC00000; return Buffer.concat([ Buffer.from([(lenVal >> 16) & 0xFF, (lenVal >> 8) & 0xFF, lenVal & 0xFF]), Buffer.from(bitmap.slice(0, byteCount)) ]); } // 帧编码 (对应 e7f) // 帧 = [2或4字节长度(BE)][nullmap][payload] function encodeFrame(payload, nullmap) { const length = nullmap.length + payload.length; if (length < 0x4000) { // 2-byte format return Buffer.concat([ Buffer.from([(length >> 8) & 0xFF, length & 0xFF]), nullmap, payload ]); } else { // 4-byte format with 0x80 flag return Buffer.concat([ Buffer.from([ ((length >>> 24) & 0xFF) | 0x80, (length >>> 16) & 0xFF, (length >>> 8) & 0xFF, length & 0xFF ]), nullmap, payload ]); } } // 帧解码 (对应 f7f 的读取逻辑) function decodeFrame(buf) { if (buf.length < 2) return null; const b0 = buf[0]; let frameLength; let headerSize; let compressed = false; if ((b0 & 0x80) !== 0) { // 4-byte format if (buf.length < 4) return null; const b1 = buf[1], b2 = buf[2], b3 = buf[3]; frameLength = (((b0 & 0x7F) << 24) | (b1 << 16) | (b2 << 8) | b3) >>> 0; if (b0 & 0x40) compressed = true; // compression flag headerSize = 4; } else { // 2-byte format const b1 = buf[1]; frameLength = ((b0 & 0x3F) << 8) | b1; if (b0 & 0x40) compressed = true; headerSize = 2; } const totalFrameSize = headerSize + frameLength; if (buf.length < totalFrameSize) return null; const frameData = buf.slice(headerSize, totalFrameSize); return { frameData, headerSize, frameLength, compressed, totalFrameSize }; } // 解码 nullmap (对应 x7g) function decodeNullmap(buf, offset) { if (offset >= buf.length) return { nullmap: null, nextOffset: offset }; const r = buf[offset++]; if ((r & 0x80) !== 0) { // 大格式: bit 7 = 1 let size; if ((r & 0x40) !== 0) { // 3-byte size const b1 = buf[offset++]; const b2 = buf[offset++]; size = (((r & 0x3F) << 16) | (b1 << 8) | b2) >>> 0; } else { // 1-byte size size = r & 0x3F; } // 读取 size 字节的 nullmap 数据 const data = buf.slice(offset, offset + size); offset += size; return { nullmap: data, bitCount: size * 8, nextOffset: offset }; } // 小格式: bit 7 = 0 const tier = (r & 0x60) >>> 5; let bitCount; let data; switch (tier) { case 0: // 5 bits, 1 byte bitCount = 5; data = Buffer.from([r << 3]); // 移位到高位 break; case 1: { // 13 bits, 2 bytes bitCount = 13; const b1 = buf[offset++]; data = Buffer.from([ (r + (b1 >>> 5)) & 0xFF, (b1 << 3) & 0xFF ]); break; } case 2: { // 21 bits, 3 bytes bitCount = 21; const b1 = buf[offset++]; const b2 = buf[offset++]; data = Buffer.from([ (r + (b1 >>> 5)) & 0xFF, ((b1 << 3) + (b2 >>> 5)) & 0xFF, (b2 << 3) & 0xFF ]); break; } case 3: { // 29 bits, 4 bytes bitCount = 29; const b1 = buf[offset++]; const b2 = buf[offset++]; const b3 = buf[offset++]; data = Buffer.from([ (r + (b1 >>> 5)) & 0xFF, ((b1 << 3) + (b2 >>> 5)) & 0xFF, ((b2 << 3) + (b3 >>> 5)) & 0xFF, (b3 << 3) & 0xFF ]); break; } } return { nullmap: data, bitCount, nextOffset: offset }; } // ============================================================ // Hash Request 构造 // ============================================================ // 命令 ID 常量 (对应 w.p7j_1 等) const CMD_HASH_REQUEST = 1; const CMD_HASH_RESPONSE = 2; const CMD_OPEN_SPACE = 32; const CMD_CLIENT_LOG = 32; // 构造 Hash Request payload (对应 or.c7g + Li.c7g) function buildHashRequest(paramNames, paramValues) { // 1. Command ID (1 字节) = CMD_HASH_REQUEST const cmdId = encodeCommandId(CMD_HASH_REQUEST); // 2. paramNames 字符串数组 const names = encodeStringArray(paramNames); // 3. paramValues 字符串数组 const values = encodeStringArray(paramValues); // nullmap: 0 个可选字段 → 1 byte 0x00 const nullmap = encodeNullmap(0, null); const payload = Buffer.concat([cmdId, names, values]); return { payload, nullmap }; } // 构造 ClientLog 命令 (对应 Fi.c7g) // ClientLog = Bi(LogEntryType, string, string) // 编码: [cmdId=32][logEntryType.ordinal(1字节)][string1][string2] function buildClientLog(logType, str1, str2) { const cmdId = encodeCommandId(CMD_CLIENT_LOG); // 1 byte = 32 const typeByte = Buffer.from([logType & 0xFF]); // LogEntryType ordinal const s1 = encodeString(str1); const s2 = encodeString(str2); const nullmap = encodeNullmap(0, null); // 无可空字段 const payload = Buffer.concat([cmdId, typeByte, s1, s2]); return { payload, nullmap }; } // 默认 Hash Request 参数 (从 module_61860.js 提取) function getDefaultParams() { return [ ['lang', 'zh-CN'], ['browser_user_agent', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'], ['os', 'Win32'], ['device_type', 'DESKTOP'], ['client_type', 'DESKTOP'], ['hardware_concurrency', '8'], ['time_zone', 'Asia/Shanghai'], ['video_card', 'ANGLE (NVIDIA, NVIDIA GeForce RTX 3060 Direct3D11 vs_5_0 ps_5_0)'], ['mobile_device', 'false'], ['touchscreen_support', 'false'], ['client_version', 'e3b4a3f1c8b8ea24cddc91a30e51b6903d55c0c7'], ['ram_size', '16'], ]; } // ============================================================ // 工具函数 // ============================================================ function hexDump(buffer, prefix = '') { const bytes = Buffer.isBuffer(buffer) ? buffer : Buffer.from(buffer); const lines = []; for (let i = 0; i < bytes.length; i += 16) { const chunk = bytes.slice(i, Math.min(i + 16, bytes.length)); const hex = Array.from(chunk).map(b => b.toString(16).padStart(2, '0')).join(' '); const ascii = Array.from(chunk).map(b => (b >= 32 && b < 127) ? String.fromCharCode(b) : '.').join(''); lines.push(`${prefix}${i.toString(16).padStart(8, '0')} ${hex.padEnd(48)} ${ascii}`); } return lines.join('\n'); } // 尝试解码响应帧 function tryDecodeResponse(buf) { const frame = decodeFrame(buf); if (!frame) return null; const result = { frameLength: frame.frameLength, headerSize: frame.headerSize, compressed: frame.compressed, nullmap: null, payload: null, commands: [] }; let offset = 0; const frameData = frame.frameData; // 解码 nullmap const nm = decodeNullmap(frameData, offset); offset = nm.nextOffset; result.nullmap = nm.nullmap; // 剩余 = payload (可能包含多个命令) const payload = frameData.slice(offset); result.payload = payload; // 解码命令 (命令 ID = 1 字节, 然后是 body) let pOffset = 0; const cmdNames = { 1: 'Hash Request', 2: 'Hash Response', 32: 'Open Space' }; while (pOffset < payload.length) { const cmdStart = pOffset; const cmdId = payload[pOffset++]; // 1 字节命令 ID const cmdInfo = { offset: cmdStart, commandId: cmdId, name: cmdNames[cmdId] || '未知', body: null }; if (cmdId === 2) { // Hash Response: 32 字节 hash + 1 字节 boolean if (pOffset + 33 <= payload.length) { const hash = payload.slice(pOffset, pOffset + 32); const boolean = payload[pOffset + 32]; cmdInfo.body = { hash: hash.toString('hex'), hashBytes: Array.from(hash), boolean: boolean !== 0 }; pOffset += 33; } else { cmdInfo.body = { error: 'Hash Response body 不完整', remaining: payload.length - pOffset }; break; } } else if (cmdId === 32) { // Open Space: 8 字节 64位 Long (BIG_ENDIAN) = Space ID // 对应 Xt.d7g → n2t(): W(getInt32(n, BE), getInt32(n+4, BE)) = W(high, low) if (pOffset + 8 <= payload.length) { const spaceIdBuf = payload.slice(pOffset, pOffset + 8); // BIG_ENDIAN: high 在前, low 在后 const high = spaceIdBuf.readInt32BE(0); const low = spaceIdBuf.readUInt32BE(4); // 组合成 64 位值 (JavaScript 无原生 int64, 用字符串表示) const spaceIdStr = (high * 0x100000000 + low).toString(); cmdInfo.body = { rawHex: spaceIdBuf.toString('hex'), spaceIdHigh: high, spaceIdLow: low, spaceId: spaceIdStr }; pOffset += 8; } else { cmdInfo.body = { error: 'Open Space body 不完整', remaining: payload.length - pOffset }; break; } } else { // 未知命令, 记录剩余字节 const bodyBytes = payload.slice(pOffset); cmdInfo.body = { rawHex: bodyBytes.toString('hex'), rawLength: bodyBytes.length }; pOffset = payload.length; } result.commands.push(cmdInfo); } return result; } // ============================================================ // 主程序 // ============================================================ const sessionName = process.argv[2] || `forge_${Date.now()}`; const sessionDir = path.join(__dirname, sessionName); if (!fs.existsSync(sessionDir)) { fs.mkdirSync(sessionDir, { recursive: true }); } const logFile = path.join(sessionDir, 'log.txt'); const logStream = fs.createWriteStream(logFile, { flags: 'w' }); function log(msg) { const line = `[${new Date().toISOString()}] ${msg}`; console.log(line); logStream.write(line + '\n'); } function saveBinary(data, direction, seq) { const filename = `${direction}_${seq.toString().padStart(4, '0')}.bin`; const filepath = path.join(sessionDir, filename); fs.writeFileSync(filepath, data); return filename; } // 构造 Hash Request const params = getDefaultParams(); const paramNames = params.map(p => p[0]); const paramValues = params.map(p => p[1]); log('=== 3DTank WebSocket 伪造客户端 ==='); log(`会话: ${sessionName}`); log(`目录: ${sessionDir}`); log(''); log('--- Hash Request 参数 ---'); params.forEach(([k, v]) => log(` ${k} = ${v.length > 60 ? v.slice(0, 57) + '...' : v}`)); log(''); const { payload, nullmap } = buildHashRequest(paramNames, paramValues); const frame = encodeFrame(payload, nullmap); log('--- 编码结果 ---'); log(`Payload 长度: ${payload.length} 字节`); log(`Nullmap: ${nullmap.toString('hex')} (${nullmap.length} 字节)`); log(`Frame 总长: ${frame.length} 字节`); log(''); log('Frame 十六进制:'); log(hexDump(frame)); log(''); // 连接参数 // 默认连真实服务器;加 --url ws://localhost:9091/ 可改为连本地中继/模拟服务器 const url = (() => { const I = process.argv.indexOf('--url'); return I >= 0 && process.argv[I + 1] ? process.argv[I + 1] : 'wss://c1.3dtank.com:9090/'; })(); let recvSeq = 0; let sendSeq = 0; let hashRequestSent = false; let clientLogSent = false; log(`正在连接 ${url} ...`); const ws = new WebSocket(url, { rejectUnauthorized: false, headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', 'Origin': 'https://3dtank.com' } }); ws.binaryType = 'arraybuffer'; ws.on('open', function open() { log('✓ 连接成功!'); log(''); // 发送 Hash Request log('>>> 发送 Hash Request...'); const fname = saveBinary(frame, 'send', sendSeq++); log(` 已保存: ${fname}`); log(` ${frame.length} 字节:`); log(hexDump(frame, ' ')); log(''); ws.send(frame, { binary: true, mask: true }, function(err) { if (err) { log(`✗ 发送失败: ${err.message}`); } else { hashRequestSent = true; log('✓ Hash Request 已发送'); log(''); } }); }); ws.on('message', function incoming(data) { const buf = Buffer.from(data); const fname = saveBinary(buf, 'recv', recvSeq++); log(`<<< 接收消息 #${recvSeq - 1} (${buf.length} 字节) -> ${fname}`); log(hexDump(buf, ' ')); log(''); // 尝试解码帧 const decoded = tryDecodeResponse(buf); if (decoded) { log(` 帧解码:`); log(` 头部长度: ${decoded.headerSize} 字节`); log(` 帧体长度: ${decoded.frameLength} 字节`); log(` 压缩: ${decoded.compressed}`); log(` Nullmap: ${decoded.nullmap ? decoded.nullmap.toString('hex') : 'null'} (${decoded.nullmap ? decoded.nullmap.length : 0} 字节)`); log(` Payload 长度: ${decoded.payload ? decoded.payload.length : 0} 字节`); if (decoded.commands && decoded.commands.length > 0) { log(` 命令 (${decoded.commands.length} 个):`); decoded.commands.forEach((cmd, i) => { log(` [${i}] offset=${cmd.offset} ID=${cmd.commandId} (${cmd.name})`); if (cmd.body) { if (cmd.body.hash) { log(` hash: ${cmd.body.hash}`); log(` boolean: ${cmd.body.boolean}`); } else if (cmd.body.spaceId !== undefined) { log(` spaceId (BE int64): ${cmd.body.spaceId}`); log(` high=${cmd.body.spaceIdHigh}, low=${cmd.body.spaceIdLow}`); log(` raw: ${cmd.body.rawHex}`); } else if (cmd.body.rawHex) { log(` body (${cmd.body.rawLength} 字节): ${cmd.body.rawHex}`); } else if (cmd.body.error) { log(` 错误: ${cmd.body.error}`); } } }); } // 空 payload = ping/keepalive, 回复 pong (相同的空帧 00 01 00) if (decoded.payload && decoded.payload.length === 0) { const pong = Buffer.from([0x00, 0x01, 0x00]); const pongFname = saveBinary(pong, 'send', sendSeq++); log(`>>> 回复 Pong (ping 保持连接) -> ${pongFname}`); ws.send(pong, { binary: true, mask: true }); } // 收到 Open Space 后发送 ClientLog (测试是否能保持连接) if (decoded.commands) { const hasOpenSpace = decoded.commands.some(c => c.commandId === 32 && c.body && c.body.spaceId !== undefined); if (hasOpenSpace && !clientLogSent) { clientLogSent = true; const { payload: clPayload, nullmap: clNullmap } = buildClientLog(0, 'client_ready', 'forge_test'); const clFrame = encodeFrame(clPayload, clNullmap); const clFname = saveBinary(clFrame, 'send', sendSeq++); log(`>>> 发送 ClientLog (测试保活) -> ${clFname}`); log(` ${clFrame.length} 字节: ${clFrame.toString('hex')}`); ws.send(clFrame, { binary: true, mask: true }); } } } log(''); }); ws.on('close', function close(code, reason) { log('---'); log(`✗ 连接关闭 (code=${code})`); if (reason && reason.length > 0) { log(` 原因: ${reason.toString()}`); } log(` Hash Request 已发送: ${hashRequestSent}`); log(` 总共接收: ${recvSeq} 条消息`); log(` 总共发送: ${sendSeq} 条消息`); logStream.end(); }); ws.on('error', function error(err) { log(`✗ 错误: ${err.message}`); if (err.stack) log(err.stack); // 不在这里 end logStream, 让 close 事件处理 }); // 优雅退出 process.on('SIGINT', function() { log('---'); log('用户中断,正在关闭...'); if (ws.readyState === WebSocket.OPEN) ws.close(); logStream.end(); process.exit(0); }); // 超时退出 (5 分钟) setTimeout(() => { log('---'); log('超时自动退出 (5 分钟)'); if (ws.readyState === WebSocket.OPEN) ws.close(); logStream.end(); process.exit(0); }, 5 * 60 * 1000);