首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> C++ >> 關於C++

螢幕取字技術原理與實作:GDI Hook 攔截 TextOut/ExtTextOut

     編輯:關於C++

核心原理

螢幕文字繪製最終皆經由 GDI 函數:

  • TextOutA/W
  • ExtTextOutA/W

實現取字關鍵在於 攔截這些函數呼叫,取得傳入的字串參數。

三步驟實作流程

步驟 1:取得滑鼠座標下的視窗


// 安裝滑鼠 Hook 取得全域座標
HHOOK hMouseHook = SetWindowsHookEx(WH_MOUSE, MouseHookProc, hDll, 0);

// Hook 回呼中
POINT pt;
GetCursorPos(&pt);
HWND hTarget = WindowFromPoint(pt);  // 取得座標下視窗
ScreenToClient(hTarget, &pt);        // 轉為用戶區座標

步驟 2:強制目標視窗重繪觸發文字繪製


InvalidateRect(hTarget, NULL, TRUE);  // 標記整個用戶區無效
UpdateWindow(hTarget);                // 立即發送 WM_PAINT,觸發重繪

步驟 3:IAT Hook 攔截 GDI 文字函數

自訂攔截函數 (以 TextOutA 為例)


// 原始函數指標
FARPROC pfnTextOutA = GetProcAddress(GetModuleHandle("gdi32.dll"), "TextOutA");

// 自訂函數 (簽名必須完全一致)
BOOL WINAPI MyTextOutA(HDC hdc, int x, int y, LPCSTR lpString, int c) {
    // 1. 處理攔截到的字串 (如儲存、顯示、翻譯等)
    if (lpString && c > 0) {
        // 此處處理捕獲的文字...
    }
    // 2. 呼叫原始函數完成實際繪製
    return ((BOOL(WINAPI*)(HDC,int,int,LPCSTR,int))pfnTextOutA)(hdc, x, y, lpString, c);
}

掃描進程模組並修改 IAT


void HookTargetProcessModules() {
    MEMORY_BASIC_INFORMATION mbi;
    LPVOID base = 0;

    // 遍歷虛擬記憶體區域
    while (VirtualQuery(base, &mbi, sizeof(mbi))) {
        // 僅處理映射映像 (EXE/DLL)
        if (mbi.Type == MEM_IMAGE) {
            HookModuleIAT((HMODULE)mbi.BaseAddress);
        }
        base = (LPVOID)((DWORD_PTR)mbi.BaseAddress + mbi.RegionSize);
    }
}

BOOL HookModuleIAT(HMODULE hMod) {
    PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)hMod;
    if (pDos->e_magic != IMAGE_DOS_SIGNATURE) return FALSE;

    PIMAGE_NT_HEADERS pNT = (PIMAGE_NT_HEADERS)((DWORD_PTR)pDos + pDos->e_lfanew);
    if (pNT->Signature != IMAGE_NT_SIGNATURE) return FALSE;

    // 匯入表
    DWORD importRVA = pNT->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress;
    if (!importRVA) return FALSE;

    PIMAGE_IMPORT_DESCRIPTOR pImport = (PIMAGE_IMPORT_DESCRIPTOR)((DWORD_PTR)hMod + importRVA);
    while (pImport->Name) {
        CHAR* szDllName = (CHAR*)((DWORD_PTR)hMod + pImport->Name);
        // 僅處理 gdi32.dll 匯入
        if (lstrcmpiA(szDllName, "gdi32.dll") == 0) {
            PIMAGE_THUNK_DATA pThunk = (PIMAGE_THUNK_DATA)((DWORD_PTR)hMod + pImport->FirstThunk);
            while (pThunk->u1.Function) {
                if (pThunk->u1.Function == (DWORD_PTR)pfnTextOutA) {
                    // 修改保護並替換
                    DWORD oldProtect;
                    VirtualProtect(&pThunk->u1.Function, sizeof(DWORD_PTR),
                                   PAGE_READWRITE, &oldProtect);
                    pThunk->u1.Function = (DWORD_PTR)MyTextOutA;
                    VirtualProtect(&pThunk->u1.Function, sizeof(DWORD_PTR),
                                   oldProtect, &oldProtect);
                    return TRUE;
                }
                pThunk++;
            }
        }
        pImport++;
    }
    return FALSE;
}

關鍵技術點總結

技術點說明
為何用滑鼠 Hook系統自動將 Hook DLL 注入所有 GUI 進程,免去手動注入
VirtualQuery 遍歷找出進程載入的所有模組基地址,無需 Toolhelp32 API
IAT 修改範圍僅影響該模組對 gdi32.dll 的匯入,不影響其他模組或系統全域
Unicode 支援需同時 Hook TextOutW, ExtTextOutA, ExtTextOutW
效能考量每次文字繪製皆經攔截函數,複雜邏輯會拖慢繪製,建議極簡處理
現代替代方案UI Automation, OCR, DirectWrite 分析等更穩健

完整攔截目標清單


// 必須同時攔截以下四個函數以覆蓋 ANSI/Unicode
TextOutA, TextOutW, ExtTextOutA, ExtTextOutW

限制與風險

  • 僅適用 GDI 繪製文字:DirectWrite、GDI+、WPF、Chrome/Edge 等不經 GDI TextOut,無法攔截
  • 安全軟體攔截:修改 IAT 可能觸發防毒/EDR 警報
  • 64 位元相容:指標大小、指令編碼不同,需分別編譯
  • 系統更新風險:Windows 更新可能變更 gdi32.dll 內部結構
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved