Win9x 時代可透過 VxD、LDT、IDT 進入 Ring 0 修改記憶體,但 Win2K (NT 核心) 禁止此類操作。幸好 Win2K 提供強大的使用者模式記憶體操作 API:
VirtualProtectEx:修改目標進程記憶體頁面保護屬性WriteProcessMemory:寫入目標進程記憶體ReadProcessMemory:讀取目標進程記憶體配合全域 Hook 將 DLL 注入目標進程,即可在使用者模式實現 API 攔截。
BOOL VirtualProtectEx(HANDLE hProcess, LPVOID lpAddress, DWORD dwSize,
DWORD flNewProtect, PDWORD lpflOldProtect);
BOOL WriteProcessMemory(HANDLE hProcess, LPVOID lpBaseAddress,
LPCVOID lpBuffer, DWORD nSize, LPDWORD lpNumberOfBytesWritten);
BOOL ReadProcessMemory(HANDLE hProcess, LPCVOID lpBaseAddress,
LPVOID lpBuffer, DWORD nSize, LPDWORD lpNumberOfBytesRead);
HHOOK g_hHook;
HINSTANCE g_hInstDll;
FARPROC fpMessageBoxA;
HMODULE hUser32;
BYTE OldCode[5], NewCode[5];
DWORD dwOldProtect, dwNewProtect;
BOOL bHooked = FALSE;
struct HOOK_API {
LPCSTR szFunc; // 目標函數名
PROC pNewProc; // 新函數指標
PROC pOldProc; // 原函數指標
};
LRESULT WINAPI EmptyHook(int nCode, WPARAM wParam, LPARAM lParam) {
return CallNextHookEx(g_hHook, nCode, wParam, lParam);
}
extern "C" __declspec(dllexport) BOOL __stdcall InstallHook() {
g_hInstDll = LoadLibrary("HookDll.dll"); // 自身 DLL 檔名
g_hHook = SetWindowsHookEx(WH_GETMESSAGE, EmptyHook, g_hInstDll, 0);
return g_hHook != NULL;
}
extern "C" __declspec(dllexport) BOOL __stdcall UninstallHook() {
return UnhookWindowsHookEx(g_hHook);
}
BOOL Init() {
hUser32 = LoadLibrary("user32.dll");
fpMessageBoxA = GetProcAddress(hUser32, "MessageBoxA");
if (!fpMessageBoxA) return FALSE;
// 1. 備份原始 5 字節 (JMP 指令長度)
memcpy(OldCode, fpMessageBoxA, 5);
// 2. 構造 JMP MyMessageBoxA (0xE9 + 相對位移 4 字節)
NewCode[0] = 0xE9;
DWORD_PTR target = (DWORD_PTR)MyMessageBoxA;
DWORD_PTR src = (DWORD_PTR)fpMessageBoxA;
*(DWORD*)&NewCode[1] = target - src - 5; // 相對位移
HookOn();
return TRUE;
}
int WINAPI MyMessageBoxA(HWND hWnd, LPCTSTR lpText, LPCTSTR lpCaption, UINT uType) {
HookOff(); // 必須先關閉 Hook,否則遞迴呼叫導致死循環
int ret = MessageBoxA(hWnd, "來自 Hook 的內容", lpCaption, MB_OK | MB_ICONINFORMATION);
HookOn();
return ret;
}
void HookOn() {
HANDLE hProc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, GetCurrentProcessId());
VirtualProtectEx(hProc, fpMessageBoxA, 5, PAGE_READWRITE, &dwOldProtect);
WriteProcessMemory(hProc, fpMessageBoxA, NewCode, 5, NULL);
VirtualProtectEx(hProc, fpMessageBoxA, 5, dwOldProtect, &dwOldProtect);
bHooked = TRUE;
CloseHandle(hProc);
}
void HookOff() {
HANDLE hProc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, GetCurrentProcessId());
VirtualProtectEx(hProc, fpMessageBoxA, 5, PAGE_READWRITE, &dwOldProtect);
WriteProcessMemory(hProc, fpMessageBoxA, OldCode, 5, NULL);
VirtualProtectEx(hProc, fpMessageBoxA, 5, dwOldProtect, &dwOldProtect);
bHooked = FALSE;
CloseHandle(hProc);
}
BOOL WINAPI DllEntryPoint(HINSTANCE hinst, DWORD reason, LPVOID) {
switch (reason) {
case DLL_PROCESS_ATTACH:
if (!Init()) MessageBoxA(NULL, "Init Failed", "Error", MB_OK);
break;
case DLL_PROCESS_DETACH:
if (bHooked) UninstallHook();
break;
}
return TRUE;
}
extern "C" __declspec(dllimport) BOOL __stdcall InstallHook();
extern "C" __declspec(dllimport) BOOL __stdcall UninstallHook();
void __fastcall TForm1::Button1Click(TObject *Sender) {
if (InstallHook()) {
MessageBoxA(NULL, "原始內容", "標題", MB_OK); // 實際顯示 "來自 Hook 的內容"
UninstallHook();
}
}
| 技術點 | 說明 |
|---|---|
| 為何用 WH_GETMESSAGE | 郵件 Hook 會將 DLL 注入所有擁有訊息佇列的 GUI 進程 |
| 5 字節 JMP | x86 近程相對跳轉指令固定 5 字節 (0xE9 + 32bit 位移) |
| VirtualProtectEx | 修改目標進程記憶體保護,需 PROCESS_VM_OPERATION 權限 |
| OpenProcess 權限 | PROCESS_ALL_ACCESS = VM_OPERATION | VM_READ | VM_WRITE |
| HookOn/HookOff 配對 | 自訂函數內呼叫原 API 時必須先 HookOff,避免無限遞迴 |
| 64 位元限制 | 此技術僅適用 32 位元進程;x64 需不同指令編碼與更複雜的注入方式 |