// decrypt_frames.js - 破解并解密游戏通信流(自变体 XOR 流密码) // 用法: node decrypt_frames.js [--dir ../packets] [--label relay-04] [--key N] [--verbose] // // 密码算法(逆向自 module_61860,类 ir): // 构造: key[8] 数组,key[i] = K ^ (i << 3) (解码方向 g7m_1) // key[i] = K ^ (i << 3) ^ 87 (编码方向 i7m_1) // 解码: p = c ^ key[idx]; key[idx] = p; idx ^= (p & 7) // 编码: c = p ^ key[idx]; key[idx] = p; idx ^= (p & 7) —— 与解码同构 // K 由握手数据折叠而来(1 字节),故可直接穷举 256 种可能: // 正确的 K 会让解密后的每个 WebSocket 消息都成为长度自洽的帧。 const fs = require('fs'); const path = require('path'); const zlib = require('zlib'); const PROJECT = path.resolve(__dirname, '..'); const Argv = process.argv.slice(2); const Arg = (Name, Def) => { const I = Argv.indexOf('--' + Name); return I >= 0 && Argv[I + 1] ? Argv[I + 1] : Def; }; const PACKET_DIR = path.resolve(Arg('dir', path.join(PROJECT, 'packets'))); const LABEL = Arg('label', ''); const FIXED_KEY = Arg('key', null); const VERBOSE = Argv.indexOf('--verbose') >= 0; // 帧头:{ TotalSize, HeaderSize, BodySize } —— 返回 null 表示数据不足 function PeekFrame(Buf) { if (Buf.length < 2) return null; const B0 = Buf[0]; if (B0 & 0x80) { if (Buf.length < 4) return null; return { TotalSize: 4 + ((((B0 & 0x3f) << 24) | (Buf[1] << 16) | (Buf[2] << 8) | Buf[3]) >>> 0), HeaderSize: 4, Long: true, Compressed: (B0 & 0x40) !== 0, }; } return { TotalSize: 2 + (((B0 & 0x3f) << 8) | Buf[1]), HeaderSize: 2, Long: false, Compressed: (B0 & 0x40) !== 0 }; } // nullmap 长度(对应 x7g) function NullmapSize(Buf, Offset) { const R = Buf[Offset]; if (R === undefined) return 0; if ((R & 0x80) !== 0) { if (R & 0x40) return 4 + (((R & 0x3f) << 16) | (Buf[Offset + 1] << 8) | Buf[Offset + 2]); return 1 + (R & 0x3f); } return 1 + ((R & 0x60) >>> 5); } // 流密码解密:Bufs 依次解密,密钥状态跨消息连续 function DecryptStream(Bufs, Key, Encoded) { const Slot = new Int32Array(8); for (let I = 0; I < 8; I++) Slot[I] = ((Key ^ (I << 3)) ^ (Encoded ? 87 : 0)) & 0xff; let Idx = 0; return Bufs.map((Buf) => { const Out = Buffer.alloc(Buf.length); for (let I = 0; I < Buf.length; I++) { const Plain = (Buf[I] ^ Slot[Idx]) & 0xff; Out[I] = Plain; Slot[Idx] = Plain; Idx = Idx ^ (Plain & 7); } return Out; }); } // 评分:解密后有多少条消息是长度自洽的帧 function Score(Decoded) { let Ok = 0; let Ping = 0; for (const Buf of Decoded) { const Info = PeekFrame(Buf); if (Info && Info.TotalSize === Buf.length) { Ok++; const At = Info.HeaderSize + NullmapSize(Buf, Info.HeaderSize); if (At >= Buf.length) Ping++; // 空 payload = 保活 } } return { Ok, Ping }; } function LoadSessions() { const Labels = LABEL ? [LABEL] : fs .readdirSync(PACKET_DIR, { withFileTypes: true }) .filter((D) => D.isDirectory()) .map((D) => D.name); const Sessions = []; for (const L of Labels) { const Dir = path.join(PACKET_DIR, L); if (!fs.existsSync(Dir)) continue; for (const D of fs.readdirSync(Dir, { withFileTypes: true })) { if (!D.isDirectory() || !D.name.startsWith('session_')) continue; const SessionDir = path.join(Dir, D.name); const Messages = { c2s: [], s2c: [] }; for (const F of fs.readdirSync(SessionDir).sort()) { const M = /^(c2s|s2c)_\d+\.bin$/.exec(F); if (M) Messages[M[1]].push(fs.readFileSync(path.join(SessionDir, F))); } Sessions.push({ Name: L + '/' + D.name, Messages }); } } return Sessions; } const Sessions = LoadSessions().filter((S) => !Arg('session', '') || S.Name.indexOf(Arg('session', '')) >= 0); console.log('抓包目录: ' + PACKET_DIR + ' 会话数: ' + Sessions.length); // 全量命令统计(只统计密钥完全自洽「解出来的」会话方向,避免脏数据污染) const Tally = { c2s: {}, s2c: {} }; const Keys = {}; function CountMessage(Direction, Buf) { const Info = PeekFrame(Buf); if (!Info || Info.TotalSize !== Buf.length) { Tally[Direction].BAD = (Tally[Direction].BAD || 0) + 1; return; } let Body = Buf.slice(Info.HeaderSize); if (Info.Compressed) { // 帧头 0x40 位 = 帧体被 zlib 压缩(客户端用 fflate 的 unzlibSync) Tally[Direction].COMPRESSED = (Tally[Direction].COMPRESSED || 0) + 1; try { Body = zlib.inflateSync(Body); Tally[Direction].UNZIP_OK = (Tally[Direction].UNZIP_OK || 0) + 1; } catch (Err) { Tally[Direction].UNZIP_FAIL = (Tally[Direction].UNZIP_FAIL || 0) + 1; return; } } const At = NullmapSize(Body, 0); if (At >= Body.length) { Tally[Direction].PING = (Tally[Direction].PING || 0) + 1; return; } const Id = Body[At]; const E = (Tally[Direction][Id] = Tally[Direction][Id] || { Count: 0, Bytes: 0, Sample: Body.slice(At).slice(0, 24) }); E.Count++; E.Bytes += Body.length - At; } for (const Session of Sessions) { for (const Direction of ['c2s', 's2c']) { const All = Session.Messages[Direction]; if (!All.length) continue; const Encoded = Direction === 'c2s'; // 客户端编码用 i7m_1(^87) const Total = All.reduce((A, B) => A + B.length, 0); // 密码可能在会话中途才启用,故同时穷举「起始消息下标 Start」: // Start 之前的消息保持明文,Start 起的消息用新密钥状态解密 const Results = []; for (let Start = 0; Start < All.length; Start++) { // 前缀必须是「明文且自洽」的帧,否则这个起始点不成立(防止把乱码当明文刷分) let Bad = 0; for (let I = 0; I < Start; I++) { const Info = PeekFrame(All[I]); if (!Info || Info.TotalSize !== All[I].length) Bad++; } if (Bad) continue; const Bufs = All.slice(Start); const Keys = FIXED_KEY !== null ? [Number(FIXED_KEY)] : Array.from({ length: 256 }, (_, I) => I); for (const K of Keys) { const S = Score(DecryptStream(Bufs, K, Encoded)); Results.push({ K, Start, Ok: S.Ok + Start, CipherOk: S.Ok, Ping: S.Ping, N: All.length }); } } Results.sort((A, B) => B.Ok - A.Ok || B.Ping - A.Ping || A.Start - B.Start); const Best = Results[0]; const RunnerUp = Results[1]; const Perfect = Best.Ok === All.length; const Verdict = Perfect ? '★ 全部自洽' : Best.Ok > (RunnerUp ? RunnerUp.Ok : 0) ? '领先' : '不确定'; Keys[Session.Name + ' ' + Direction] = { K: Best.K, Start: Best.Start, Perfect }; // 只有完全自洽的方向才计入总表 if (Perfect) { All.slice(0, Best.Start).forEach((Buf) => CountMessage(Direction, Buf)); DecryptStream(All.slice(Best.Start), Best.K, Encoded).forEach((Buf) => CountMessage(Direction, Buf)); } console.log( '\n' + Session.Name + ' ' + Direction + ' 消息=' + All.length + ' 字节=' + Total + ' → 最佳 明文' + Best.Start + '条 + K=' + Best.K + ' 加密' + Best.CipherOk + '条' + ' 合计自洽=' + Best.Ok + '/' + All.length + ' 保活=' + Best.Ping + ' 次优 ' + (RunnerUp ? '明文' + RunnerUp.Start + '+K=' + RunnerUp.K + '(' + RunnerUp.Ok + ')' : '-') + ' ' + Verdict, ); if (VERBOSE) { const Decoded = DecryptStream(All.slice(Best.Start), Best.K, Encoded); All.slice(0, Best.Start).forEach((Buf, I) => console.log(' #' + String(I).padStart(3) + ' [明文] ' + Buf.slice(0, 24).toString('hex'))); Decoded.forEach((Buf, I) => { const Info = PeekFrame(Buf); const At = Info && Info.TotalSize === Buf.length ? Info.HeaderSize + NullmapSize(Buf, Info.HeaderSize) : -1; const Id = At >= 0 && At < Buf.length ? Buf[At] : -1; const Ascii = Array.from(Buf.slice(0, 36)) .map((B) => (B >= 32 && B < 127 ? String.fromCharCode(B) : '.')) .join(''); console.log( ' #' + String(I + Best.Start).padStart(3) + ' ' + String(Buf.length).padStart(6) + 'B 长=' + (Info ? Info.TotalSize : '?') + ' 命令ID=' + String(Id).padStart(4) + ' ' + Buf.slice(0, 28).toString('hex') + ' |' + Ascii + '|', ); }); } } } // ============================================================ // 解密后的命令总表 // ============================================================ const CMD_NAME = { c2s: { 1: 'Hash Request', 3: 'Auth/EnterSpace(待证)', 32: 'ClientLog' }, s2c: { 0: '待证(带 int64 与时间戳)', 2: 'Hash Response', 32: 'Open Space' }, }; function Report(Direction) { console.log('\n===== 解密后命令总表(' + (Direction === 'c2s' ? '客户端 → 服务器' : '服务器 → 客户端') + ')====='); const Table = Tally[Direction]; const Ids = Object.keys(Table) .filter((K) => /^\d+$/.test(K)) .map(Number) .sort((A, B) => A - B); for (const Id of Ids) { const E = Table[Id]; const Name = (CMD_NAME[Direction] && CMD_NAME[Direction][Id]) || '未知'; console.log( ' ID=' + String(Id).padStart(4) + ' ' + Name.padEnd(24) + ' 次数=' + String(E.Count).padStart(5) + ' 负载=' + String(E.Bytes).padStart(8) + 'B 样本=' + E.Sample.toString('hex'), ); } if (Table.PING) console.log(' [保活 ping] ' + Table.PING + ' 条'); if (Table.COMPRESSED) console.log(' [压缩帧] ' + Table.COMPRESSED + ' 条(解压成功 ' + (Table.UNZIP_OK || 0) + ' / 失败 ' + (Table.UNZIP_FAIL || 0) + ')'); if (Table.BAD) console.log(' [解不开/残缺] ' + Table.BAD + ' 条'); } Report('c2s'); Report('s2c'); const Out = { Note: '由 decrypt_frames.js 暴力穷举单字节密钥后统计,仅包含完全自洽的会话方向', Keys, C2s: {}, S2c: {}, }; for (const Direction of ['c2s', 's2c']) { const Target = Direction === 'c2s' ? Out.C2s : Out.S2c; for (const K of Object.keys(Tally[Direction])) { const E = Tally[Direction][K]; Target[K] = E.Count !== undefined ? { Name: CMD_NAME[Direction][K] || '未知', Count: E.Count, Bytes: E.Bytes, Sample: E.Sample.toString('hex') } : E; } } const OutFile = path.join(PROJECT, 'names', 'commands_decrypted.json'); fs.writeFileSync(OutFile, JSON.stringify(Out, null, 2), 'utf8'); console.log('\n已写出: ' + OutFile);