「滑鼠螢幕取字」廣泛應用於電子字典(如四通利方、金山詞霸),使用者將滑鼠指向螢幕文字即可即時翻譯。看似簡單,在 Windows 系統下實作卻相當複雜,主要有兩種路徑:
TextOut、TextOutA 等繪圖 API 呼叫。本文專注於第一種——API Hooking,此技術威力遠超想像,甚至可改造整個作業系統行為,許多外掛式中文平台即基於此原理。
要攔截其他進程的 API 呼叫,必須打破進程邊界,將 Hook 程式碼注入目標進程。此工作交由 Hook 函數 SetWindowsHookEx 完成。系統 Hook 函數必須置於 DLL 中,當進程載入該 DLL 時,DLL 程式碼即成為進程一部分,以該進程身分執行,使用其堆疊。
EXE/DLL 映射到虛擬記憶體時,保持磁碟上 PE (Portable Executable) 格式結構。關鍵在 .idata 區段,包含所有匯入函數資訊與 IAT (Import Address Table) 的 RVA (Relative Virtual Address)。
進程對外部 DLL 函數的呼叫,實際上是跳轉到自身 .idata 區段的 IAT,由 IAT 再跳轉到目標 DLL 真實入口。因此,修改 IAT 中對應函數的地址為自訂函數位址,即可實現攔截。
GetModuleHandle 或 AfxGetInstanceHandle)。IMAGE_IMPORT_DESCRIPTOR 陣列,尋找目標 DLL(如 gdi32.dll)。IMAGE_THUNK_DATA 陣列(OriginalFirstThunk 指向原始匯入名稱表,FirstThunk 指向已填入實際位址的 IAT)遍歷匯入函數。TextOutA),找到後修改 FirstThunk 對應項目的函數位址為自訂函數位址。VirtualProtect 變更記憶體保護屬性為可寫,修改完恢復原保護屬性。SetWindowsHookEx 取得滑鼠訊息。WindowFromPoint、ScreenToClient、InvalidateRect 發送重繪訊息,誘使目標視窗呼叫繪圖 API。TextOutA 取得字串參數:在 Hook DLL 中實作 MyTextOutA,處理攔截到的字串,再呼叫原版 TextOutA 繪製。// 自訂 TextOutA
BOOL WINAPI MyTextOutA(HDC hdc, int x, int y, LPCSTR lpString, int c)
{
// 這裡處理 lpString(取字邏輯)
return RealTextOutA(hdc, x, y, lpString, c); // 呼叫原函數
}
// Hook 設定
HOOKFUNCDESC hd;
PROC pOrigFunc;
hd.szFunc = "TextOutA";
hd.pProc = (PROC)MyTextOutA;
HookImportFunction(AfxGetInstanceHandle(), "gdi32.dll", &hd, &pOrigFunc);
BOOL HookImportFunction(HMODULE hModule, LPCSTR szImportModule,
LPHOOKFUNCDESC pHookFunc, PROC* pOrigFuncs)
{
// 參數驗證、檢查模組基地址是否在共享區(Win9x)等略...
PIMAGE_IMPORT_DESCRIPTOR pImportDesc = GetNamedImportDescriptor(hModule, szImportModule);
if (!pImportDesc) return FALSE; // 目標 DLL 未被匯入
// 取得原始 Thunk (匯入名稱表) 與實際 Thunk (IAT)
PIMAGE_THUNK_DATA pOrigThunk = MAKE_PTR(PIMAGE_THUNK_DATA, hModule, pImportDesc->OriginalFirstThunk);
PIMAGE_THUNK_DATA pRealThunk = MAKE_PTR(PIMAGE_THUNK_DATA, hModule, pImportDesc->FirstThunk);
while (pOrigThunk->u1.Function) {
// 僅處理按名稱匯入的函數
if (!(pOrigThunk->u1.Ordinal & IMAGE_ORDINAL_FLAG)) {
PIMAGE_IMPORT_BY_NAME pByName = MAKE_PTR(PIMAGE_IMPORT_BY_NAME, hModule, pOrigThunk->u1.AddressOfData);
if (pByName->Name[0] == '\0') { pOrigThunk++; pRealThunk++; continue; }
// 比對目標函數名
if (stricmp(pHookFunc->szFunc, (char*)pByName->Name) == 0) {
// 修改記憶體保護
MEMORY_BASIC_INFORMATION mbi;
VirtualQuery(pRealThunk, &mbi, sizeof(mbi));
VirtualProtect(mbi.BaseAddress, mbi.RegionSize, PAGE_READWRITE, &mbi.Protect);
// 保存原函數位址
if (pOrigFuncs) *pOrigFuncs = (PROC)pRealThunk->u1.Function;
// 替換為自訂函數位址
pRealThunk->u1.Function = (DWORD)pHookFunc->pProc;
// 恢復保護
DWORD dwOld;
VirtualProtect(mbi.BaseAddress, mbi.RegionSize, mbi.Protect, &dwOld);
return TRUE;
}
}
pOrigThunk++; pRealThunk++;
}
return FALSE;
}
輔助函數 GetNamedImportDescriptor 解析 PE 標頭定位目標 DLL 的 Import Descriptor,IsNT 判斷是否 NT 核心系統。
API Hooking 是 Windows 系統程式設計的核心技術之一,掌握 IAT 修改法可實現強大的系統擴充功能。本文從虛擬記憶體、PE 格式、Hook 原理到完整實作代碼逐層剖析,希望能幫助開發者攻克這項充滿挑戰的技術。