From 2ca06e113a7627d026f3226f46ce2b5758f620cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=81=B5=E5=AE=9D=E4=BB=99=E5=B0=8A?= Date: Thu, 5 Nov 2026 10:41:47 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E5=AE=A2=E6=88=B7=E7=AB=AF?= =?UTF-8?q?=E5=AE=8C=E6=95=B4=E6=80=A7=E6=A0=A1=E9=AA=8C=EF=BC=9A=E8=A1=A5?= =?UTF-8?q?=E9=BD=90=20CRC=20=E5=90=8E=E5=A4=84=E7=90=86=E8=84=9A=E6=9C=AC?= =?UTF-8?q?=E5=B9=B6=E5=9B=9E=E7=BB=91=2090CN.dll=20=E6=8C=87=E7=BA=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题:上一提交把 boundDll 指纹改成 1230336/F6DDA534…,但配套的 90CN.dll 未交付到客户端目录,服务端启动即被 verifyClientArtifacts 拦下 (client dll: size mismatch: 1230848 != 1230336),日志表现为空壳。 本次改动: - 新增 client-patch/tools/patch_dll_crc.py,实现仓库中缺失的 DLL 自校验 槽位回填(magic 0xC0DEC0DE + offset + 全文件 CRC32,跳过自身 4 字节), 附带 verify 子命令可回读校验 - 新增 client-patch/tools/build_90cn.bat / .ps1,一键编译 + 打 CRC + 输出指纹 - client_build_id.go 指纹回绑到本次实际构建产物: 1215488 / 30E41FF583893E611E6975DF96246E1320B042F7B33A32503C987F211D1776F5 验证:重编 DNF90Server.exe 后裸跑,完整性校验通过,PVF 加载完成, dnfbridge 正常监听 :7001,game_ports=31,admin http 127.0.0.1:18111。 --- client-patch/tools/build_90cn.bat | 7 + client-patch/tools/build_90cn.ps1 | 96 +++++++++++ client-patch/tools/patch_dll_crc.py | 152 ++++++++++++++++++ .../services/dnfbridge/client_build_id.go | 4 +- 4 files changed, 257 insertions(+), 2 deletions(-) create mode 100644 client-patch/tools/build_90cn.bat create mode 100644 client-patch/tools/build_90cn.ps1 create mode 100644 client-patch/tools/patch_dll_crc.py diff --git a/client-patch/tools/build_90cn.bat b/client-patch/tools/build_90cn.bat new file mode 100644 index 0000000..6feba17 --- /dev/null +++ b/client-patch/tools/build_90cn.bat @@ -0,0 +1,7 @@ +@echo off +rem Double-click wrapper for build_90cn.ps1 (Release|Win32 + CRC patch). +setlocal +cd /d "%~dp0" +powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0build_90cn.ps1" %* +echo. +pause diff --git a/client-patch/tools/build_90cn.ps1 b/client-patch/tools/build_90cn.ps1 new file mode 100644 index 0000000..8b73cc3 --- /dev/null +++ b/client-patch/tools/build_90cn.ps1 @@ -0,0 +1,96 @@ +# 90CN.dll one-click build script. +# +# 1. locate MSBuild (Build Tools / VS 2019 / VS 2022) +# 2. build Release|Win32 from client-patch/90CN.vcxproj +# 3. write the self-integrity CRC32 into the fresh DLL (patch_dll_crc.py) +# 4. print the size + SHA-256 that client_build_id.go must carry +# +# Usage (from anywhere): +# powershell -ExecutionPolicy Bypass -File build_90cn.ps1 +# powershell -ExecutionPolicy Bypass -File build_90cn.ps1 -SkipCrc +# +# The output DLL lands in client-patch\Release\90CN.dll and is copied to +# ..\out\90CN.dll for convenience. + +param( + [switch]$SkipCrc, + [string]$Configuration = "Release", + [string]$Platform = "Win32" +) + +$ErrorActionPreference = "Stop" +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$projectDir = Split-Path -Parent $here +$vcxproj = Join-Path $projectDir "90CN.vcxproj" + +function Find-MSBuild { + $vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" + if (Test-Path $vswhere) { + $path = & $vswhere -latest -products * -requires Microsoft.Component.MSBuild ` + -find "MSBuild\**\Bin\MSBuild.exe" | Select-Object -First 1 + if ($path) { return $path } + } + $candidates = @( + "C:\VS\BuildTools\MSBuild\Current\Bin\MSBuild.exe", + "${env:ProgramFiles}\Microsoft Visual Studio\2022\BuildTools\MSBuild\Current\Bin\MSBuild.exe", + "${env:ProgramFiles}\Microsoft Visual Studio\2022\Community\MSBuild\Current\Bin\MSBuild.exe", + "${env:ProgramFiles(x86)}\Microsoft Visual Studio\2019\BuildTools\MSBuild\Current\Bin\MSBuild.exe", + "${env:ProgramFiles(x86)}\Microsoft Visual Studio\2019\Community\MSBuild\Current\Bin\MSBuild.exe" + ) + foreach ($c in $candidates) { if (Test-Path $c) { return $c } } + return $null +} + +if (-not (Test-Path $vcxproj)) { throw "vcxproj not found: $vcxproj" } + +$msbuild = Find-MSBuild +if (-not $msbuild) { + throw @" +MSBuild not found. + +Install the free "Build Tools for Visual Studio" and tick: + - 使用 C++ 的桌面开发 (Desktop development with C++) + - MSVC v142 - VS 2019 C++ x64/x86 生成工具 + - Windows 10 SDK + +Download: https://aka.ms/vs/17/release/vs_BuildTools.exe +"@ +} + +Write-Host "[1/4] MSBuild : $msbuild" +Write-Host "[2/4] building $Configuration|$Platform ..." + +& $msbuild $vcxproj /nologo /v:minimal /m ` + /p:Configuration=$Configuration /p:Platform=$Platform +if ($LASTEXITCODE -ne 0) { throw "build failed (exit $LASTEXITCODE)" } + +$dll = Join-Path $projectDir "$Configuration\90CN.dll" +if (-not (Test-Path $dll)) { throw "build succeeded but $dll is missing" } + +if (-not $SkipCrc) { + Write-Host "[3/4] patching self-integrity CRC ..." + $python = (Get-Command python -ErrorAction SilentlyContinue).Source + if (-not $python) { $python = (Get-Command py -ErrorAction SilentlyContinue).Source } + if (-not $python) { throw "python not found; run with -SkipCrc or install Python" } + & $python (Join-Path $here "patch_dll_crc.py") patch $dll + if ($LASTEXITCODE -ne 0) { throw "CRC patch/verify failed" } +} else { + Write-Host "[3/4] CRC patch skipped (-SkipCrc)" +} + +$outDir = Join-Path (Split-Path -Parent $projectDir) "out" +New-Item -ItemType Directory -Force -Path $outDir | Out-Null +Copy-Item $dll (Join-Path $outDir "90CN.dll") -Force + +$size = (Get-Item $dll).Length +$sha = (Get-FileHash $dll -Algorithm SHA256).Hash + +Write-Host "[4/4] done" +Write-Host "" +Write-Host " dll : $dll" +Write-Host " size : $size" +Write-Host " sha256: $sha" +Write-Host "" +Write-Host "Update server/internal/services/dnfbridge/client_build_id.go with:" +Write-Host " boundDllBytes = int64($size)" +Write-Host " boundDllSHA256 = `"$sha`"" diff --git a/client-patch/tools/patch_dll_crc.py b/client-patch/tools/patch_dll_crc.py new file mode 100644 index 0000000..0024499 --- /dev/null +++ b/client-patch/tools/patch_dll_crc.py @@ -0,0 +1,152 @@ +#!/usr/bin/env python3 +"""90CN.dll self-integrity CRC32 patcher (reimplementation of patch_dll_crc.py). + +The client patch embeds this struct (see client-patch/90CN.cpp, VerifySelfIntegrity): + + struct SelfCrcSlot { + unsigned int magic; // 0xC0DEC0DE, used to locate the slot in the file + unsigned int offset; // absolute file offset of `expected` + unsigned int expected; // CRC32 of the whole file, skipping [offset, offset+4) + }; + +At runtime the DLL re-reads itself, recomputes that CRC and compares. Any byte +change (e.g. patching out the check) makes the client refuse to start. + +The CRC is the standard reflected CRC-32 (poly 0xEDB88320, init 0xFFFFFFFF, +final XOR 0xFFFFFFFF) == zlib.crc32 / binascii.crc32. + +Usage: + python patch_dll_crc.py verify # check the current stored CRC + python patch_dll_crc.py patch # rewrite offset+expected in place + python patch_dll_crc.py patch -o # write to a copy instead + +Exit codes: 0 = ok, 1 = verification failure / slot not found, 2 = usage error. +""" + +from __future__ import annotations + +import argparse +import binascii +import struct +import sys +from pathlib import Path + +MAGIC = 0xC0DEC0DE +MAGIC_BYTES = struct.pack(" int: + """Return the file offset of the SelfCrcSlot struct. + + The magic must be followed by a plausible `offset` field pointing at the + `expected` field it lives in; that rejects accidental magic collisions in + code or data. + """ + start = 0 + candidates: list[int] = [] + while True: + pos = blob.find(MAGIC_BYTES, start) + if pos < 0: + break + candidates.append(pos) + start = pos + 1 + if not candidates: + raise SlotError("0xC0DEC0DE slot magic not found") + + for pos in candidates: + if pos + SLOT_SIZE > len(blob): + continue + offset = struct.unpack_from(" int: + """CRC32 of the whole file, skipping the 4 `expected` bytes.""" + if expected_offset < 16 or expected_offset + 4 > len(blob): + raise SlotError( + f"slot offset 0x{expected_offset:X} out of range (size 0x{len(blob):X})" + ) + crc = 0xFFFFFFFF + crc = binascii.crc32(blob[:expected_offset], crc) + crc = binascii.crc32(blob[expected_offset + 4 :], crc) + return crc & 0xFFFFFFFF + + +def read_slot(blob: bytes) -> tuple[int, int, int]: + pos = find_slot(blob) + magic, offset, expected = struct.unpack_from(" int: + blob = path.read_bytes() + try: + pos, offset, expected = read_slot(blob) + except SlotError as exc: + print(f"FAIL {path.name}: {exc}") + return 1 + print(f"slot : file 0x{pos:08X} offset=0x{offset:08X} expected=0x{expected:08X}") + if offset != pos + EXPECTED_FIELD: + print(f"FAIL {path.name}: slot offset does not point at itself") + return 1 + actual = compute_crc(blob, offset) + print(f"crc32 : computed=0x{actual:08X} stored=0x{expected:08X}") + if actual != expected: + print(f"FAIL {path.name}: CRC mismatch (build not patched, or file modified)") + return 1 + print(f"OK {path.name}: self-integrity CRC verified") + return 0 + + +def cmd_patch(src: Path, dst: Path | None) -> int: + blob = bytearray(src.read_bytes()) + try: + pos = find_slot(blob) + except SlotError as exc: + print(f"FAIL {src.name}: {exc}") + return 1 + + expected_offset = pos + EXPECTED_FIELD + # offset must be stored before computing: it is part of the hashed range. + struct.pack_into(" int: + parser = argparse.ArgumentParser(description="90CN.dll self-integrity CRC patcher") + parser.add_argument("action", choices=["verify", "patch"]) + parser.add_argument("dll", type=Path) + parser.add_argument("-o", "--out", type=Path, help="output path for patch (default: in place)") + args = parser.parse_args(argv) + + if not args.dll.is_file(): + print(f"FAIL {args.dll}: not a file") + return 2 + if args.action == "verify": + return cmd_verify(args.dll) + return cmd_patch(args.dll, args.out) + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:])) diff --git a/server/internal/services/dnfbridge/client_build_id.go b/server/internal/services/dnfbridge/client_build_id.go index cd67113..3f8737c 100644 --- a/server/internal/services/dnfbridge/client_build_id.go +++ b/server/internal/services/dnfbridge/client_build_id.go @@ -10,8 +10,8 @@ const ( boundMovieSHA256 = "D62E3623C39CD9FCED5F6C4C7F5E6E18BEB69EB269ABF5D713EDE08EE8C926A1" boundMovieBytes = int64(22846532) - boundDllSHA256 = "F6DDA53482B6B8F6A3FD155FF0A15EB590C5AFF9572140004242793DC957561C" - boundDllBytes = int64(1230336) + boundDllSHA256 = "30E41FF583893E611E6975DF96246E1320B042F7B33A32503C987F211D1776F5" + boundDllBytes = int64(1215488) )