DLL 劫持与内联挂钩实战:破除 CF 2.0 Item 限制
在针对 CrossFire(CF 2.0)服务端架构进行扩容改造时,核心难点在于突破其硬编码的物品数量限制5119。
早期的解决方案通常依赖于静态修改(Hex Editing)或基于轮询(Polling)的运行时内存补丁。前者破坏文件完整性,极易触发校验;后者引入了竞态条件(Race Condition),导致补丁成功率无法达到 100%。
本文将探讨一种确定性的解决方案:利用 DLL 劫持(DLL Proxying) 配合 Inline Hook 技术,拦截 kernel32!LoadLibraryA,将补丁操作原子化地嵌入到模块加载流程中。
竞态困境:为什么 while(true) 是有问题的?
在早期的补丁开发中,常见的逻辑是开启一个后台线程,不断轮询 GetModuleHandle:
1 | // 反面教材:非确定性补丁 |
这种方案本质上是在与 OS 的 Loader 赛跑。
- 解压延迟:目标模块(CShell.dll)通常带有加壳保护(如 VMP/Themida)。
GetModuleHandle返回非空仅代表 PE 头已映射,并不代表代码段(.text)已解压。此时写入内存可能无效或导致崩溃。 - 读取抢跑:如果补丁线程被调度器挂起,而游戏主线程恰好执行到了
CMP EAX, 1400h,那么这次启动就宣告扩容失败。
为了实现工业级的稳定性,必须消除这种“时间窗口”。
架构设计:基于 LoadLibrary 的同步劫持
Windows PE 加载机制决定了 LoadLibrary 是最佳的切入点。当 LoadLibrary 函数返回时,意味着:
- 文件映射完成。
- 导入表(IAT)重定位完成。
DllMain(Process Attach) 执行完毕 —— 这意味着壳代码已经完成了自解压,内存中的机器码已就绪。
因此,如果我们在 LoadLibrary 返回之后、但在句柄交给调用者(游戏主程序)之前插入补丁逻辑,就能实现原子级的同步修改。
执行流重定向 (Control Flow Redirection)
我们采用 MinHook 库对 LoadLibraryA 进行 Inline Hook。
1 | graph TD |
在该模型下,补丁操作成为模块加载事务的一部分。对上层逻辑而言,它拿到的 CShell 句柄在“出生”的一瞬间就已经突破了上限。
核心实现
实现分为两部分:DLL 代理转发(维持系统调用兼容性)与 Hook 逻辑。
基础设施搭建
利用 version.dll 进行劫持,首先需要导出原版 DLL 的所有函数,防止游戏启动报错。
1 | // version.dll 代理转发 (部分示例) |
Detour 函数实现
这是核心 Hook 逻辑。注意我们需要在 fpLoadLibraryA 返回后立即操作。
1 | // 定义函数原型 |
内存特征扫描
为了对抗 ASLR(地址空间布局随机化)及不同版本的偏移变化,不能使用硬编码地址。必需基于特征码(Opcode Signature)进行全段扫描。
特别注意:扩容不仅涉及 0x1400 (5120) 的 Limit Check,还涉及内存池大小的分配 (new size)。
1 | void ApplyPatchesToCShell(HMODULE hCShell) { |
注:IsCompareOpcode 需自行实现,用于过滤 CMP (0x3D, 0x81) 等指令,避免误改 SUB/ADD 指令导致的 ID 偏移错误。
关键技术细节
为什么必须过滤 Offset 计算?
在逆向过程中发现,服务端存在大量 ID - 5120 的操作。这是用于将 WeaponID 映射到数组索引的基准偏移(Base Offset)。如果盲目将所有的 5120 替换为 8192,会导致索引计算溢出(负数索引),引发 Access Violation。
原则:只改限制(Limit),不改偏移(Offset)。
MinHook 的初始化时机
MinHook 的初始化应放在 DllMain 的 DLL_PROCESS_ATTACH 阶段。此时 kernel32.dll 必定已加载,Hook 操作是安全的。
1 | BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) { |
笔记
一条item占用的空间为 0xEA8
005.LTC 单个武器占用空间为 0x479C
1 | std::vector<PatchTask> tasks = { |
