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/(含数据库口令)不进库,可用上述工具重新生成。
This commit is contained in:
1 parent
90c377ad79
commit
b6f2cb71ee
65 files changed
+710187
-868
No files matched your search
+61
-37
@@ -12,11 +12,14 @@
|
||||
index.json # 模块索引
|
||||
names/
|
||||
evidence.json # 命名证据(见下)
|
||||
command_alias.json # 派生命名词典(读源码证实语义后起的别名,不是原始名;含 Classes/Methods/Fields/MethodIds/Spaces)
|
||||
rename_report.json # apply_names.js 的产出报告(含 Aliases 段)
|
||||
tools/
|
||||
extract.js # bundle → src/
|
||||
mine_evidence.js # src/ → names/evidence.json
|
||||
(待建) src_named/ # 应用重命名后的源码
|
||||
(待建) tools/apply_names.js / build_bundle.js
|
||||
apply_names.js # src/ → src_named/(证据命名;加 --aliases 时附带回填 command_alias.json 的别名)
|
||||
src_named/ # 应用重命名后的源码(126 个模块,三道校验:node --check / 字符串集合一致 / 标识符计数)
|
||||
(待建) tools/build_bundle.js
|
||||
```
|
||||
|
||||
原始 bundle 不复制进本工程,默认引用 `../main.6469b966.js`(可用参数覆盖)。
|
||||
@@ -152,52 +155,73 @@ getter(`v("codec_名字")` 的报错串就是字段名)/ `q7g`(slot →
|
||||
**上行字段没有可读名**:命令类不像模型 codec 那样保留 `codec_<名字>` getter,555 个上行字段在模块内
|
||||
没有任何属性访问点,压缩后原可读名已不可恢复,只能人工标注或靠调用点语义推断。
|
||||
|
||||
### 离线模拟(fake 模式)
|
||||
### 服务器:中继(分析)与真服务器(`server/`)
|
||||
|
||||
```bash
|
||||
cd tools
|
||||
node wss_server.js --mode relay # 中继:客户端 → 本地 → 真实服务器(抓包 + 实时解析)
|
||||
node wss_server.js --mode fake # 离线:自己回 Hash 握手 + Open Space,按抓包回放
|
||||
node wss_server.js --mode fake --replay-scale 0.5 --replay-gap-max 200
|
||||
node wss_server.js # 中继:客户端 → 本地 → 真实服务器(抓包 + 实时解析)
|
||||
node server/index.js --port 9091 # 真服务器(M1):自己握手 / 自己管空间 / 把未实现命令记全
|
||||
node server/selftest.js # MakeWriter 往返自检(拿真实抓包逐字节比对)
|
||||
node decode_space_frames.js --dir ../packets/plain/space-100045720 --fields # 事后逐字段解一包
|
||||
node pair_flow.js # 重新生成请求→应答配对表
|
||||
node ingest.js --label mine-01 # 把一次抓包入库到 packets/plain/
|
||||
node pair_flow.js # 重新生成请求→应答配对表
|
||||
node ingest.js --label mine-01 # 把一次抓包入库到 packets/plain/
|
||||
```
|
||||
|
||||
回放分两类:**主动推送**按抓包时间线(`timeline_s2c.txt`)铺开;**应答**挂在 `Watchers` 上等对应 c2s 到达。
|
||||
应答签名(必须与 `pair_flow.js` 的 `Signature` 一致,改一边就要改另一边):
|
||||
**回放(fake 模式)方案已废弃**(2026-09-16 实测结论)。原因:客户端一次会话是「大厅 + 6 个空间轮流连」
|
||||
的链(`mine-01` 的 `session_001…007`:大厅 → 100045260 → 920007382011 → 100119080 → 100072911 →
|
||||
100045720 → 车库 100073070),导航全部由大厅那几帧 `Open Space` 驱动,且每个连接都会往客户端的
|
||||
**全局注册表**(gameType / model / resource / space)里塞数据。只回放单条连接必然缺状态,实测表现为
|
||||
进车库后抛 `Class not found exception class=-2147482689`(`src/module_45686.js:117` 的 `ft()` 查
|
||||
`gameTypeRegistry` 落空),弹错后页面自动刷新,死循环。**空间层是问答式的,只能真服务器。**
|
||||
|
||||
- 上行空间帧 → `S:<methodId>`(源码常量,跨会话稳定,**能精确到某条具体命令**)
|
||||
- 其余 → `<命令 id>:<负载前 4 字节 hex>`
|
||||
`wss_server.js` 只保留 relay(协议分析);真服务器在 `tools/server/`:
|
||||
|
||||
于是"客户端点商店/改名/切频道"这类上行都能触发它自己的那组下行,等不到触发的仍按原时间线兜底。
|
||||
|
||||
### 容器(无 codec 的记录)现状
|
||||
|
||||
容器的模型恒为 `_t`(`src/module_45686.js:94`),codec 是 `lt`(同文件 483-494),只有一个字段 `idCodec`(i64):
|
||||
读 i64,`0` → 值是紧跟其后的那个 i64;否则按 modelRegister(`module_45686` 的 `at.k1we()`)里该 id 的 codec 就地展开。
|
||||
|
||||
容器负载 = `[头部][内层数据]`,头部实测形状是**模式表**:
|
||||
|
||||
```
|
||||
[i32 a][i32 b][i64 objId][i32 N][N × i64 modelId] 共 20 + 8N 字节
|
||||
```
|
||||
|
||||
N 个 modelId 就是"容器里出现哪些模型",语义完全对得上:
|
||||
|
||||
| 实测容器 | 模式表 |
|
||||
| 文件 | 职责 |
|
||||
|---|---|
|
||||
| `space-100045260/s2c_0000`(30 个) | CaptchaCC / RegistrationModelCC / EntranceResourcesCC / ExternalEntranceCC / ServerHaltEntranceCC … (登录+大厅初始化批次) |
|
||||
| `space-100045720/s2c_0000`(5 个) | _t / Kz / ChatCC / ClanChatCC / NewsShowingCC(聊天批次) |
|
||||
| `space-100072911`(2 个) | Jj / ShopCategoryCC(商店) |
|
||||
| `link.js` | 帧边界 / nullmap / 明文协商(握手回帧里把「启用链路加密」置 0,整条链路明文) |
|
||||
| `entry.js` | Hash Request·Response / EnterSpace / Open Space / ClientLog |
|
||||
| `codec.js` | `MakeWriter`(写包)+ `ResolveFields`(按 methodId 取字段表,服务端与自检同一口径) |
|
||||
| `dispatch.js` | 空间帧识别(两道门)→ 逐条记录解码 → 落地日志 → 记「未实现」 |
|
||||
| `world.js` | 空间实例表 / 空间分类 / 未实现命令清单 |
|
||||
| `index.js` | 启动、连接生命周期、日志落盘(`server-logs/<label>/session_00N/`) |
|
||||
| `selftest.js` | MakeWriter 往返自检 |
|
||||
|
||||
`decode_space_frames.js` 会用这个模式表约束内层扫描(只认表里的模型、且按表里的先后顺序出现),
|
||||
内层记录"吃满边界"的比例从 60% 提到 **70%**,假命中基本清掉。
|
||||
M1 现状:入口层全量(握手、鉴权、ClientLog、保活);空间层**只记录、不伪造**,一个字节都不回放。
|
||||
未实现清单就是 M2 的施工图。M2 要做的是按 `src/module_29554.js` 的 modelService 协议
|
||||
(`_loadDependencies` / `_loadObjectsData` / `_setModelCommandAlias` / `_unloadObjects`)与
|
||||
`src/module_45686.js` 的容器三编码(`lt`/`pt`/`zt`)把四张注册表填起来,让客户端进车库不报错。
|
||||
|
||||
**写包能力验收**(`node server/selftest.js`:13621 个抓包文件 / 1283 个空间帧 / 1206 条记录):
|
||||
逐字节一致 **1108 条(92%)**、不一致 9 条、解码中断 89 条、写侧不支持 0 条。
|
||||
残留的 9 + 89 条全部落在 `ChatCC` / `ClanNotifierData` 两个命令上(字段表漂移),不是写包逻辑问题。
|
||||
|
||||
### 容器(无 codec 的记录)—— 三个布局已闭合
|
||||
|
||||
容器命令没有普通 codec(客户端只读不写:`src/module_45686.js` 里 `lt.o7g` 是空实现、`pt.o7g`/`zt.o7g` 直接
|
||||
`throw`),它的负载由三个「容器 codec」描述。**三个布局已逐字节标定完成**,实现在 `tools/server/container.js`
|
||||
(读写都有,写侧由读侧反推,用真实抓包往返验证):
|
||||
|
||||
```
|
||||
lt : [i64 id] id == 0 → 值 = 紧跟的那个 i64;否则按 modelRegister[id] 的 codec 就地展开
|
||||
pt : [i32 C][C × (i64 objId, i64 gameTypeId)][i32 D][D × lt项]
|
||||
zt : [i32 X][i32 C][C × { i64 ObjId, i32 N, N × i64 ModelId }] ← 模型归属表(原「模式表」)
|
||||
[i32 D][D × { i64 R, i16 E, i16 U, i64 S, u8 O, varint Skip, Skip × i64 }]
|
||||
```
|
||||
|
||||
- 宽度依据:`zt` 项的字段顺序来自源码 `$t()`(`q2u, o2u, o2u, q2u, bool` + `zt` 的 `n2u` 跳过数)。
|
||||
实测每项恰好 **22 字节** ⇒ `8 + 2w + 8 + 1 + s = 22` ⇒ `o2u` = **i16**、`n2u` = 1 字节(按 varint 读)。
|
||||
- 标定证据:`space-100045260/s2c_0000.bin`(915B,解压 1799B)用 `zt` 布局 **1782/1782 字节精确吃满**。
|
||||
- 计数一律 **i32(4 字节)大端**,不是 varint(这一点此前被误判过)。
|
||||
- 模型归属表里的 N 个 modelId 语义与旧结论一致,例如
|
||||
`space-100045260`(30 个)= CaptchaCC / RegistrationModelCC / EntranceResourcesCC …(登录+大厅初始化批次)、
|
||||
`space-100045720`(5 个)= _t / Kz / ChatCC / ClanChatCC / NewsShowingCC(聊天批次)、
|
||||
`space-100072911`(2 个)= Jj / ShopCategoryCC(商店)。
|
||||
- **布局判定以「哪个布局恰好吃到帧尾」为准**,不要信 `names/method_table.json` 的 `Codec` 列 ——
|
||||
实测它把 `_t` 那类行标成 `lt`,而真实负载是 `zt` 形状。
|
||||
- 验收:`node server/selftest.js` 里容器帧 **11/11 逐字节一致**(10 个 `zt` + 1 个 `pt`)。
|
||||
仍有 2800 余个帧的 methodId 不在普通 codec / 上行表里且三布局都判不出——那些是**尚未拿到 codec 的其它命令**,
|
||||
不是容器,属于后续要补的表。
|
||||
|
||||
**尚未闭合**:头部前 12 字节(`a`/`b`/`objId`)与模型负载之间还没对齐 —— `_t` 的 codec 读到的首个 i64 实测是 `1`,
|
||||
而 modelRegister 里没有 id=1 的条目,说明 methodId 与模型负载之间还有一小段(可能是命令 codec 自己写的 tag/版本);
|
||||
大帧(如 41KB 那个容器)连模式表都认不出,头部形状可能还随场景变化。要彻底闭合需要拿到容器命令自身的 codec,
|
||||
而不是 `_t` 的模型 codec。
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user