dinput8 代理 DLL 原理
前言:這篇在說什麼?
當你執行 MapleStory,遊戲會載入一堆 Windows 系統提供的工具檔案(就是 .dll 檔)。
我們的登入器核心技術,就是把其中一個工具檔案「偷偷換成自己做的版本」,讓遊戲在載入時順便執行我們的程式碼。
這個技術叫做 Proxy DLL(代理 DLL),而我們選的那個工具檔案叫做 dinput8.dll。
1. 什麼是 Proxy DLL(代理 DLL)?
郵局中繼站比喻
用郵局比喻理解 Proxy
想像你開了一間假郵局,地址跟真郵局一樣掛在路口。
- 所有寄給「dinput8 郵局」的信,都會先送到你的假郵局
- 你看完信之後,原封不動轉發給真正的郵局
- 但在轉發之前,你可以偷看信件、蓋章、甚至塞一張紙條進去
MapleStory 就是那個寄信的人,它不知道中間有一個假郵局。
技術上怎麼做到的?
- 我們把自己編譯的
dinput8.dll放進遊戲資料夾 - Windows 有一個載入規則:同一資料夾的 DLL 優先於系統資料夾
- 所以 MapleStory 啟動時,會先載入我們的假
dinput8.dll - 我們的 DLL 執行完注入程式碼後,把所有呼叫轉發給真正的系統 dinput8.dll
替換,不是覆蓋
我們並沒有修改
C:\Windows\System32\dinput8.dll(那是系統檔案,亂動會讓 Windows 壞掉)。 我們只是在遊戲資料夾裡放一個「同名的假貨」。
2. DLL Export 是什麼?
門牌比喻
一個 DLL 就像一棟大樓,裡面有很多功能(函式)住在不同房間。
Export(匯出) 就是在大樓門口掛門牌,讓外人知道「這棟樓裡有哪些服務」。
如果你的假 DLL 沒有掛和真 DLL 一樣的門牌,MapleStory 就會說:
找不到 DirectInput8Create!程式即將關閉。
關鍵規則
我們的假
dinput8.dll必須匯出(export)完全相同的函式名稱,才能騙過 MapleStory。真正的
dinput8.dll有兩個匯出函式:
DirectInput8Create(建立輸入裝置)GetdfDIJoystick(取得搖桿資料格式)我們的假 DLL 也必須有這兩個名稱。
3. FARPROC 是什麼?(函式指標)
在程式碼最頂端有這兩行:
FARPROC DirectInput8Create_Proc;
FARPROC GetdfDIJoystick_Proc;用導航座標比喻
FARPROC是一種特殊的記憶體地址容器,專門用來記住「某個函式住在記憶體的哪個位置」。你可以想成是 GPS 座標:
- 還沒載入真 DLL 之前,座標是空的(
NULL)CreateHook()執行完之後,座標就會指向真正系統 DLL 裡的函式位置- 之後每次 MapleStory 呼叫我們的假函式,我們就用這個座標跳過去
4. 逐行解析 CreateHook()
這是整個代理 DLL 的初始化核心,只需要執行一次。
void dinput8::CreateHook() {
// 宣告一個字元陣列,用來存放系統目錄的路徑文字
// MAX_PATH 是 Windows 定義的常數,代表路徑最長 260 個字元
char szPath[MAX_PATH];
// GetSystemDirectoryA:詢問 Windows「你的 System32 在哪裡?」
// 答案通常是 C:\Windows\System32
// 如果成功,把路徑存到 szPath 裡
if (GetSystemDirectoryA(szPath, sizeof(szPath))) {
// strcat:字串拼接,在路徑後面加上 \dinput8.dll
// szPath 現在變成 C:\Windows\System32\dinput8.dll
strcat(szPath, "\\dinput8.dll");
} else {
// 萬一連系統目錄都找不到(極罕見),就暫停執行並跳出錯誤視窗
Sleep(20);
SuspendThread(MainMain::mainTHread);
MessageBox(NULL, L"Failed to load original dinput8.dll from system location...",
L"systems directory inaccessible", 0);
ExitProcess(0); // 強制結束程式
}
// LoadLibraryA:叫 Windows 把真正的 dinput8.dll 載入到記憶體
// 回傳值 hModule 是一個「模組代號」,代表這個 DLL 在記憶體中的位置
HMODULE hModule = LoadLibraryA(szPath);
if (hModule) {
// GetProcAddress:在已載入的 DLL 裡,查詢指定函式的記憶體地址
// 找到後存入我們的 FARPROC 全域變數,之後 jmp 指令會跳過去
DirectInput8Create_Proc = GetProcAddress(hModule, "DirectInput8Create");
GetdfDIJoystick_Proc = GetProcAddress(hModule, "GetdfDIJoystick");
} else {
// 找不到真正的 dinput8.dll,同樣跳出錯誤後結束
Sleep(20);
SuspendThread(MainMain::mainTHread);
MessageBox(NULL, L"Failed to find original dinput8.dll...", L"Missing file", 0);
ExitProcess(0);
}
}三個步驟記憶法
- 問路(
GetSystemDirectoryA)→ 找到系統目錄- 開門(
LoadLibraryA)→ 把真 DLL 載入記憶體- 記地址(
GetProcAddress)→ 把真函式的位置存起來
5. __declspec(naked) + __asm jmp:裸函式直接跳轉
// extern "C":告訴編譯器,匯出的函式名稱不要加 C++ 裝飾(保持純英文原名)
// __declspec(dllexport):把這個函式「掛門牌」,對外公開
// __declspec(naked):「裸函式」——編譯器不替這個函式加任何準備或收尾程式碼
extern "C" __declspec(dllexport) __declspec(naked) void DirectInput8Create() {
// __asm:直接寫 x86 組合語言指令
// jmp dword ptr[DirectInput8Create_Proc]:
// 讀取 DirectInput8Create_Proc 這個地址,然後「跳過去」執行
// 整個函式就只有這一行,沒有任何其他動作
__asm jmp dword ptr[DirectInput8Create_Proc]
}
extern "C" __declspec(dllexport) __declspec(naked) void GetdfDIJoystick() {
__asm jmp dword ptr[GetdfDIJoystick_Proc]
}為什麼用 naked(裸函式)?
裸函式的意義
正常的函式執行前,C++ 編譯器會自動插入幾行「準備動作」(建立堆疊框架等),執行後也有「收尾動作」。
但我們的假函式什麼都不想做,只想把控制權整個丟給真函式。如果加了那些準備/收尾程式碼,真函式的堆疊就會亂掉,導致 MapleStory 崩潰。
用
naked加上jmp組合語言,就像一個空房間裡只有一扇直通門——進來就直接出去,不留任何痕跡。
6. 整個流程:從 MapleStory 到真函式
sequenceDiagram participant MS as MapleStory.exe participant FD as 假 dinput8.dll<br/>(遊戲資料夾) participant RD as 真 dinput8.dll<br/>(System32) participant HC as CreateHook()<br/>(初始化階段) Note over MS,HC: ── 啟動階段 ── MS->>FD: 載入 dinput8.dll(優先找遊戲資料夾) FD->>HC: DLL 載入時自動執行 CreateHook() HC->>RD: LoadLibraryA 載入真 dinput8.dll RD-->>HC: 傳回模組代號 hModule HC->>RD: GetProcAddress 查詢函式地址 RD-->>HC: 傳回 DirectInput8Create_Proc 地址 Note over HC: 地址記好了,初始化完成 Note over MS,RD: ── 遊戲執行中 ── MS->>FD: 呼叫 DirectInput8Create(...) FD->>RD: __asm jmp → 跳到真函式 RD-->>MS: 真函式執行完,結果回傳給 MapleStory
關鍵理解:我們的修改碼「更早」執行
你可能會問:「這樣不就只是個轉發器?那我們的 hack 碼什麼時候執行?」
答案是:在
CreateHook()執行的同時,同一個 DLL 裡還有其他程式碼負責注入 hack(例如MainMain類別)。
dinput8.cpp的工作只有一個:讓 MapleStory 不知道它載入了假 DLL。 注入的魔法發生在其他地方,dinput8只是那扇「進場的門」。
7. 為什麼選 dinput8.dll?
不是隨便哪個 DLL 都能做代理,dinput8.dll 有幾個天然優勢:
| 條件 | dinput8.dll 符合嗎? | 說明 |
|---|---|---|
| MapleStory 一定會載入它 | ✅ | 遊戲需要鍵盤輸入,一定載入 DirectInput |
| 原本不在遊戲資料夾裡 | ✅ | 它住在 System32,遊戲資料夾沒有它,我們才能放假的 |
| 匯出函式數量少 | ✅ | 只需要假裝 2 個函式,工作量最小 |
| 不被防毒誤報 | ✅(相對) | 比其他常見注入目標低調 |
Windows DLL 搜尋順序
Windows 載入 DLL 時,依序搜尋以下位置:
- 程式所在資料夾(我們的假 DLL 放這裡)
- System32 目錄
- 其他系統目錄…
因為遊戲資料夾排第一,我們的假 DLL 一定先被找到。這不是 bug,是 Windows 的設計。
延伸閱讀
- 01_什麼是DLL與DLL劫持 — 本系列第一篇,了解 DLL 和劫持的基本原理
- 02_MapleStory_v83客戶端架構概覽 — 了解整個登入器為何要攔截 dinput8
- 02_dllmain.cpp_DLL入口點 — Phase 2:DLL 如何啟動,CreateHook 在哪裡被呼叫
- 03_MainMain.cpp_初始化流程解析 — Phase 6:MainMain 執行緒與等待機制
- 01_什麼是記憶體位址與指標 — Phase 3:FARPROC、函式指標的深入說明