修正客户端完整性校验:补齐 CRC 后处理脚本并回绑 90CN.dll 指纹
问题:上一提交把 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。
This commit is contained in:
1 parent
c58168406b
commit
2ca06e113a
4 files changed
+257
-2
No files matched your search
@@ -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
|
||||||
@@ -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`""
|
||||||
@@ -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 <dll> # check the current stored CRC
|
||||||
|
python patch_dll_crc.py patch <dll> # rewrite offset+expected in place
|
||||||
|
python patch_dll_crc.py patch <dll> -o <out.dll> # 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("<I", MAGIC)
|
||||||
|
# struct layout is three consecutive little-endian uint32
|
||||||
|
SLOT_SIZE = 12
|
||||||
|
OFFSET_FIELD = 4
|
||||||
|
EXPECTED_FIELD = 8
|
||||||
|
|
||||||
|
|
||||||
|
class SlotError(RuntimeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def find_slot(blob: bytes) -> 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("<I", blob, pos + OFFSET_FIELD)[0]
|
||||||
|
if offset == pos + EXPECTED_FIELD and offset + 4 <= len(blob):
|
||||||
|
return pos
|
||||||
|
# Fall back to the first candidate so `patch` can still repair an unpatched
|
||||||
|
# or corrupted slot (fresh build has offset == 0).
|
||||||
|
return candidates[0]
|
||||||
|
|
||||||
|
|
||||||
|
def compute_crc(blob: bytes, expected_offset: int) -> 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("<III", blob, pos)
|
||||||
|
return pos, offset, expected
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_verify(path: Path) -> 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("<I", blob, pos + OFFSET_FIELD, expected_offset)
|
||||||
|
crc = compute_crc(bytes(blob), expected_offset)
|
||||||
|
struct.pack_into("<I", blob, pos + EXPECTED_FIELD, crc)
|
||||||
|
|
||||||
|
out = dst or src
|
||||||
|
out.write_bytes(bytes(blob))
|
||||||
|
print(f"slot : file 0x{pos:08X} offset=0x{expected_offset:08X} expected=0x{crc:08X}")
|
||||||
|
print(f"OK {out.name}: patched ({len(blob)} bytes)")
|
||||||
|
|
||||||
|
# Re-read and verify what we just wrote.
|
||||||
|
return cmd_verify(out)
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: list[str]) -> 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:]))
|
||||||
@@ -10,8 +10,8 @@ const (
|
|||||||
boundMovieSHA256 = "D62E3623C39CD9FCED5F6C4C7F5E6E18BEB69EB269ABF5D713EDE08EE8C926A1"
|
boundMovieSHA256 = "D62E3623C39CD9FCED5F6C4C7F5E6E18BEB69EB269ABF5D713EDE08EE8C926A1"
|
||||||
boundMovieBytes = int64(22846532)
|
boundMovieBytes = int64(22846532)
|
||||||
|
|
||||||
boundDllSHA256 = "F6DDA53482B6B8F6A3FD155FF0A15EB590C5AFF9572140004242793DC957561C"
|
boundDllSHA256 = "30E41FF583893E611E6975DF96246E1320B042F7B33A32503C987F211D1776F5"
|
||||||
boundDllBytes = int64(1230336)
|
boundDllBytes = int64(1215488)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user