在针对 CrossFire(CF 2.0)服务端架构进行扩容改造时,核心难点在于突破其硬编码的物品数量限制5119。

早期的解决方案通常依赖于静态修改(Hex Editing)或基于轮询(Polling)的运行时内存补丁。前者破坏文件完整性,极易触发校验;后者引入了竞态条件(Race Condition),导致补丁成功率无法达到 100%。

本文将探讨一种确定性的解决方案:利用 DLL 劫持(DLL Proxying) 配合 Inline Hook 技术,拦截 kernel32!LoadLibraryA,将补丁操作原子化地嵌入到模块加载流程中。

竞态困境:为什么 while(true) 是有问题的?

在早期的补丁开发中,常见的逻辑是开启一个后台线程,不断轮询 GetModuleHandle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 反面教材:非确定性补丁
DWORD WINAPI PatchThread(LPVOID) {
while (true) {
HMODULE hModule = GetModuleHandleA("CShell.dll");
if (hModule) {
// 危险区:模块可能刚被映射,但 Shell 代码尚未解压
// 或者模块已初始化完毕,主逻辑已经读取了旧的 Limit 值
ApplyPatch(hModule);
break;
}
Sleep(1); // 上下文切换带来的不确定性
}
return 0;
}

这种方案本质上是在与 OS 的 Loader 赛跑。

  1. 解压延迟:目标模块(CShell.dll)通常带有加壳保护(如 VMP/Themida)。GetModuleHandle 返回非空仅代表 PE 头已映射,并不代表代码段(.text)已解压。此时写入内存可能无效或导致崩溃。
  2. 读取抢跑:如果补丁线程被调度器挂起,而游戏主线程恰好执行到了 CMP EAX, 1400h,那么这次启动就宣告扩容失败。

为了实现工业级的稳定性,必须消除这种“时间窗口”。

架构设计:基于 LoadLibrary 的同步劫持

Windows PE 加载机制决定了 LoadLibrary 是最佳的切入点。当 LoadLibrary 函数返回时,意味着:

  1. 文件映射完成。
  2. 导入表(IAT)重定位完成。
  3. DllMain (Process Attach) 执行完毕 —— 这意味着壳代码已经完成了自解压,内存中的机器码已就绪。

因此,如果我们在 LoadLibrary 返回之后、但在句柄交给调用者(游戏主程序)之前插入补丁逻辑,就能实现原子级的同步修改。

执行流重定向 (Control Flow Redirection)

我们采用 MinHook 库对 LoadLibraryA 进行 Inline Hook。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
graph TD
Game["Game Main Thread"] -->|Call| Trampoline["LoadLibraryA (Hooked)"]
Trampoline -->|Jump| Detour["DetourLoadLibraryA"]

Detour -->|Call Original| Kernel["Kernel32!LoadLibraryA"]
Kernel -->|Load & Unpack| CShell["CShell.dll in Memory"]
Kernel -->|Return Handle| Detour

Detour -->|Inspect| Check{"Is CShell?"}
Check -- Yes --> Patch["**Apply Memory Patch**"]
Check -- No --> Return

Patch -->|Modify .text| CShell
Patch --> Return["Return Handle"]

Return --> Game

在该模型下,补丁操作成为模块加载事务的一部分。对上层逻辑而言,它拿到的 CShell 句柄在“出生”的一瞬间就已经突破了上限。

核心实现

实现分为两部分:DLL 代理转发(维持系统调用兼容性)与 Hook 逻辑。

基础设施搭建

利用 version.dll 进行劫持,首先需要导出原版 DLL 的所有函数,防止游戏启动报错。

1
2
3
4
// version.dll 代理转发 (部分示例)
#pragma comment(linker, "/EXPORT:GetFileVersionInfoA=_Proxy_GetFileVersionInfoA")
// ... 其他导出函数定义

Detour 函数实现

这是核心 Hook 逻辑。注意我们需要在 fpLoadLibraryA 返回后立即操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 定义函数原型
typedef HMODULE(WINAPI* tLoadLibraryA)(LPCSTR lpLibFileName);
tLoadLibraryA fpLoadLibraryA = NULL;

// Detour 函数
HMODULE WINAPI DetourLoadLibraryA(LPCSTR lpLibFileName) {
// 1. Pass-through: 让 OS 完成加载和解压
HMODULE hModule = fpLoadLibraryA(lpLibFileName);

// 2. Inspection: 检查是否为目标模块 (忽略大小写)
if (hModule && IsTargetModule(lpLibFileName)) {
// 3. Deterministic Patch: 此时内存已由 RX 变为 RWX (或可修改状态)
// 且主线程被阻塞在此函数内,绝对安全
ApplyPatchesToCShell(hModule);
}

return hModule;
}

内存特征扫描

为了对抗 ASLR(地址空间布局随机化)及不同版本的偏移变化,不能使用硬编码地址。必需基于特征码(Opcode Signature)进行全段扫描。

