Detours 函式庫使用方式
本篇定位
這是 Phase 4 的第 2 篇。假設你已讀過 01_什麼是函式掛鉤(Hooking),理解 Trampoline 和 Inline Hook 的概念。 本篇說明如何在 MapleEzorsia-v2 中使用 Microsoft Detours 函式庫,讓複雜的 Hook 安裝變成幾行程式碼。
一、什麼是 Detours?
Microsoft Detours 是微軟研究院開發、現已開源的 C++ 函式庫(github.com/microsoft/Detours)。它的核心功能:
- 把目標函式開頭改成
JMP(Detour) - 自動建立 Trampoline,保留原始指令
- 提供乾淨的 API 讓你安裝和卸載 Hook
Detours 替你處理了最繁瑣的部分:
- 計算
JMP的相對偏移 - 分析函式開頭的指令邊界(確保不截斷多 byte 指令)
- 在可執行記憶體中分配 Trampoline buffer
- 在安裝/卸載期間暫停所有其他執行緒(避免競爭條件)
二、Detours 的核心 API
#include "detours.h"
// 開始一個 Hook 操作事務
DetourTransactionBegin();
// 告訴 Detours 要考慮哪些執行緒(NULL = 目前執行緒)
DetourUpdateThread(GetCurrentThread());
// 附加 Hook:pOriginal 原本指向目標函式,之後會被改成指向 Trampoline
DetourAttach(&pOriginal, pDetour);
// 提交事務(這一步才真正寫入記憶體)
DetourTransactionCommit();
// ---
// 卸載 Hook
DetourTransactionBegin();
DetourUpdateThread(GetCurrentThread());
DetourDetach(&pOriginal, pDetour);
DetourTransactionCommit();為什麼要用「事務(Transaction)」?
如果你要同時安裝 10 個 Hook,使用事務可以讓它們全部一起生效,而不是一個一個生效。在所有 Hook 就緒之前,遊戲不會「看到」一半安裝好的 Hook,避免中間狀態造成崩潰。 類似資料庫的 BEGIN / COMMIT 機制。
三、完整的 Hook 實作範例
以攔截 CClientSocket::Connect 為例,說明完整的模式:
第一步:定義函式指標型別
// CClientSocket::Connect 的原始簽名
// __thiscall 是 C++ 成員函式的呼叫慣例(this 指標透過 ECX 傳入)
typedef void(__thiscall* tCClientSocket_Connect)(
void* thisPtr, // this 指標(CClientSocket 物件)
const char* host, // 目標 IP 或主機名稱
unsigned short port // 目標 Port
);第二步:宣告 Trampoline 指標
// 初始值:指向原始函式
// DetourAttach 之後,Detours 會把這個指標改成指向 Trampoline
tCClientSocket_Connect original_CClientSocket_Connect =
(tCClientSocket_Connect)addys::CClientSocket_Connect1;第三步:撰寫 Hook 函式
// 我們的 Hook 函式,簽名必須和原始函式完全一致
void __fastcall Hook_CClientSocket_Connect(
void* thisPtr,
void* edxDummy, // __fastcall 的 EDX 位置(填充用)
const char* host,
unsigned short port
) {
// 把連線目標換成設定檔裡的私服 IP
const char* privateServerIp = MainMain::GetInstance()->GetServerIp();
unsigned short privatePort = MainMain::GetInstance()->GetServerPort();
std::cout << "[Hook] Connect redirected: "
<< host << ":" << port
<< " → "
<< privateServerIp << ":" << privatePort
<< std::endl;
// 呼叫 Trampoline(執行原始函式,但帶著修改後的參數)
original_CClientSocket_Connect(thisPtr, privateServerIp, privatePort);
}
__thiscallvs__fastcall原始函式是
__thiscall(this放 ECX),但我們的 Hook 用__fastcall(ECX = 第一個參數,EDX = 第二個參數)。 這是一個常用技巧:用__fastcall+ 插入一個edxDummy參數,可以讓函式正確收到thisPtr(原本在 ECX 的 this 指標),同時相容 Detours 的 Hook 機制。
第四步:安裝 Hook
void Hook_CClientSocket_Connect1(bool install) {
if (install) {
DetourTransactionBegin();
DetourUpdateThread(GetCurrentThread());
DetourAttach(
&(PVOID&)original_CClientSocket_Connect, // Trampoline 指標的位址
Hook_CClientSocket_Connect // 我們的 Hook 函式
);
DetourTransactionCommit();
} else {
DetourTransactionBegin();
DetourUpdateThread(GetCurrentThread());
DetourDetach(
&(PVOID&)original_CClientSocket_Connect,
Hook_CClientSocket_Connect
);
DetourTransactionCommit();
}
}在 MainFunc() 裡呼叫:
Hook_CClientSocket_Connect1(true); // 安裝
// ...
// Hook_CClientSocket_Connect1(false); // 卸載(通常在 DLL_PROCESS_DETACH 時)四、記憶體狀態變化圖
【安裝前】
original_CClientSocket_Connect → 0x00494CA3
記憶體 0x00494CA3:
55 PUSH EBP
8B EC MOV EBP, ESP
83 EC 08 SUB ESP, 8
...(原始函式本體)
【DetourAttach 之後】
original_CClientSocket_Connect → [Trampoline Buffer]
記憶體 0x00494CA3:
E9 XX XX XX XX JMP Hook_CClientSocket_Connect ← 已修改
[Trampoline Buffer](由 Detours 動態分配):
55 PUSH EBP ← 複製的原始開頭指令
8B EC MOV EBP, ESP
83 EC 08 SUB ESP, 8
E9 YY YY YY YY JMP 0x00494CA3+5 ← 跳回原始函式繼續執行
五、MapleEzorsia-v2 的 Hook 命名模式
整個專案裡,Hook 的命名模式非常一致:
// 安裝 / 卸載函式
void Hook_sub_XXXXXX(bool install);
// Trampoline 指標(全域)
tFuncType original_sub_XXXXXX = (tFuncType)addys::sub_XXXXXX;
// Hook 本體
ReturnType [__fastcall / __cdecl] Hooked_sub_XXXXXX(params...);sub_XXXXXX 是 IDA Pro 自動產生的函式名稱,XXXXXX 是十六進位地址。旁邊的注解才是真實函式名稱(如 CClientSocket::Connect)。
六、常見問題
Q:
DetourTransactionBegin()失敗了怎麼辦?
DetourTransactionBegin()回傳NO_ERROR(0)表示成功,其他值表示失敗。實作中應該檢查回傳值,失敗時不要繼續呼叫DetourAttach。實際上很少失敗,除非系統記憶體嚴重不足。
Q:可以在同一個函式上安裝兩個 Hook 嗎?
可以,Detours 支援 Hook 鏈(Chain Hook)。第二個 Hook 的
original指標應指向第一個 Hook 安裝後的 Trampoline,這樣兩個 Hook 會依序執行。但這樣的設計會讓程式碼複雜度大幅上升,需要非常小心地管理卸載順序。
Q:Hook 安裝失敗後,
DetourTransactionCommit()還是要呼叫嗎?是的,
DetourTransactionAbort()才是取消事務的正確方式。如果DetourAttach失敗(例如找不到指定地址的函式開頭),應該呼叫DetourTransactionAbort()取消整個事務,而不是繼續Commit。
延伸閱讀
- 01_什麼是函式掛鉤(Hooking) — 本篇的前置概念
- 03_codecaves.h_Code_Cave技術實作 — 不依賴 Detours 的手動 Hook 方式
- 03_dinput8.cpp_Proxy載入器 — 另一種不使用 Detours 的 Hook:IAT 重導向
- 02_dllmain.cpp_DLL入口點 — 完整的 Hook 安裝呼叫清單