Files
90CNServer/docs/客户端补丁与CRC校验.md
T
hanfuit da502b3524 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 段说法),与实现对齐
2026-11-05 16:40:33 +08:00

7.5 KiB
Raw Blame History

客户端补丁自校验(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)

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 计算严格按下面这套,和脚本必须逐字节一致:

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。

三、命令行用法

# 看节表布局与槽位现状(不改文件)
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 失效,按顺序来:

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 不防「有动机的攻击者」——他们可以直接改期望值——它防的是误改与低级篡改,对单机模拟器这个场景够用,且零依赖、构建期一次写入。