什麼是函式掛鉤(Function Hooking)?
本篇定位
這是 Phase 4「Hook 技術與 Detours 函式庫」的第一篇,也是整個登入器技術核心的入口。 讀完這篇,你會理解:登入器修改遊戲行為的根本手段——不是修改 EXE 檔,而是在執行時期劫持函式的執行流程。
一、問題:我們想改遊戲行為,但沒有原始碼
MapleStory 是閉源軟體。我們拿到的只是編譯好的 MapleStory.exe,裡面全是 CPU 直接執行的機器碼,沒有 C++ 原始碼。
但我們想做的事包括:
- 讓遊戲連到私服 IP 而不是官方伺服器
- 修改遊戲解析度
- 繞過 CRC 完整性檢查
- 讓遊戲可以多開
直接修改 EXE 是一個選項,但有幾個問題:
- 遊戲每次更新就要重新 patch
- 修改後的 EXE 檔在某些情況下無法通過自身的完整性驗證
- 分發修改過的 EXE 涉及法律問題
Function Hooking 的解法:不修改 EXE,而是在執行時把函式的入口重新導向我們的程式碼。
二、核心概念:攔截 CPU 的執行路徑
正常執行時,遊戲呼叫自己的函式:
遊戲程式碼
│
▼
呼叫 CClientSocket::Connect(...)
│
▼
函式執行:連到官方 IP 106.52.xxx.xxx
安裝 Hook 之後:
遊戲程式碼
│
▼
呼叫 CClientSocket::Connect(...)
│
▼
【Hook 攔截】→ 跳轉到我們的函式
│
├─ 修改參數:把 IP 換成私服 127.0.0.1
│
▼
呼叫原始函式(帶著修改後的參數)
│
▼
函式執行:連到私服
遊戲完全不知道中間發生了什麼,它只是「呼叫 Connect」,但連線目標已經被我們換掉了。
三、Hook 的實作原理:改寫函式開頭的機器碼
在 x86 架構中,JMP rel32(無條件跳轉指令)是 5 個 byte:
E9 XX XX XX XX
│ └──────────── 跳轉距離(相對位移,4 byte)
└─────────────── JMP 指令的 opcode
安裝 Hook 的過程:
【安裝 Hook 前】
函式起始地址 0x00494CA3:
55 PUSH EBP ← 原始函式的開頭
8B EC MOV EBP, ESP
83 EC 08 SUB ESP, 8
...
【安裝 Hook 後】
函式起始地址 0x00494CA3:
E9 XX XX XX XX JMP 到我們的 Hook 函式 ← 改寫了
XX (原始指令的殘餘,可能已被截斷)
...
當遊戲執行到 0x00494CA3,CPU 看到的第一個指令是 JMP,立刻跳到我們的函式。
四、Hook 的三種主要型態
4.1 Inline Hook(改寫函式頭部)
最常見的方式,就是上面描述的:直接把函式開頭改成 JMP。
優點:通用,不依賴任何特殊機制
缺點:需要保存被覆蓋的原始指令,並在 Trampoline 中執行後再跳回
4.2 IAT Hook(Import Address Table Hook)
利用 PE(Windows 可執行檔)的匯入表:程式呼叫外部函式(如 Windows API)時,實際上是透過一張「地址表」間接跳轉的。
程式碼: CALL [IAT + offset_CreateMutexA]
│
▼
IAT 裡存的地址 → kernel32.dll 的 CreateMutexA
我們把 IAT 裡那個地址換成我們自己的函式:
程式碼: CALL [IAT + offset_CreateMutexA]
│
▼
IAT 裡存的地址(已被我們改掉)→ 我們的 HookCreateMutexA
MapleEzorsia-v2 對
CreateMutexA、WSPStartup等 Windows API 就是使用 IAT Hook,因為這些函式的呼叫點是透過 IAT 的,不需要改寫函式本體。
4.3 VTable Hook(虛函式表 Hook)
C++ 多型物件有一個 vftable(虛函式表),記錄每個虛函式的地址。把 vftable 裡某個位置換掉,就能攔截對應虛函式的呼叫。
五、Trampoline:保留原始行為的關鍵
如果我們把函式開頭 5 個 byte 全部覆蓋成 JMP,那被覆蓋的原始指令就消失了。
如果我們的 Hook 函式想在「做完自己的事之後,讓遊戲繼續正常執行」,就需要一個 Trampoline(跳板):
【Trampoline 的結構】
Trampoline Buffer(動態分配的一小塊記憶體):
55 PUSH EBP ← 被覆蓋的原始指令(複製過來)
8B EC MOV EBP, ESP ← 同上
E9 XX XX XX XX JMP 回原始函式 +5 ← 跳回原始函式的第 6 個 byte 繼續執行
整個執行流程:
遊戲呼叫原始函式
│
▼
執行到 JMP(我們裝的)
│
▼
跳入我們的 Hook 函式
│
├─ 執行我們的邏輯(修改參數、記錄日誌等)
│
▼
呼叫 Trampoline(讓原始函式繼續執行)
│
▼
Trampoline 執行被覆蓋的原始指令
│
▼
JMP 回原始函式 +5(跳過已被覆蓋的開頭,繼續執行函式本體)
│
▼
原始函式正常完成
Trampoline 就是一張「備份紙條」
在改寫函式開頭前,先把那幾個 byte 抄下來,放在安全的地方,配上一個「跳回去」的指令。之後我們的 Hook 函式可以呼叫這份備份,讓原始函式該做的事還是能繼續做。
六、Detours:讓這一切不那麼痛苦
手動實作 Inline Hook + Trampoline 非常繁瑣:
- 需要計算相對跳轉距離
- 需要分析函式開頭的指令長度(必須完整複製,不能截斷一條指令)
- 需要動態分配 Trampoline buffer
- 需要處理多執行緒競爭
Microsoft Detours 是一個函式庫,幫你處理上面所有細節。下一篇會詳細說明用法。
七、MapleEzorsia-v2 的 Hook 架構總覽
graph TD A[DllMain — DLL_PROCESS_ATTACH] -->|立刻安裝| B[Windows API Hook] B --> B1[Hook_CreateMutexA — IAT] B --> B2[Hook_WSPStartup — IAT] B --> B3[Hook_CreateWindowExA — IAT] B --> B4[Hook_FindFirstFileA — IAT] A -->|開新執行緒| C[MainMain::CreateInstance] C -->|等 Themida 解壓| D[MainFunc — 安裝遊戲內部 Hook] D -->|Inline Hook| E[遊戲函式 Hook] E --> E1[Hook_sub_494CA3 — CClientSocket::Connect] E --> E2[Hook_sub_9F4E54 — CRC32 檢查] E --> E3[Hook_sub_9F5239 — CWvsApp::SetUp] E --> E4[Hook_sub_9F5C50 — CWvsApp::Run] E --> E5[... 其他 Hook ...]
八、常見問題
Q:Hook 之後遊戲的 CRC 不是會偵測到我們的修改嗎?
很好的問題。這就是為什麼其中一個 Hook 是針對 CRC32 檢查函式本身。我們先把 CRC 檢查的函式 Hook 掉,讓它永遠回傳「沒問題」,這樣後續的所有 Hook 修改就不會被偵測到。Hook 有安裝順序上的考量。
Q:Hook 和修改 EXE 有什麼本質差別?
修改 EXE 是改磁碟上的檔案(靜態修改),每次啟動都帶著修改。Hook 是在執行時期改記憶體(動態修改),遊戲關閉後記憶體釋放,原始 EXE 完好無損。下次啟動遊戲時必須重新安裝 Hook(由 DLL 自動完成)。
Q:如果 Hook 函式本身有 bug 會怎樣?
因為 Hook 函式在遊戲進程裡執行,任何崩潰(未處理的例外、堆疊溢位)都會直接讓遊戲崩潰。Hook 函式必須非常穩定,盡量避免複雜邏輯,並做好錯誤處理。
延伸閱讀
- 02_Detours函式庫使用方式 — 下一篇:用 Detours 實作 Inline Hook
- 03_codecaves.h_Code_Cave技術實作 — 手動 Hook 的進階變體
- 02_Memory.cpp_讀寫記憶體工具 — Hook 安裝依賴 WriteProtected 函式
- 02_dllmain.cpp_DLL入口點 — Hook 在 DllMain 和 MainFunc 中的安裝時機