修复 DLL 自校验 CRC 算法并加固槽位定位(频道无法连接问题)

根因:重写的 patch_dll_crc.py 用 binascii.crc32(data, 0xFFFFFFFF) 计算 CRC,
该参数是"末值异或之后的运行值",再喂回去等于重复施加了条件化,得到的是
CRC32 的另一个变体,与 90CN.cpp 里 Crc32Update 的实现不一致。结果是补丁写
入的 expected 永远校验不过,客户端在 VerifySelfIntegrity 失败后安装流程中断,
transport 钩子(codec/decode/route/dproto)全部没装上,表现为"无法连接频道"。

改动:
- tools/patch_dll_crc.py
  * CRC 改为标准 zlib.crc32(init 默认 0),与 C 端 Crc32Update 完全一致;
    已用旧版本 DLL 反向验证(10-20、11-03、10-28 三份的存储值均能复算一致)
  * 槽位改为按 PE 段表定位,只接受 .data/.rdata 内的候选,拒绝把补丁写进
    .text 机器码(此前曾命中 cmp ..., 0C0DEC0DEh 的立即数导致访问违例)
  * 新增 info/verify 子命令,patch 后自动回读校验
- 90CN.cpp
  * SelfCrcSlot 三个字段加 volatile,避免 /O2 把 offset/expected 折叠成常量
  * VerifySelfIntegrity 增加分步诊断日志,便于定位安装线程中断点
- client_build_id.go
  * 指纹重绑为本次构建产物:1215488 / C7BE377C…8247071A

验证:客户端日志 self crc check computed==expected==0x80AFC116 ok=1,
transport DLL installed codec=1 decode=1 route=1 dproto=1,
channel connect result=1,first-login watchdog server_seen=1 client_seen=1。
This commit is contained in:
hanfuit committed 2026-11-05 11:37:10 +08:00
1 parent 2ca06e113a
commit 6e570fe820
3 files changed
+162 -43

No files matched your search

+14 -3
View File
@@ -13169,9 +13169,11 @@ static bool VerifyBoundMoviePresent()
// expected = 整个文件的 CRC32(计算时跳过 [offset, offset+4),避免自指)
// 运行时重新读盘、用同样规则重算并比对;任何字节被改动 → 不符 → 客户端无法启动。
static struct SelfCrcSlot {
unsigned int magic; // 固定 0xC0DEC0DE
unsigned int offset; // expected 字段的文件偏移(脚本写入)
unsigned int expected; // 文件 CRC32(脚本写入)
// volatile:这三个字段只由构建后脚本改文件字节,代码里从不写。
// 不加 volatile 时 /O2 会把 offset/expected 当常量折叠成 0,补丁写对了也读不到。
volatile unsigned int magic; // 固定 0xC0DEC0DE
volatile unsigned int offset; // expected 字段的文件偏移(脚本写入)
volatile unsigned int expected; // 文件 CRC32(脚本写入)
} g_selfCrcSlot = { 0xC0DEC0DEu, 0u, 0u };
static unsigned int Crc32Update(unsigned int crc, const unsigned char* data, size_t length)
@@ -13189,6 +13191,8 @@ static unsigned int Crc32Update(unsigned int crc, const unsigned char* data, siz
static bool VerifySelfIntegrity()
{
wchar_t path[MAX_PATH] = {};
// 崩溃定位断点:worker 在 TCLS 之后踩 AV 时靠这些行判断停在哪一步。
LogLine("self crc step=enter slot=%p", (void*)&g_selfCrcSlot);
// 用数据变量地址解析自身模块;函数地址在某些构建/加载方式下解析不到(实测 err=126)。
HMODULE selfModule = nullptr;
if (!GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
@@ -13197,10 +13201,12 @@ static bool VerifySelfIntegrity()
LogLine("self crc diag: GetModuleHandleEx failed err=%u", GetLastError());
return false;
}
LogLine("self crc step=module module=0x%p", (void*)selfModule);
if (!GetModuleFileNameW(selfModule, path, MAX_PATH)) {
LogLine("self crc diag: GetModuleFileName failed err=%u", GetLastError());
return false;
}
LogLine("self crc step=path");
FILE* file = nullptr;
if (_wfopen_s(&file, path, L"rb") != 0 || !file) {
LogLine("self crc diag: open failed");
@@ -13209,6 +13215,7 @@ static bool VerifySelfIntegrity()
fseek(file, 0, SEEK_END);
const long size = ftell(file);
fseek(file, 0, SEEK_SET);
LogLine("self crc step=size size=%ld", size);
if (size < 4096) {
LogLine("self crc diag: file too small size=%ld", size);
fclose(file);
@@ -13221,6 +13228,7 @@ static bool VerifySelfIntegrity()
}
const size_t read = fread(image, 1, static_cast<size_t>(size), file);
fclose(file);
LogLine("self crc step=read read=%u image=%p", (unsigned int)read, (void*)image);
if (read != static_cast<size_t>(size)) {
free(image);
return false;
@@ -13228,6 +13236,7 @@ static bool VerifySelfIntegrity()
const unsigned int slotOffset = g_selfCrcSlot.offset;
const unsigned int expected = g_selfCrcSlot.expected;
LogLine("self crc step=slot offset=0x%X expected=0x%08X", slotOffset, expected);
bool ok = false;
if (g_selfCrcSlot.magic != 0xC0DEC0DEu || slotOffset < 16 ||
static_cast<size_t>(slotOffset) + 4 > static_cast<size_t>(size)) {
@@ -14995,7 +15004,9 @@ DWORD WINAPI InstallTransportWorkerInner(void*)
TerminateProcess(GetCurrentProcess(), 1);
}
// 影片绑定校验:文件缺失/损坏直接终止,客户端无法启动。
LogLine("self crc verified, entering movie binding check");
EnforceBoundMovieOrExit();
LogLine("movie binding check passed");
// 攻坚队创建入口默认写死 owner 0x76(RaidCreateWindow),把该立即数改成
// 0x7A 后打开原生 RaidSelectSingleModeWindow(团队/单人选择),单人按钮
// 再把 mode 0 传回 0x76 的创建窗口。窗口与按钮回调全部是客户端原生实现。