返回博客列表

逆向工程 Kenshi:通过 DLL 注入实现多人联机

我如何通过 DLL 注入和内存 Hook 为一款单机游戏添加多人联机功能,而无需修改游戏二进制文件。

#Game Dev#Reverse Engineering#C++#Win32

Kenshi 是一款独特的沙盒 RPG,以其残酷的难度和开放的游戏性著称。作为一款单机游戏,它天生不支持多人联机。本项目探索是否可以通过外部代码注入添加协作多人模式——而不触碰游戏的原始二进制文件或 MOD 系统。

技术挑战

为一款从未设计过多人模式的游戏添加联机功能面临诸多挑战:

  1. 没有网络代码 — 一切必须从零构建
  2. 没有源代码 — 我们只能操作编译后的二进制文件
  3. 反 MOD 架构 — 游戏的 MOD 系统不支持玩法修改
  4. 复杂的游戏状态 — 数百个实体,每个都有位置、属性、物品等

方案:外部 DLL 注入

我们不修改游戏文件,而是向运行中的进程注入一个 DLL:

┌─────────────────────────────────────────────────────────┐
│                    Kenshi.exe                           │
│  ┌─────────────────────────────────────────────────┐  │
│  │              游戏内存空间                        │  │
│  │  ┌─────────────┐  ┌──────────────────────────┐  │  │
│  │  │ 游戏代码    │  │ 注入的 KenshiMP.dll       │  │  │
│  │  │ (原始)      │  │ • 内存 hook              │  │  │
│  │  │             │  │ • 网络同步              │  │  │
│  │  │             │  │ • 状态管理              │  │  │
│  │  └─────────────┘  └──────────────────────────┘  │  │
│  └─────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────┘

注入过程

// 启动器: KenshiMultiplayer.exe
bool InjectDLL(HANDLE hProcess, const char* dllPath) {
    // 在目标进程中为 DLL 路径分配内存
    LPVOID pRemotePath = VirtualAllocEx(
        hProcess,
        nullptr,
        strlen(dllPath) + 1,
        MEM_COMMIT | MEM_RESERVE,
        PAGE_READWRITE
    );
 
    if (!pRemotePath) return false;
 
    // 将 DLL 路径写入目标进程
    WriteProcessMemory(hProcess, pRemotePath, dllPath, strlen(dllPath) + 1, nullptr);
 
    // 获取 LoadLibraryA 地址
    HMODULE hKernel32 = GetModuleHandleA("kernel32.dll");
    FARPROC pLoadLibrary = GetProcAddress(hKernel32, "LoadLibraryA");
 
    // 创建远程线程来加载我们的 DLL
    HANDLE hThread = CreateRemoteThread(
        hProcess,
        nullptr,
        0,
        (LPTHREAD_START_ROUTINE)pLoadLibrary,
        pRemotePath,
        0,
        nullptr
    );
 
    WaitForSingleObject(hThread, INFINITE);
 
    // 清理
    VirtualFreeEx(hProcess, pRemotePath, 0, MEM_RELEASE);
    CloseHandle(hThread);
 
    return true;
}

内存 Hook:找到正确的地址

多人系统的核心是 Hook 游戏函数来拦截和同步状态。我们需要找到以下内容的内存地址:

1. 角色位置

// 通过模式扫描找角色结构基址
void* FindCharacterBase() {
    // 通过逆向工程发现的已知模式
    // mov rax, [CharacterPool + offset]
    // Pattern: 48 8B 05 ?? ?? ?? ?? 48 85 C0
    const char pattern[] = "\x48\x8B\x05\x00\x00\x00\x00\x48\x85\xC0";
    const char mask[] = "xxx???xxx";
 
    return ScanPattern(GetModuleHandle(nullptr), pattern, mask, 10);
}

2. Hook 角色更新

// 角色位置更新的 Trampoline hook
void __fastcall HookedUpdatePosition(Character* character, float x, float y, float z) {
    // 保存原始位置
    Vector3 originalPos = {character->x, character->y, character->z};
 
    // 调用原始函数
    OriginalUpdatePosition(character, x, y, z);
 
    // 如果是选中的角色,广播到网络
    if (character == GetSelectedCharacter()) {
        NetworkManager::BroadcastPosition({
            .id = character->id,
            .x = character->x,
            .y = character->y,
            .z = character->z
        });
    }
}

