docs:补上 90CN.dll 自校验 CRC 工具的文档说明
- 新增 docs/客户端补丁与CRC校验.md:SelfCrcSlot 结构、CRC32 算法与种子坑、 槽位为什么只在 .data/.rdata 里找、info/verify/patch 用法与退出码、 build_90cn.ps1 集成、与「服务端 DLL 指纹」的区别、六类故障排查 - 新增 client-patch/tools/README.md:脚本清单与常用命令速查 - docs/README.md:文档索引与上手路径补一条 - 根 README:关键设计说明补「客户端自校验(CRC32)」小节,文档索引补一行 - 90CN.cpp:清掉过期的重复注释(旧的 patch_selfcrc.py / .text 段说法),与实现对齐
This commit is contained in:
1 parent
9bdaf28683
commit
da502b3524
5 files changed
+174
-8
No files matched your search
@@ -161,6 +161,16 @@ pvfexport -pvf Script.pvf -out dnf_items.json -compact
|
|||||||
|
|
||||||
服务端实现 `/local/dnf-gm/auction/*`:免押金上架(`Deposit=0`)、自动补足上架(背包不足时先发差额)、列背包 / 删除背包行(含穿戴装备)。
|
服务端实现 `/local/dnf-gm/auction/*`:免押金上架(`Deposit=0`)、自动补足上架(背包不足时先发差额)、列背包 / 删除背包行(含穿戴装备)。
|
||||||
|
|
||||||
|
### 客户端自校验(CRC32)
|
||||||
|
|
||||||
|
`90CN.dll` 里有一个 `SelfCrcSlot` 结构(`magic=0xC0DEC0DE` + `offset` + `expected`),构建后由 `client-patch/tools/patch_dll_crc.py` 把「整个文件的 CRC32」与槽位偏移写进去;运行时读盘重算并比对,不符就弹框退出——所以「patch 掉校验分支」这条路是堵死的。
|
||||||
|
|
||||||
|
- 编译脚本 `build_90cn.ps1` 第三步会自动补 CRC,`-SkipCrc` 出来的 DLL 不要发布
|
||||||
|
- 定位槽位只认 `.data` / `.rdata`,因为 `0xC0DEC0DE` 在 `.text` 里也有一份(MSVC 的比较立即数),扫错会写坏机器码,表现为读槽位后立刻崩
|
||||||
|
- 改完 DLL 除了补 CRC,**还要重编服务端**:`DNF90Server.exe` 编译期绑定了 DLL 的 SHA-256(`boundDllSHA256`),指纹不配套会在登录阶段被踢
|
||||||
|
|
||||||
|
详见 [docs/客户端补丁与CRC校验.md](docs/客户端补丁与CRC校验.md)。
|
||||||
|
|
||||||
## 十、常见坑
|
## 十、常见坑
|
||||||
|
|
||||||
| 现象 | 原因 | 处理 |
|
| 现象 | 原因 | 处理 |
|
||||||
@@ -180,6 +190,7 @@ pvfexport -pvf Script.pvf -out dnf_items.json -compact
|
|||||||
| [docs/开发文档.md](docs/开发文档.md) | 启动器 / GM 工具:环境、架构、API 契约、打包部署、常见问题 |
|
| [docs/开发文档.md](docs/开发文档.md) | 启动器 / GM 工具:环境、架构、API 契约、打包部署、常见问题 |
|
||||||
| [docs/服务端开发文档.md](docs/服务端开发文档.md) | 服务端架构、GM 接口契约、拍卖行与背包改动 |
|
| [docs/服务端开发文档.md](docs/服务端开发文档.md) | 服务端架构、GM 接口契约、拍卖行与背包改动 |
|
||||||
| [docs/PVF解析开发文档.md](docs/PVF解析开发文档.md) | pvfine 与 pvfexport、离线名称集成 |
|
| [docs/PVF解析开发文档.md](docs/PVF解析开发文档.md) | pvfine 与 pvfexport、离线名称集成 |
|
||||||
|
| [docs/客户端补丁与CRC校验.md](docs/客户端补丁与CRC校验.md) | 90CN.dll 自校验 CRC、`patch_dll_crc.py` 用法、与 DLL 指纹的区别 |
|
||||||
| `server/README.md`、`pvf/README.md`、`launcher/README.md`、`gmtool/README.md` | 各子项目说明 |
|
| `server/README.md`、`pvf/README.md`、`launcher/README.md`、`gmtool/README.md` | 各子项目说明 |
|
||||||
| [docs/release/](docs/release/) | 发布说明模板、网盘下载 `.url` 模板 |
|
| [docs/release/](docs/release/) | 发布说明模板、网盘下载 `.url` 模板 |
|
||||||
|
|
||||||
|
|||||||
@@ -13159,16 +13159,11 @@ static bool VerifyBoundMoviePresent()
|
|||||||
|
|
||||||
// ── 自校验(self-integrity-crc)────────────────────────────────────────
|
// ── 自校验(self-integrity-crc)────────────────────────────────────────
|
||||||
//
|
//
|
||||||
// 构建后由 patch_selfcrc.py 把自身 .text 段的 CRC32 写进 g_selfCrcExpected。
|
// 构建后由 tools/patch_dll_crc.py 写入两个字段:
|
||||||
// 运行时重新读取磁盘上的本 DLL,算 .text 段 CRC 并比对:
|
|
||||||
// 任何字节被改动(比如 patch 掉校验分支)→ CRC 不符 → 客户端无法启动。
|
|
||||||
// 占位初值必须是 0xC0DEC0DE,脚本靠它定位写入位置。
|
|
||||||
// ── 自校验(self-integrity-crc)────────────────────────────────────────
|
|
||||||
//
|
|
||||||
// 构建后由 patch_dll_crc.py 写入两个字段:
|
|
||||||
// offset = 本结构 expected 字段在文件中的绝对偏移
|
// offset = 本结构 expected 字段在文件中的绝对偏移
|
||||||
// expected = 整个文件的 CRC32(计算时跳过 [offset, offset+4),避免自指)
|
// expected = 整个文件的 CRC32(计算时跳过 [offset, offset+4),避免自指)
|
||||||
// 运行时重新读盘、用同样规则重算并比对;任何字节被改动 → 不符 → 客户端无法启动。
|
// 运行时重新读盘、用同样规则重算并比对;任何字节被改动 → 不符 → 客户端无法启动。
|
||||||
|
// 占位初值必须是 0xC0DEC0DE,脚本靠它定位写入位置(详见 docs/客户端补丁与CRC校验.md)。
|
||||||
static struct SelfCrcSlot {
|
static struct SelfCrcSlot {
|
||||||
// volatile:这三个字段只由构建后脚本改文件字节,代码里从不写。
|
// volatile:这三个字段只由构建后脚本改文件字节,代码里从不写。
|
||||||
// 不加 volatile 时 /O2 会把 offset/expected 当常量折叠成 0,补丁写对了也读不到。
|
// 不加 volatile 时 /O2 会把 offset/expected 当常量折叠成 0,补丁写对了也读不到。
|
||||||
@@ -14996,7 +14991,7 @@ DWORD WINAPI InstallTransportWorkerInner(void*)
|
|||||||
bool tclsInstalled = InstallTclsCompatibility();
|
bool tclsInstalled = InstallTclsCompatibility();
|
||||||
LogLine("TCLS early bootstrap result=%d", tclsInstalled);
|
LogLine("TCLS early bootstrap result=%d", tclsInstalled);
|
||||||
|
|
||||||
// 自校验:本 DLL 的 .text 段 CRC 必须与构建时写入的值一致。
|
// 自校验:本 DLL 整个文件的 CRC32 必须与构建时写入的值一致。
|
||||||
if (!VerifySelfIntegrity()) {
|
if (!VerifySelfIntegrity()) {
|
||||||
LogLine("self crc check failed: terminating client");
|
LogLine("self crc check failed: terminating client");
|
||||||
MessageBoxW(nullptr,
|
MessageBoxW(nullptr,
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# client-patch/tools
|
||||||
|
|
||||||
|
编译与构建后处理脚本。完整说明见 [docs/客户端补丁与CRC校验.md](../../docs/客户端补丁与CRC校验.md)。
|
||||||
|
|
||||||
|
| 文件 | 用途 |
|
||||||
|
|------|------|
|
||||||
|
| `build_90cn.bat` | 双击入口,转调下面的 ps1 |
|
||||||
|
| `build_90cn.ps1` | MSBuild 编 `Release\|Win32` → 收集产物 → 调 `patch_dll_crc.py` 写自校验 CRC;`-SkipCrc` 可跳过(仅调编译用,别拿去发布) |
|
||||||
|
| `patch_dll_crc.py` | 补齐 / 校验 `90CN.dll` 自校验 CRC32 |
|
||||||
|
|
||||||
|
## 常用命令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 编 DLL 并自动补 CRC
|
||||||
|
powershell -NoProfile -ExecutionPolicy Bypass -File build_90cn.ps1
|
||||||
|
|
||||||
|
# 只处理已有的 DLL
|
||||||
|
python patch_dll_crc.py info <dll> # 看节表与槽位
|
||||||
|
python patch_dll_crc.py verify <dll> # 校验当前 CRC
|
||||||
|
python patch_dll_crc.py patch <dll> # 重写 CRC(-o 可输出到副本)
|
||||||
|
```
|
||||||
|
|
||||||
|
退出码:`0` 通过 / `1` 校验失败或找不到槽位 / `2` 用法错误。
|
||||||
|
|
||||||
|
> ⚠️ 改完 DLL 除了补 CRC,还要**重编服务端**——`DNF90Server.exe` 在编译期绑定了 DLL 的 SHA-256,指纹不配套会被拒绝连接。
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
| [开发文档.md](开发文档.md) | **GM 启动器 / 整合工具** 完整开发文档(环境搭建、架构、API 契约、打包部署、常见问题) |
|
| [开发文档.md](开发文档.md) | **GM 启动器 / 整合工具** 完整开发文档(环境搭建、架构、API 契约、打包部署、常见问题) |
|
||||||
| [服务端开发文档.md](服务端开发文档.md) | **服务端(Go)** 架构、GM 接口契约、拍卖行/背包改动、编译部署 |
|
| [服务端开发文档.md](服务端开发文档.md) | **服务端(Go)** 架构、GM 接口契约、拍卖行/背包改动、编译部署 |
|
||||||
| [PVF解析开发文档.md](PVF解析开发文档.md) | **PVF 解析** 套件(pvfine)、pvfexport 工具、离线名称显示集成 |
|
| [PVF解析开发文档.md](PVF解析开发文档.md) | **PVF 解析** 套件(pvfine)、pvfexport 工具、离线名称显示集成 |
|
||||||
|
| [客户端补丁与CRC校验.md](客户端补丁与CRC校验.md) | **客户端补丁(90CN.dll)** 自校验机制、`patch_dll_crc.py` 用法、与「服务端 DLL 指纹」的区别、排查手册 |
|
||||||
| [launcher-README.md](launcher-README.md) | 整合启动器 + GM 工具的原始 README |
|
| [launcher-README.md](launcher-README.md) | 整合启动器 + GM 工具的原始 README |
|
||||||
|
|
||||||
其余子项目的 README 分散在各自目录:
|
其余子项目的 README 分散在各自目录:
|
||||||
@@ -23,3 +24,4 @@
|
|||||||
2. **改 GM 工具(启动器)**:看 [开发文档.md](开发文档.md)。
|
2. **改 GM 工具(启动器)**:看 [开发文档.md](开发文档.md)。
|
||||||
3. **改服务端 GM 接口**:看 [服务端开发文档.md](服务端开发文档.md) 的「GM 接口契约」。
|
3. **改服务端 GM 接口**:看 [服务端开发文档.md](服务端开发文档.md) 的「GM 接口契约」。
|
||||||
4. **改 PVF 解析 / 物品名**:看 [PVF解析开发文档.md](PVF解析开发文档.md)。
|
4. **改 PVF 解析 / 物品名**:看 [PVF解析开发文档.md](PVF解析开发文档.md)。
|
||||||
|
5. **改客户端补丁 / 重编 DLL**:看 [客户端补丁与CRC校验.md](客户端补丁与CRC校验.md)——改完 DLL 要补 CRC,并且要重编服务端。
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
# 客户端补丁自校验(CRC32)与 patch_dll_crc.py
|
||||||
|
|
||||||
|
`client-patch/tools/patch_dll_crc.py` 是 `90CN.dll` 的**自校验补齐工具**。DLL 编译出来自带一个 CRC 槽位(初值全 0),必须由这个脚本在构建后把「整个文件的 CRC32 + 槽位偏移」写进去;不写,客户端启动时自校验直接失败。
|
||||||
|
|
||||||
|
> 一句话:**改完 DLL 必须跑 patch,跑完 CRC 变了必须重编服务端**(服务端绑的是 DLL 的 SHA-256,见第六节)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、它解决什么问题
|
||||||
|
|
||||||
|
90CN 补丁是外挂式 DLL,谁都能拿十六进制编辑器改两个字节。加了自校验之后:
|
||||||
|
|
||||||
|
- DLL 每次启动先读**磁盘上自己**的字节,重算 CRC32,与构建时写入的值比对
|
||||||
|
- 不一致 → 弹「客户端补丁文件已被修改,无法启动」→ `TerminateProcess`
|
||||||
|
- 因此「patch 掉校验分支」这条路走不通:改分支就等于改 CRC,CRC 对不上直接启动失败
|
||||||
|
|
||||||
|
代价是每次重编 DLL 都要补一次 CRC,这就是本工具的职责。
|
||||||
|
|
||||||
|
## 二、两端机制
|
||||||
|
|
||||||
|
### DLL 侧(`client-patch/90CN.cpp` → `SelfCrcSlot` / `VerifySelfIntegrity`)
|
||||||
|
|
||||||
|
```c
|
||||||
|
static struct SelfCrcSlot {
|
||||||
|
volatile unsigned int magic; // 固定 0xC0DEC0DE,脚本靠它定位槽位
|
||||||
|
volatile unsigned int offset; // expected 字段在文件中的绝对偏移(脚本写入)
|
||||||
|
volatile unsigned int expected; // 整个文件的 CRC32(脚本写入)
|
||||||
|
} g_selfCrcSlot = { 0xC0DEC0DEu, 0u, 0u };
|
||||||
|
```
|
||||||
|
|
||||||
|
三个要点,都踩过坑:
|
||||||
|
|
||||||
|
1. **必须 `volatile`**:这三个字段只由脚本改文件字节,C++ 代码里从不写。不加 `volatile`,`/O2` 会把 `offset`/`expected` 当常量折叠成 0,脚本写对了运行时也读不到。
|
||||||
|
2. **用数据地址解析自身模块**:`GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, &g_selfCrcSlot, ...)`。用函数地址在部分加载方式下解析不到(实测 `err=126`)。
|
||||||
|
3. **校验发生在窗口出来之前**:`VerifySelfIntegrity()` 在 `InstallTclsCompatibility()` 之后、影片哈希之前调用,失败即弹框并退出。
|
||||||
|
|
||||||
|
CRC 计算严格按下面这套,和脚本必须逐字节一致:
|
||||||
|
|
||||||
|
```c
|
||||||
|
unsigned int crc = 0xFFFFFFFFu;
|
||||||
|
crc = Crc32Update(crc, image, slotOffset); // 槽位之前的全部字节
|
||||||
|
crc = Crc32Update(crc, image + slotOffset + 4, size - slotOffset - 4); // 跳过 expected 那 4 字节
|
||||||
|
crc ^= 0xFFFFFFFFu;
|
||||||
|
```
|
||||||
|
|
||||||
|
即标准的反射 CRC-32(poly `0xEDB88320`、init `0xFFFFFFFF`、final XOR `0xFFFFFFFF`),等价于 `zlib.crc32()` 的**默认**用法。
|
||||||
|
|
||||||
|
### 脚本侧(`patch_dll_crc.py`)
|
||||||
|
|
||||||
|
| 动作 | 做什么 |
|
||||||
|
|------|--------|
|
||||||
|
| 定位槽位 | 解析 PE 节表,只在 `.data` / `.rdata` 里找 `0xC0DEC0DE`,可执行节里的命中一律拒绝 |
|
||||||
|
| 写 `offset` | `offset = 槽位文件偏移 + 8`(指向 `expected` 自己) |
|
||||||
|
| 写 `expected` | 先写 `offset` 再算 CRC —— 因为 `offset` 本身也在被哈希的范围内 |
|
||||||
|
| 复核 | 写完立刻跑一次 `verify`,退出码即结果 |
|
||||||
|
|
||||||
|
**为什么不直接扫第一个 `0xC0DEC0DE`**:MSVC 会把 `magic != 0xC0DEC0DEu` 这个比较编译成 `.text` 里的立即数,于是镜像里有多处命中。早期脚本扫到机器码就往上写,运行时表现为「读槽位后立刻访问冲突」。现在按节属性过滤,并且只接受 `offset` 为 0(新编译出来)或已自指(已 patch 过)的候选。
|
||||||
|
|
||||||
|
**CRC 种子坑(曾上线一版错误实现)**:不要给 `binascii.crc32(data, 0xFFFFFFFF)` 传种子。那个参数是**已经过 final XOR** 的运行中 CRC,再传进去等于把 conditioning 应用两次,算出来是另一个变体,客户端会拒绝自己的 DLL。正确写法是 `zlib.crc32(part1)` 再用返回值续算 `part2`。
|
||||||
|
|
||||||
|
## 三、命令行用法
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 看节表布局与槽位现状(不改文件)
|
||||||
|
python patch_dll_crc.py info Client/90CN.dll
|
||||||
|
|
||||||
|
# 只校验:当前文件与槽位里存的 CRC 是否一致
|
||||||
|
python patch_dll_crc.py verify Client/90CN.dll
|
||||||
|
|
||||||
|
# 补齐/重写 CRC(原地写)
|
||||||
|
python patch_dll_crc.py patch Client/90CN.dll
|
||||||
|
|
||||||
|
# 写到副本,原文件不动(发布前比对用)
|
||||||
|
python patch_dll_crc.py patch Client/90CN.dll -o build/90CN_patched.dll
|
||||||
|
```
|
||||||
|
|
||||||
|
输出示例(当前发布版 `90CN.dll` 的真实结果):
|
||||||
|
|
||||||
|
```
|
||||||
|
slot : file 0x0009B2AC in .data offset=0x0009B2B4 expected=0x9AFDA31F
|
||||||
|
crc32 : computed=0x9AFDA31F stored=0x9AFDA31F
|
||||||
|
OK 90CN.dll: self-integrity CRC verified
|
||||||
|
```
|
||||||
|
|
||||||
|
**退出码**:`0` 成功 / `1` 校验失败或找不到槽位 / `2` 用法错误(参数或文件不存在)。
|
||||||
|
|
||||||
|
## 四、构建集成
|
||||||
|
|
||||||
|
`client-patch/tools/build_90cn.ps1`(双击走 `build_90cn.bat`)三步走:
|
||||||
|
|
||||||
|
1. MSBuild 编 `Release|Win32` 出 `90CN.dll`
|
||||||
|
2. 收集产物
|
||||||
|
3. 调 `patch_dll_crc.py patch <dll>` 写入 CRC(找不到 Python 且没加 `-SkipCrc` 就直接抛错,不会静默产出未 patch 的 DLL)
|
||||||
|
|
||||||
|
`-SkipCrc` 只用于「先编出来看看能不能过编译」,**不要**拿这种 DLL 发布。
|
||||||
|
|
||||||
|
## 五、手动改过 DLL 之后
|
||||||
|
|
||||||
|
任何对 DLL 的字节级改动(换图标、改资源、再打一次补丁)都会让 CRC 失效,按顺序来:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python patch_dll_crc.py verify Client/90CN.dll # 1. 先确认现在是否已经是坏的
|
||||||
|
python patch_dll_crc.py patch Client/90CN.dll # 2. 重写 CRC
|
||||||
|
python patch_dll_crc.py verify Client/90CN.dll # 3. 复核
|
||||||
|
```
|
||||||
|
|
||||||
|
然后**必须重编服务端**,见下一节。
|
||||||
|
|
||||||
|
## 六、和「服务端 DLL 指纹」是两回事
|
||||||
|
|
||||||
|
| 机制 | 在哪 | 校验什么 | 失败表现 |
|
||||||
|
|------|------|----------|----------|
|
||||||
|
| 自校验 CRC32 | `90CN.dll` 内部 | 自己文件字节没被改 | 启动即弹框「客户端补丁文件已被修改」并退出 |
|
||||||
|
| DLL 指纹 SHA-256 | `DNF90Server.exe`(编译期 `boundDllSHA256`) | 客户端上报的 DLL 哈希是否等于服务端编进去的那一个 | 能进登录,但连接被拒 / 掉线 |
|
||||||
|
|
||||||
|
所以**换 DLL 必须重编服务端**,否则 CRC 自己是对了,指纹对不上照样连不上。发布包里 `Client\90CN.dll` 与 `Server\runtime\bin\DNF90Server.exe` 必须成对。
|
||||||
|
|
||||||
|
## 七、排查手册
|
||||||
|
|
||||||
|
| 现象 | 原因 | 处理 |
|
||||||
|
|------|------|------|
|
||||||
|
| 启动弹「客户端补丁文件已被修改」 | CRC 不符,或槽位根本没 patch(`magic`/`offset` 检查不过) | 跑 `patch`;看日志 `self crc check: computed=... expected=...` 两值 |
|
||||||
|
| 读槽位后立刻访问冲突(AV) | 早期脚本把 `0xC0DEC0DE` 当普通字符串扫,写进了 `.text` 机器码 | 用当前脚本重编重 patch;`info` 确认槽位在 `.data` |
|
||||||
|
| `verify` 报 `offset does not point at itself` | 文件被别的工具改写,或用了错误版本的脚本 | `patch` 重写后复核 |
|
||||||
|
| `0xC0DEC0DE only found in an executable section` | 说明槽位没编进数据段(被优化掉/常量折叠) | 检查 `volatile` 是否还在,重编 |
|
||||||
|
| 每台机器都「校验失败」但本地 `verify` 通过 | 分发过程中文件被改(签壳、压缩包截断、杀软修复) | 对比分发前后 SHA-256,禁用会改二进制的「修复」 |
|
||||||
|
| 校验通过但登录掉线 | DLL 指纹与 `DNF90Server.exe` 不配套 | 用同一份 DLL 重编服务端 |
|
||||||
|
|
||||||
|
日志前缀统一是 `self crc `(`step=` 为流程、`check:` 为结果、`diag:` 为失败原因),排查时直接 grep 这个前缀。
|
||||||
|
|
||||||
|
## 八、为什么不用数字签名
|
||||||
|
|
||||||
|
自签名证书要装根、要处理时间戳与过期;WinVerifyTrust 还要依赖系统服务。CRC32 不防「有动机的攻击者」——他们可以直接改期望值——它防的是**误改与低级篡改**,对单机模拟器这个场景够用,且零依赖、构建期一次写入。
|
||||||
Reference in new issue
Block a user