ReplacementFuncs.h 解析:函式替換技術
本篇定位
這是 Phase 6「進階功能實作」的第一篇。
ReplacementFuncs.h定義了一批「替換函式(Replacement Functions)」—— 不只是攔截遊戲函式,而是完全取代其行為,重新實作整個邏輯。
一、Hook 攔截 vs 函式替換
到目前為止,我們學到的 Hook 模式是「攔截 + 選擇是否呼叫原始函式」:
// 攔截模式:修改參數,再呼叫原始函式
void Hooked_Connect(void* thisPtr, const char* host, uint16_t port) {
original_Connect(thisPtr, "127.0.0.1", 8484); // 改 IP,但還是呼叫原始邏輯
}函式替換模式不同:完全不呼叫原始函式,我們自己重新實作整個功能:
// 替換模式:原始函式的邏輯完全被我們的版本取代
void* Replacement_GetProcAddress(HMODULE hModule, LPCSTR lpProcName) {
// 整個函式邏輯都是我們寫的
// 遊戲的原始 GetProcAddress 永遠不會被呼叫
return OurCustomGetProcAddress(hModule, lpProcName);
}什麼時候用替換而不用攔截?
- 原始函式的行為完全不符合需求(不只是改參數,而是整個邏輯要換)
- 原始函式有反除錯 / 反注入邏輯,呼叫它會讓遊戲偵測到我們
- 我們對這個功能有更好的實作方式(例如自訂記憶體分配器)
二、MyGetProcAddress:遊戲的自訂查詢函式
MapleStory v83 有一個內部函式 sub_44E88E,作用類似 Windows 的 GetProcAddress,但加入了 Themida 的混淆邏輯。遊戲用它來動態查詢自己 DLL 匯出的函式地址,同時可以偵測「這個函式有沒有被 Hook 過」。
我們把它完全替換:
// 原始函式簽名(從 IDA Pro 逆向得到)
using tMyGetProcAddress = void*(__cdecl*)(HMODULE hModule, LPCSTR lpProcName);
// Trampoline(替換後不呼叫,但仍需宣告供 Detours 使用)
tMyGetProcAddress original_MyGetProcAddress =
(tMyGetProcAddress)addys::MyGetProcAddress;
// 替換函式
void* __cdecl Replacement_MyGetProcAddress(HMODULE hModule, LPCSTR lpProcName) {
// 直接呼叫真正的 Windows GetProcAddress,跳過 Themida 混淆層
void* result = (void*)::GetProcAddress(hModule, lpProcName);
// 如果 Windows 找不到(result == nullptr),有幾種情況:
// 1. 函式不存在(正常情況,回傳 nullptr 即可)
// 2. 函式名稱被 Themida 加密(需要解密後再查詢)
// MapleEzorsia-v2 選擇直接回傳 Windows 的結果,跳過加密層
return result;
}為什麼繞過 Themida 的 GetProcAddress?
Themida 的自訂 GetProcAddress 可能包含「檢查回傳的函式指標是否被 Hook 過」的邏輯(比對函式開頭的 byte)。如果我們的 Hook 改了某個函式開頭,原版 GetProcAddress 可能就回傳 nullptr 或觸發崩潰。用我們的版本繞過這個檢查。
三、Replacement_CWvsApp_CWvsApp:替換遊戲建構子
CWvsApp 是 MapleStory 的主應用程式類別,它的建構子(sub_9F4FDA)做了大量初始化工作,其中有幾個我們不想要的行為:
using tCWvsApp_Ctor = void(__thiscall*)(void* thisPtr);
tCWvsApp_Ctor original_CWvsApp_Ctor = (tCWvsApp_Ctor)addys::CWvsApp_Ctor;
void __fastcall Replacement_CWvsApp_Ctor(void* thisPtr, void* edx) {
// 先呼叫原始建構子(這次是攔截 + 替換的混合模式)
original_CWvsApp_Ctor(thisPtr);
// 原始建構子跑完後,修正它做了但我們不想要的事:
// 1. 把「全螢幕模式」設定改回視窗模式
// (建構子內部有一段從 registry 讀全螢幕設定的邏輯)
*(BOOL*)((BYTE*)thisPtr + 0x20) = TRUE; // Windowed = TRUE
// 2. 停用多螢幕偵測
// (建構子會查詢 Monitor 數量,多螢幕時改變解析度策略)
*(int*)((BYTE*)thisPtr + 0x3C) = 1; // MonitorCount = 1(假裝只有一個螢幕)
}四、Replacement_StringPool_GetString:字串重導向
StringPool::GetString 是遊戲取得所有 UI 字串的入口。替換它讓我們可以在執行期修改任何遊戲顯示的文字:
using tStringPool_GetString = const wchar_t*(__thiscall*)(void* thisPtr, int stringId);
tStringPool_GetString original_StringPool_GetString =
(tStringPool_GetString)addys::StringPool_GetString;
// 我們維護的自訂字串表
std::unordered_map<int, std::wstring> g_customStrings;
const wchar_t* __fastcall Replacement_StringPool_GetString(
void* thisPtr, void* edx, int stringId)
{
// 先查我們的自訂表
auto it = g_customStrings.find(stringId);
if (it != g_customStrings.end()) {
return it->second.c_str(); // 回傳自訂字串
}
// 找不到就用原始的
return original_StringPool_GetString(thisPtr, stringId);
}使用範例:
// 在 MainFunc 裡修改登入畫面的伺服器選單文字
g_customStrings[12001] = L"Scania (私服)";
g_customStrings[12002] = L"Bera (私服)";五、ReplacementFuncs.h 的整體結構
#pragma once
// ── 函式型別定義 ──────────────────────────────────────────────────────────────
using tMyGetProcAddress = void*(__cdecl*)(HMODULE, LPCSTR);
using tCWvsApp_Ctor = void(__thiscall*)(void*);
using tStringPool_GetString = const wchar_t*(__thiscall*)(void*, int);
// ...
// ── Trampoline 指標(替換函式也需要宣告,供 Detours DetourAttach 使用)──────
extern tMyGetProcAddress original_MyGetProcAddress;
extern tCWvsApp_Ctor original_CWvsApp_Ctor;
extern tStringPool_GetString original_StringPool_GetString;
// ...
// ── 替換函式宣告 ──────────────────────────────────────────────────────────────
void* __cdecl Replacement_MyGetProcAddress(HMODULE, LPCSTR);
void __fastcall Replacement_CWvsApp_Ctor(void*, void*);
const wchar_t* __fastcall Replacement_StringPool_GetString(void*, void*, int);
// ...
// ── 安裝 / 卸載函式 ───────────────────────────────────────────────────────────
void Hook_MyGetProcAddress(bool install);
void Hook_CWvsApp_Ctor(bool install);
void Hook_StringPool_GetString(bool install);六、常見問題
Q:替換函式如果根本不呼叫原始函式,Trampoline 指標還需要嗎?
需要。
DetourAttach的第一個參數就是 Trampoline 指標,Detours 用它來找到目標函式的位置(它從這個指標讀取地址,然後在那裡安裝 JMP)。即使你的替換函式不打算呼叫original_xxx,也必須正確宣告並傳入指標。
Q:完全替換建構子安全嗎?
有風險。建構子的邏輯通常很複雜,如果完全替換,很容易遺漏某些初始化步驟導致後續崩潰。MapleEzorsia-v2 採用「先呼叫原始建構子,再修正」的混合模式,比完全替換安全得多。
延伸閱讀
- 02_Detours函式庫使用方式 — 替換函式同樣用 DetourAttach 安裝
- 02_dllmain.cpp_DLL入口點 — 這些替換函式在 MainFunc 中安裝
- 02_ZAllocEx.cpp_自訂記憶體分配 — 下一篇:另一種「替換」技術