Detours 函式庫使用方式

本篇定位

這是 Phase 4 的第 2 篇。假設你已讀過 01_什麼是函式掛鉤(Hooking),理解 Trampoline 和 Inline Hook 的概念。 本篇說明如何在 MapleEzorsia-v2 中使用 Microsoft Detours 函式庫,讓複雜的 Hook 安裝變成幾行程式碼。


一、什麼是 Detours?

Microsoft Detours 是微軟研究院開發、現已開源的 C++ 函式庫(github.com/microsoft/Detours)。它的核心功能:

  1. 把目標函式開頭改成 JMP(Detour)
  2. 自動建立 Trampoline,保留原始指令
  3. 提供乾淨的 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);
}

__thiscall vs __fastcall

原始函式是 __thiscallthis 放 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


延伸閱讀