特别注意:扩容不仅涉及 0x1400 (5120) 的 Limit Check,还涉及内存池大小的分配 (new size)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
void ApplyPatchesToCShell(HMODULE hCShell) {
MODULEINFO mInfo;
GetModuleInformation(GetCurrentProcess(), hCShell, &mInfo, sizeof(MODULEINFO));

DWORD_PTR start = (DWORD_PTR)mInfo.lpBaseOfDll;
DWORD_PTR end = start + mInfo.SizeOfImage;

// 特征值定义
const int OLD_LIMIT = 0x00001400; // 5120
const int NEW_LIMIT = 0x00002000; // 8192
const int OLD_SIZE = 0x01252000; // ~19MB
const int NEW_SIZE = 0x01D50000; // ~30MB

for (DWORD_PTR p = start; p < end - 8; p++) {
__try {
int* pValue = (int*)p;
unsigned char* ptr = (unsigned char*)p;

// 1. 扩容内存池 (Memory Pool Expansion)
if (*pValue == OLD_SIZE) {
// 校验 Opcode 上下文: PUSH (68) 或 MOV (B8)
if (*(ptr - 1) == 0x68 || *(ptr - 1) == 0xB8) {
PatchMemory(p, NEW_SIZE);
}
}

// 2. 修正逻辑上限 (Logic Limit Fix)
// 针对 CMP [Reg], 1400h 等指令
else if (*pValue == OLD_LIMIT) {
// 排除 offset 计算 (如 SUB EAX, 1400h),仅修改比较指令
if (IsCompareOpcode(ptr)) {
PatchMemory(p, NEW_LIMIT);
}
}
}
__except (1) { continue; }
}
}

注:IsCompareOpcode 需自行实现,用于过滤 CMP (0x3D, 0x81) 等指令,避免误改 SUB/ADD 指令导致的 ID 偏移错误。

关键技术细节

为什么必须过滤 Offset 计算?

在逆向过程中发现,服务端存在大量 ID - 5120 的操作。这是用于将 WeaponID 映射到数组索引的基准偏移(Base Offset)。如果盲目将所有的 5120 替换为 8192,会导致索引计算溢出(负数索引),引发 Access Violation。
原则:只改限制(Limit),不改偏移(Offset)。

MinHook 的初始化时机

MinHook 的初始化应放在 DllMainDLL_PROCESS_ATTACH 阶段。此时 kernel32.dll 必定已加载,Hook 操作是安全的。

1
2
3
4
5
6
7
8
9
10
11
12
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) {
if (ul_reason_for_call == DLL_PROCESS_ATTACH) {
DisableThreadLibraryCalls(hModule);
LoadOriginalDll(); // 建立代理

MH_Initialize();
MH_CreateHookApi(L"kernel32", "LoadLibraryA", &DetourLoadLibraryA, (LPVOID*)&fpLoadLibraryA);
MH_EnableHook(MH_ALL_HOOKS);
}
return TRUE;
}

笔记

一条item占用的空间为 0xEA8

