dinput8 代理 DLL 原理

前言:這篇在說什麼?

當你執行 MapleStory,遊戲會載入一堆 Windows 系統提供的工具檔案(就是 .dll 檔)。
我們的登入器核心技術,就是把其中一個工具檔案「偷偷換成自己做的版本」,讓遊戲在載入時順便執行我們的程式碼。

這個技術叫做 Proxy DLL(代理 DLL),而我們選的那個工具檔案叫做 dinput8.dll


1. 什麼是 Proxy DLL(代理 DLL)?

郵局中繼站比喻

用郵局比喻理解 Proxy

想像你開了一間假郵局,地址跟真郵局一樣掛在路口。

  • 所有寄給「dinput8 郵局」的信,都會先送到你的假郵局
  • 你看完信之後,原封不動轉發給真正的郵局
  • 但在轉發之前,你可以偷看信件、蓋章、甚至塞一張紙條進去

MapleStory 就是那個寄信的人,它不知道中間有一個假郵局。

技術上怎麼做到的?

  1. 我們把自己編譯的 dinput8.dll 放進遊戲資料夾
  2. Windows 有一個載入規則:同一資料夾的 DLL 優先於系統資料夾
  3. 所以 MapleStory 啟動時,會先載入我們的假 dinput8.dll
  4. 我們的 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);
    }
}

三個步驟記憶法

  1. 問路GetSystemDirectoryA)→ 找到系統目錄
  2. 開門LoadLibraryA)→ 把真 DLL 載入記憶體
  3. 記地址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 時,依序搜尋以下位置:

  1. 程式所在資料夾(我們的假 DLL 放這裡)
  2. System32 目錄
  3. 其他系統目錄…

因為遊戲資料夾排第一,我們的假 DLL 一定先被找到。這不是 bug,是 Windows 的設計。


延伸閱讀