状态同步

网络架构

┌────────────┐                    ┌────────────┐
│   主机     │◄────── TCP ──────►│   客户端   │
│            │                    │            │
│ • 权威状态 │                    │ • 接收状态 │
│ • 广播变更 │                    │ • 本地应用 │
│ • MOD 验证 │                    │ • 预测补偿 │
└────────────┘                    └────────────┘
      │                                  │
      ▼                                  ▼
┌─────────────────────────────────────────────┐
│              同步状态                        │
│ • 选中角色的位置/HP                         │
│ • 金币数量                                  │
│ • 世界时间                                  │
└─────────────────────────────────────────────┘

位置同步协议

#pragma pack(push, 1)
struct PositionPacket {
    uint8_t type;        // PACKET_POSITION
    uint32_t characterId;
    float x, y, z;
    uint32_t timestamp;
};
#pragma pack(pop)
 
void PositionSync::Update() {
    // 主机: 广播选中角色位置
    if (IsHost()) {
        Character* selected = GetSelectedCharacter();
        if (selected && selected->positionChanged) {
            PositionPacket pkt = {
                .type = PACKET_POSITION,
                .characterId = selected->id,
                .x = selected->x,
                .y = selected->y,
                .z = selected->z,
                .timestamp = GetTickCount()
            };
            Broadcast(&pkt, sizeof(pkt));
            selected->positionChanged = false;
        }
    }
    // 客户端: 应用接收到的位置
    else {
        ApplyPendingPositions();
    }
}

MOD 验证

为确保所有玩家拥有相同的游戏状态,连接前需验证 MOD 列表:

bool VerifyMODList(const std::vector<std::string>& clientMods) {
    auto hostMods = GetInstalledMODs();
 
    if (hostMods.size() != clientMods.size()) {
        return false;
    }
 
    for (size_t i = 0; i < hostMods.size(); i++) {
        if (hostMods[i].name != clientMods[i].name ||
            hostMods[i].hash != clientMods[i].hash) {
            return false;
        }
    }
 
    return true;
}

崩溃保护

游戏崩溃可能随时发生。我们实现 SEH(结构化异常处理)来捕获崩溃转储:

__declspec(noinline) LONG WINAPI CrashHandler(EXCEPTION_POINTERS* pException) {
    // 创建 minidump
    HANDLE hFile = CreateFileA(
        "kenshi_mp_crash.dmp",
        GENERIC_WRITE,
        0,
        nullptr,
        CREATE_ALWAYS,
        FILE_ATTRIBUTE_NORMAL,
        nullptr
    );
 
    if (hFile != INVALID_HANDLE_VALUE) {
        MINIDUMP_EXCEPTION_INFORMATION mei = {
            .ThreadId = GetCurrentThreadId(),
            .ExceptionPointers = pException,
            .ClientPointers = FALSE
        };
 
        MiniDumpWriteDump(
            GetCurrentProcess(),
            GetCurrentProcessId(),
            hFile,
            MiniDumpWithDataSegs,
            &mei,
            nullptr,
            nullptr
        );
 
        CloseHandle(hFile);
    }
 
    return EXCEPTION_CONTINUE_SEARCH;
}
 
// 安装崩溃处理器
SetUnhandledExceptionFilter(CrashHandler);

结果与限制

已实现功能

  • 角色位置同步
  • 生命值显示
  • 金币数量同步
  • 世界时间同步
  • 90 秒内断线重连

当前限制

  • 仅同步选中角色(非完整世界状态)
  • 无 NPC 同步
  • 无战斗状态同步
  • 依赖固定偏移(游戏更新后需调整)

伦理考量

本项目仅用于教育和研究目的。核心原则:

  1. 不修改游戏 — 原始二进制文件保持不变
  2. 不作弊 — 仅多人联机,无游戏优势
  3. MOD 兼容 — 在现有 MOD 生态内工作
  4. 开源 — 代码可审计,AGPL-3.0 许可

参考


本文涵盖技术实现。游戏修改工具应负责任地使用,并遵守游戏服务条款。