Files
TankIC/3DTank/tools/mine_evidence.js
T
TermiNexus b6f2cb71ee feat(3dtank): 协议逆向工程与自研服务器入库(工具链 + 协议表 + 空间回放 + MySQL)
工具链(3DTank/tools/):extract(模块提取)、mine_evidence/apply_names(命名证据与别名回填)、extract_codec_fields(字段表 + 全库槽位类型 + PrimClassWire/ResourceClasses)、mine_downlink(下行字段表)、extract_space_init/rebase_space_init/audit_space_init(空间模板与记录切分审计)、extract_space_session(把真实会话切成按上行触发的回放模板)、pair_flow(请求→应答配对)、probe_containers、ingest/decrypt_frames(抓包入库与解密)、serve/fetch_assets(本地站点镜像)、import_db(抓包对象仓库导入 MySQL)。

服务器(3DTank/tools/server/):link/entry/codec/container/dispatch/world/space/semantic(协议层与 lt/pt/zt 三个容器布局)、session(会话回放,顺序推进)、db + schema.sql(MySQL 连接层与对象仓库表结构)、store/responder(查库组帧与定向应答;接管范围默认只开 ba.lcr)。

同时修 pair_flow 的时间轴偏移:timeline_c2s 与 timeline_s2c 的 0 点相差 144~4453ms,改前配对整体偏约 2 秒(实测 space-100073070 偏 2084ms),改后 22.4% 的触发者被纠正。

src/、src_named/、packets/、3DTank/local/(含数据库口令)不进库,可用上述工具重新生成。
2026-09-17 20:00:41 +08:00

133 lines
5.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// mine_evidence.js - 从混淆代码里挖掘"原始命名证据"
// 用法: node mine_evidence.js [src目录] [输出json]
//
// 依据(编译产物里保留的线索):
// 1) 懒加载组件 getter : var x = t.<字段>; if (null != x) return x; vj("<组件名>")
// 2) 服务定位器 : t.<字段>.e2b(t, ct("<服务名>", 1, ...)) / x$("<服务名>", ...)
// 3) 闭包原名 : fn.callableName = "<原名>"
// 4) 数据类 toString : Q(类).toString = function () { return "<类名>(" + ... }
// 5) 枚举构造 : E = new 枚举类("<成员名>", 0)
// 6) 字符串常量频次(辅助命名)
//
// 结构说明:**类名/局部名都是模块内作用域**,因此按模块分域存放;
// 属性名(字段)是全局共享字符串,故全局聚合。
const fs = require('fs');
const path = require('path');
const PROJECT = path.resolve(__dirname, '..');
const argv = process.argv.slice(2);
const SRC_DIR = path.resolve(argv[0] || path.join(PROJECT, 'src'));
const OUT = path.resolve(argv[1] || path.join(PROJECT, 'names', 'evidence.json'));
function Push(Target, Key, Value) {
if (Target instanceof globalThis.Map) {
if (!Target.has(Key)) Target.set(Key, []);
const List = Target.get(Key);
if (List.indexOf(Value) < 0) List.push(Value);
return;
}
// 普通对象:Key → [Value, ...]
if (!Target[Key]) Target[Key] = [];
if (Target[Key].indexOf(Value) < 0) Target[Key].push(Value);
}
function Run() {
const Files = fs.readdirSync(SRC_DIR).filter((F) => /^module_\d+\.js$/.test(F)).sort();
const ByModule = {}; // 模块ID → 各类证据
const FieldsGlobal = new Map(); // 字段 → 组件/服务名(全局)
const Services = new Set();
const Strings = new Map();
const Stats = { modules: 0, fields: 0, classNames: 0, enumMembers: 0, closures: 0, exports: 0 };
for (const F of Files) {
const Text = fs.readFileSync(path.join(SRC_DIR, F), 'utf8');
const Id = (F.match(/module_(\d+)/) || [])[1];
const Mod = { classNames: {}, closures: {}, enums: {}, exports: {}, componentGetters: {} };
let M;
// 1) 懒加载组件 getter(容忍换行/空格)
const GetterRe = /var\s+([A-Za-z_$][\w$]*)\s*=\s*([A-Za-z_$][\w$]*)\.([A-Za-z_$][\w$]*)\s*;[\s\S]{0,40}?if\s*\(\s*null\s*!=\s*\1\s*\)\s*return\s+\1\s*;[\s\S]{0,40}?(?:vj|Ys|rq)\s*\(\s*"([^"]+)"\s*\)/g;
while ((M = GetterRe.exec(Text)) !== null) {
Mod.componentGetters[M[3]] = M[4];
Push(FieldsGlobal, M[3], M[4]);
}
// 2) 服务定位器 ct("name", 1, ...) / x$("name", ...)
for (M of Text.matchAll(/\b(?:ct|x\$|E\$|ct)\s*\(\s*"([^"]+)"\s*,\s*\d+/g)) Services.add(M[1]);
for (M of Text.matchAll(/\.e2b\s*\([^,]+,\s*(?:ct|x\$)\s*\(\s*"([^"]+)"/g)) Services.add(M[1]);
// 3) callableName(模块内作用域)
for (M of Text.matchAll(/([A-Za-z_$][\w$]*)\.callableName\s*=\s*"([^"]+)"/g)) Push(Mod.closures, M[1], M[2]);
for (M of Text.matchAll(/([A-Za-z_$][\w$]*)\(\s*([A-Za-z_$][\w$]*)\s*\)\.toString\s*=\s*function\s*\(\s*\)\s*\{\s*return\s+"([A-Za-z_$][\w$]*)\(/g)) {
Mod.classNames[M[2]] = M[3];
}
// 5) 枚举: X = new Cls("Member", 数字)
for (M of Text.matchAll(/([A-Za-z_$][\w$]*)\s*=\s*new\s+([A-Za-z_$][\w$]*)\(\s*"([A-Za-z_$][\w$ -]*)"\s*,\s*(-?\d+)\s*\)/g)) {
if (!Mod.enums[M[2]]) Mod.enums[M[2]] = [];
Mod.enums[M[2]].push({ field: M[1], member: M[3], value: Number(M[4]) });
}
// 6) 字符串常量
for (M of Text.matchAll(/"([^"\\\n]{2,60})"/g)) {
const V = M[1];
if (/^[0-9a-f]{8,}$/i.test(V) || /^[A-Za-z0-9+/=]{40,}$/.test(V)) continue;
Strings.set(V, (Strings.get(V) || 0) + 1);
}
// 7) 导出表 (t.$_$.xx = LocalName)
for (M of Text.matchAll(/t\.\$_\$\.([A-Za-z_$][\w$]*)\s*=\s*([A-Za-z_$][\w$]*)/g)) Mod.exports[M[1]] = M[2];
if (Object.keys(Mod.classNames).length) Stats.classNames += Object.keys(Mod.classNames).length;
for (const K of Object.keys(Mod.enums)) Stats.enumMembers += Mod.enums[K].length;
for (const K of Object.keys(Mod.closures)) Stats.closures += Mod.closures[K].length;
Stats.exports += Object.keys(Mod.exports).length;
Stats.fields += Object.keys(Mod.componentGetters).length;
Stats.modules++;
// 只保留有内容的模块,减小体积
if (
Object.keys(Mod.classNames).length || Object.keys(Mod.enums).length ||
Object.keys(Mod.closures).length || Object.keys(Mod.exports).length ||
Object.keys(Mod.componentGetters).length
) {
ByModule[Id] = Mod;
}
}
const Result = {
bundle: 'main.6469b966.js',
generatedAt: new Date().toISOString(),
stats: Stats,
byModule: ByModule,
fieldsGlobal: Object.fromEntries([...FieldsGlobal].sort()),
services: [...Services].sort(),
strings: Object.fromEntries([...Strings].sort((A, B) => B[1] - A[1])),
};
fs.mkdirSync(path.dirname(OUT), { recursive: true });
fs.writeFileSync(OUT, JSON.stringify(Result, null, 1), 'utf8');
console.log('=== 证据统计 ===');
for (const [K, V] of Object.entries(Stats)) console.log(' ' + K + ': ' + V);
console.log(' services: ' + Services.size + ' strings: ' + Strings.size);
console.log('--- 字段 → 组件名(前 15)---');
let C = 0;
for (const [K, V] of [...FieldsGlobal].sort()) {
if (C++ >= 15) break;
console.log(' ' + K + ' -> ' + V.join('/'));
}
console.log('--- 数据类名示例(模块 87072)---');
const M87072 = ByModule['87072'] || {};
C = 0;
for (const [K, V] of Object.entries(M87072.classNames || {})) {
if (C++ >= 10) break;
console.log(' ' + K + ' = ' + V);
}
console.log('--- 服务名(前 12)---');
console.log(' ' + [...Services].sort().slice(0, 12).join(', '));
console.log('输出: ' + OUT);
}
Run();