005.LTC 单个武器占用空间为 0x479C

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
std::vector<PatchTask> tasks = {
// Group 1: Item Limit - 1 Check (0x13FF -> 0x3FFF)
{ "ItemLimit_Sub_EAX", "\x3D\xFF\x13\x00\x00", "xxxxx", 1, {0xFF, 0x3F, 0x00, 0x00}, nullptr },
{ "ItemLimit_Sub_MOV", "\xB8\xFF\x13\x00\x00", "xxxxx", 1, {0xFF, 0x3F, 0x00, 0x00}, nullptr },
{ "ItemLimit_Sub_Gen", "\x81\x00\xFF\x13\x00\x00", "x?xxxx", 2, {0xFF, 0x3F, 0x00, 0x00}, IsCmpInstruction },

// Group 2: Item Limit Check (0x1400 -> 0x4000)
{ "ItemLimit_PUSH", "\x68\x00\x14\x00\x00", "xxxxx", 1, {0x00, 0x40, 0x00, 0x00}, nullptr },
{ "ItemLimit_EAX", "\x3D\x00\x14\x00\x00", "xxxxx", 1, {0x00, 0x40, 0x00, 0x00}, nullptr },
{ "ItemLimit_ESI", "\xBE\x00\x14\x00\x00", "xxxxx", 1, {0x00, 0x40, 0x00, 0x00}, nullptr },
{ "ItemLimit_Gen", "\x81\x00\x00\x14\x00\x00", "x?xxxx", 2, {0x00, 0x40, 0x00, 0x00}, IsCmpInstruction },

// Group 3: Item Alloc Size (0x01252000 -> 0x03AA0000)
{ "ItemAlloc_Gen", "\x81\x00\x00\x20\x25\x01", "x?xxxx", 2, {0x00, 0x00, 0xAA, 0x03}, IsCmpInstruction },
{ "ItemAlloc_PUSH", "\x68\x00\x20\x25\x01", "xxxxx", 1, {0x00, 0x00, 0xAA, 0x03}, nullptr },
{ "ItemAlloc_EAX", "\x3D\x00\x20\x25\x01", "xxxxx", 1, {0x00, 0x00, 0xAA, 0x03}, nullptr },

// Group 4: Weapon Alloc & Limit
// =======================================================
// 组 1: Limit - 1 Check (0xFFF -> 0x2FFF)
// =======================================================

// cshell.dll+D994 - 81 FA ... (CMP EDX, 2FFF)
{ "Limit1_EDX", 0xD994, { 0x81, 0xFA, 0xFF, 0x2F, 0x00, 0x00 } },

// cshell.dll+5636EB - 66 81 F9 ... (CMP CX, 2FFF)
{ "Limit1_CX_1", 0x5636EB, { 0x66, 0x81, 0xF9, 0xFF, 0x2F } },

// cshell.dll+563729 - 66 3D ... (CMP AX, 2FFF)
{ "Limit1_AX_1", 0x563729, { 0x66, 0x3D, 0xFF, 0x2F } },

// cshell.dll+56379A - 66 81 F9 ... (CMP CX, 2FFF)
{ "Limit1_CX_2", 0x56379A, { 0x66, 0x81, 0xF9, 0xFF, 0x2F } },

// cshell.dll+56C5A3 - 66 3D ... (CMP AX, 2FFF)
{ "Limit1_AX_2", 0x56C5A3, { 0x66, 0x3D, 0xFF, 0x2F } },

// cshell.dll+56C600 - 66 81 F9 ... (CMP CX, 2FFF)
{ "Limit1_CX_3", 0x56C600, { 0x66, 0x81, 0xF9, 0xFF, 0x2F } },

// cshell.dll+56D35E - 66 81 FE ... (CMP SI, 2FFF)
{ "Limit1_SI", 0x56D35E, { 0x66, 0x81, 0xFE, 0xFF, 0x2F } },

// cshell.dll+6902BF - 66 3D ... (CMP AX, 2FFF)
{ "Limit1_AX_3", 0x6902BF, { 0x66, 0x3D, 0xFF, 0x2F } },

// cshell.dll+69030F - 66 3D ... (CMP AX, 2FFF)
{ "Limit1_AX_4", 0x69030F, { 0x66, 0x3D, 0xFF, 0x2F } },

// cshell.dll+690375 - 66 3D ... (CMP AX, 2FFF)
{ "Limit1_AX_5", 0x690375, { 0x66, 0x3D, 0xFF, 0x2F } },

// cshell.dll+6904BF - 66 3D ... (CMP AX, 2FFF)
{ "Limit1_AX_6", 0x6904BF, { 0x66, 0x3D, 0xFF, 0x2F } },

// cshell.dll+6910A5 - 66 3D ... (CMP AX, 2FFF)
{ "Limit1_AX_7", 0x6910A5, { 0x66, 0x3D, 0xFF, 0x2F } },

// cshell.dll+733D96 - 66 81 F9 ... (CMP CX, 2FFF)
{ "Limit1_CX_4", 0x733D96, { 0x66, 0x81, 0xF9, 0xFF, 0x2F } },

// cshell.dll+DA174A - 81 FF ... (CMP EDI, 2FFF)
{ "Limit1_EDI", 0xDA174A, { 0x81, 0xFF, 0xFF, 0x2F, 0x00, 0x00 } },


// =======================================================
// 组 2: Limit Check (0x1000 -> 0x3000)
// =======================================================

// cshell.dll+C19E1 - 81 FD ... (CMP EBP, 3000)
{ "Limit2_EBP", 0xC19E1, { 0x81, 0xFD, 0x00, 0x30, 0x00, 0x00 } },

// cshell.dll+1A7E0B - 81 FE ... (CMP ESI, 3000)
{ "Limit2_ESI_1", 0x1A7E0B, { 0x81, 0xFE, 0x00, 0x30, 0x00, 0x00 } },

// cshell.dll+5636AF - 81 FE ... (CMP ESI, 3000)
{ "Limit2_ESI_2", 0x5636AF, { 0x81, 0xFE, 0x00, 0x30, 0x00, 0x00 } },

// cshell.dll+5756C2 - 81 FF ... (CMP EDI, 3000)
{ "Limit2_EDI_1", 0x5756C2, { 0x81, 0xFF, 0x00, 0x30, 0x00, 0x00 } },

// cshell.dll+575B13 - 81 FF ... (CMP EDI, 3000)
{ "Limit2_EDI_2", 0x575B13, { 0x81, 0xFF, 0x00, 0x30, 0x00, 0x00 } },

{ "WeaponAlloc", "\x68\x00\xC0\x79\x04", "xxxxx", 1, {0x00, 0x00, 0xD4, 0xD6}, nullptr },

};