無論是有道、金山詞霸,還是瀏覽器內建翻譯,「鼠標劃詞/懸停取詞」早已成為標配。但這看似簡單的功能,在 Windows 底層實現卻暗藏玄機。本文將系統梳理兩大主流技術路線——GDI API Hook 與 設備上下文 (DC) 鏡像追蹤——的原理、優劣與工程落地細節,並重點講解現代主流的 IAT (Import Address Table) 重定向 Hook 實現全流程。
| 維度 | GDI API Hook (攔截 TextOut 等) | DC 鏡像追蹤 (複製/監控每個 DC) |
|---|---|---|
| 實現複雜度 | 中等:需 Hook 多個 GDI 導出函數 | 極高:需管理全系統 DC 生命週期、繪圖指令重放 |
| 兼容性 | 優:僅依賴標準 API 入口 | 差:易受驅動層、DirectX、GPU 加速繞過 GDI 影響 |
| 取詞精度 | 高:直接獲取字符、字體、位置、顏色 | 中:需自行解析繪圖指令流,錯誤率高 |
| 性能開銷 | 低:僅在文字繪製時觸發 | 高:所有繪圖操作均需記錄/重放 |
| 適用場景 | 傳統 GDI 應用、Office、瀏覽器早期版本 | 早期嘗試、特殊繪圖引擎研究 |
結論:業界主流均採用 GDI API Hook + IAT 重定向 方案,兼顧穩定性與精度。
JMP 指令跳轉到自定義處理函數。VirtualProtect 修改頁屬性 (PAGE_READWRITE)、VirtualAllocEx 跨進程分配記憶體。OpenProcess (PROCESS_VM_READ/WRITE/OPERATION)、WriteProcessMemory、CreateRemoteThread 注入 DLL。DllMain (DLL_PROCESS_ATTACH) 執行 Hook 初始化。// 1. 遍歷目標模組的導入表,定位特定 DLL (如 GDI32.dll) 的特定函數 (如 TextOutW)
BOOL HookIAT(HMODULE hCallerMod, LPCSTR lpTargetDll, LPCSTR lpApiName, PROC pNewFunc, PROC* ppOldFunc) {
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)hCallerMod;
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)((BYTE*)hCallerMod + pDos->e_lfanew);
// 導入表 RVA -> VA
PIMAGE_IMPORT_DESCRIPTOR pImportDesc = (PIMAGE_IMPORT_DESCRIPTOR)
((BYTE*)hCallerMod + pNt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);
for (; pImportDesc->Name; pImportDesc++) {
LPCSTR dllName = (LPCSTR)((BYTE*)hCallerMod + pImportDesc->Name);
if (_stricmp(dllName, lpTargetDll) != 0) continue; // 找到目標 DLL
// 遍歷 INT (OriginalFirstThunk) 與 IAT (FirstThunk)
PIMAGE_THUNK_DATA pInt = (PIMAGE_THUNK_DATA)((BYTE*)hCallerMod + pImportDesc->OriginalFirstThunk);
PIMAGE_THUNK_DATA pIat = (PIMAGE_THUNK_DATA)((BYTE*)hCallerMod + pImportDesc->FirstThunk);
for (; pInt->u1.AddressOfData; pInt++, pIat++) {
if (IMAGE_SNAP_BY_ORDINAL(pInt->u1.Ordinal)) continue; // 按序號導入,略過
PIMAGE_IMPORT_BY_NAME pImportName = (PIMAGE_IMPORT_BY_NAME)((BYTE*)hCallerMod + pInt->u1.AddressOfData);
if (strcmp((char*)pImportName->Name, lpApiName) == 0) {
// 找到目標 API,修改 IAT 項
DWORD oldProtect;
VirtualProtect(&pIat->u1.Function, sizeof(ULONG_PTR), PAGE_READWRITE, &oldProtect);
*ppOldFunc = (PROC)pIat->u1.Function; // 保存原地址
pIat->u1.Function = (ULONG_PTR)pNewFunc; // 寫入新地址
VirtualProtect(&pIat->u1.Function, sizeof(ULONG_PTR), oldProtect, &oldProtect);
return TRUE;
}
}
}
return FALSE;
}
// 2. Hook 處理函數範例:攔截 TextOutW 記錄文字內容與位置
BOOL WINAPI MyTextOutW(HDC hdc, int x, int y, LPCWSTR lpString, int c) {
// 1. 獲取字符串、字體、顏色、矩形區域等上下文
// 2. 發送給主程式 (IPC: 共享記憶體/管道/WM_COPYDATA) 供取詞引擎使用
// 3. 調用原始 API 保證正常繪製
return g_pfnOldTextOutW(hdc, x, y, lpString, c);
}
// 3. DLL 入口初始化
BOOL APIENTRY DllMain(HMODULE hMod, DWORD reason, LPVOID) {
if (reason == DLL_PROCESS_ATTACH) {
DisableThreadLibraryCalls(hMod);
// Hook 當前進程的 user32.dll!TextOutW / gdi32.dll!ExtTextOutW 等
HookIAT(GetModuleHandle(NULL), "GDI32.dll", "TextOutW", (PROC)MyTextOutW, (PROC*)&g_pfnOldTextOutW);
HookIAT(GetModuleHandle(NULL), "GDI32.dll", "ExtTextOutW", (PROC)MyExtTextOutW, (PROC*)&g_pfnOldExtTextOutW);
// ... 同理 Hook DrawTextW, PolyTextOutW 等
}
return TRUE;
}
SetWindowsHookEx(WH_GETMESSAGE/WH_CBT...) + DLL;指定進程用 CreateRemoteThread + LoadLibrary 或 AppInit_DLLs (需簽名) / Image File Execution Options (IFEO)。CreateFileMapping + MapViewOfFile) + 無鎖環形隊列 傳遞文字事件,主程式輪詢處理,避免 Hook 內阻塞導致 UI 卡頓。從早期的「改寫內存代碼」到成熟的「IAT 重定向」,鼠標取詞技術見證了 Windows 應用生態從 GDI 到 DirectWrite、從單進程到多進程沙箱的演變。掌握 IAT Hook 不僅能實現取詞,更是理解 Windows 應用間攔截、熱補丁、安全防護 (EDR/HIPS) 核心原理的入門鑰匙。