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

C++ Builder 全域鍵盤 Hook 實作:WH_JOURNALRECORD 低層級記錄與 DLL 注入

     編輯:關於C++

專案目標:跨進程攔截全域鍵盤事件

實作一個鍵盤監聽器,能在背景捕獲所有進程的按鍵釋放事件 (WM_KEYUP),並透過自訂訊息通知主視窗顯示虛擬鍵碼。採用 WH_JOURNALRECORD Hook 類型,搭配 DLL 注入達成全域監聽。

為什麼選擇 WH_JOURNALRECORD?

Hook 類型用途是否需 DLL權限需求
WH_KEYBOARD攔截鍵盤訊息 (WM_KEYDOWN/UP)是 (全域時)標準
WH_KEYBOARD_LL低層級鍵盤 Hook (原始輸入)否 (可在 EXE)需 UI 權限、Vista+ 需簽署/提權
WH_JOURNALRECORD記錄輸入事件 (滑鼠/鍵盤) 供回放是標準

WH_JOURNALRECORD 歷史用於巨集錄製,能捕獲 EVENTMSG 結構 (含訊息、參數、時間、座標)。雖然屬「記錄」性質,但同樣能攔截全域鍵盤事件,且不需低層級權限,適合教學與舊版相容專案。

架構概覽

  • Project1.dll:包含 DllEntryPoint、SetHook 匯出函式、HookProc 回呼。安裝 WH_JOURNALRECORD,收到 HC_ACTION + WM_KEYUP 時 PostMessage 通知主視窗。
  • 主程式 (EXE):LoadLibrary 載入 DLL,GetProcAddress 取得 SetHook,呼叫安裝/移除。視窗程序 WndProc (或 TApplicationEvents) 處理自訂訊息顯示按鍵碼。

DLL 完整原始碼 (Project1.dll)


// Project1.cpp - DLL 專案
#include 
#include 
#pragma hdrstop

// 共用常數
const UINT MY_HOOK_MSG = 2000;   // 自訂通知訊息
const LPARAM MY_HOOK_LPARAM = 1; // 識別標記

// 全域變數 (每個進程各自一份,但 HookProc 執行於注入進程中)
static HINSTANCE g_hInst = NULL;
static HWND      g_hTargetWnd = NULL; // 主程式視窗句柄
static HHOOK     g_hHook = NULL;

// 標準 DLL 入口點
int WINAPI DllEntryPoint(HINSTANCE hinst, unsigned long reason, void*) {
  if (reason == DLL_PROCESS_ATTACH) g_hInst = hinst;
  return 1;
}

// 匯出函式:安裝/移除 Hook
extern "C" __declspec(dllexport) void __stdcall SetHook(HWND hTargetWnd, BOOL bInstall) {
  if (bInstall) {
    g_hTargetWnd = hTargetWnd;
    // WH_JOURNALRECORD: nCode=HC_ACTION 時 lParam 指向 EVENTMSG
    g_hHook = SetWindowsHookEx(WH_JOURNALRECORD, (HOOKPROC)HookProc, g_hInst, 0);
  } else {
    if (g_hHook) { UnhookWindowsHookEx(g_hHook); g_hHook = NULL; }
  }
}

// Hook 回呼函式
LRESULT CALLBACK HookProc(int nCode, WPARAM wParam, LPARAM lParam) {
  if (nCode == HC_ACTION) {
    EVENTMSG* pMsg = (EVENTMSG*)lParam;
    if (pMsg->message == WM_KEYUP) {
      // pMsg->paramL 低位元組為虛擬鍵碼
      BYTE vk = LOBYTE(pMsg->paramL);
      if (g_hTargetWnd)
        PostMessage(g_hTargetWnd, MY_HOOK_MSG, vk, MY_HOOK_LPARAM);
    }
  }
  return CallNextHookEx(g_hHook, nCode, wParam, lParam);
}

主程式完整原始碼


// Main.cpp - EXE 專案
#include 
#pragma hdrstop
#include "Main.h"
//---------------------------------------------------------------------------
#pragma package(smart_init)
#pragma resource "*.dfm"
TForm1 *Form1;

// DLL 函式指標型別
typedef void (__stdcall *SET_HOOK_PROC)(HWND, BOOL);

static HINSTANCE g_hDll = NULL;
static SET_HOOK_PROC g_pSetHook = NULL;
const UINT MY_HOOK_MSG = 2000;
const LPARAM MY_HOOK_LPARAM = 1;

//---------------------------------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner) : TForm(Owner) {}

// 表單建立:載入 DLL、安裝 Hook
void __fastcall TForm1::FormCreate(TObject *Sender) {
  g_hDll = LoadLibrary("Project1.dll");
  if (!g_hDll) { ShowMessage("無法載入 Project1.dll"); Application->Terminate(); return; }

  g_pSetHook = (SET_HOOK_PROC)GetProcAddress(g_hDll, "SetHook");
  if (!g_pSetHook) { ShowMessage("找不到 SetHook 函式"); FreeLibrary(g_hDll); Application->Terminate(); return; }

  g_pSetHook(Handle, TRUE); // 安裝 Hook
}

// 表單關閉:移除 Hook、卸載 DLL
void __fastcall TForm1::FormClose(TObject *Sender, TCloseAction &Action) {
  if (g_pSetHook) g_pSetHook(NULL, FALSE);
  if (g_hDll) FreeLibrary(g_hDll);
}

// 使用 TApplicationEvents 元件處理自訂訊息 (OnMessage 事件)
void __fastcall TForm1::ApplicationEvents1Message(tagMSG &Msg, bool &Handled) {
  if (Msg.message == MY_HOOK_MSG && Msg.lParam == MY_HOOK_LPARAM) {
    ShowMessage("收到 Hook 按鍵訊息!\n\n虛擬鍵碼: " + IntToStr(Msg.wParam));
    Handled = true;
  }
}

關鍵技術細節解析

1. EVENTMSG 結構與參數解讀

typedef struct tagEVENTMSG {
  UINT  message;   // 訊息代碼 (WM_KEYDOWN, WM_KEYUP, WM_MOUSEMOVE...)
  UINT  paramL;    // 訊息參數 wParam (鍵盤: 虛擬鍵碼在 LOBYTE)
  UINT  paramH;    // 訊息參數 lParam (鍵盤: 掃描碼、擴充旗標等)
  DWORD time;      // 事件時間戳 (GetTickCount)
  DWORD dwExtraInfo; // 額外資訊
} EVENTMSG;

鍵盤事件中:
- paramL 低位元組 = 虛擬鍵碼 (VK_A, VK_RETURN 等)。
- paramH 低位元組 = 掃描碼,高位元組含擴充鍵、Alt 鍵狀態等旗標。

2. Hook 鏈傳遞:CallNextHookEx

回呼**必須**呼叫 CallNextHookEx(g_hHook, nCode, wParam, lParam) 將事件傳遞給下一個 Hook 或系統預設處理,否則會導致鍵盤輸入系統癱瘓 (無法輸入字元、熱鍵失效)。

3. DLL 實例與全域變數隔離

DLL 被注入每個進程時,g_hInst, g_hTargetWnd, g_hHook 在各進程各有一份副本。但 SetHook 只在主程式進程呼叫一次,因此只有主程式進程的 g_hTargetWnd 被設定。Hook 回呼在**目標進程**執行時,其 g_hTargetWnd 為 0。
解決方案: 本範例利用 SetHook 只在主程式呼叫的特性,僅在主程式進程建立 Hook。但 WH_JOURNALRECORD 本質是全域 Hook,系統會將 DLL 映射到所有進程,並在每個進程內呼叫 HookProc。此時目標進程的 g_hTargetWnd 為 0,PostMessage 會失敗。
正確做法: 應將 g_hTargetWnd 存入**共享記憶體** (如 CreateFileMapping + PAGE_READWRITE),或使用 WH_KEYBOARD 搭配共享資料段 (#pragma data_seg) 讓所有進程共享同一份 Hook 句柄與目標視窗句柄。參考文章 7842 第 3、5 節共享記憶體實作。

4. 訊息衝突避免:RegisterWindowMessage

建議將自訂訊息改用 RegisterWindowMessage("MyUniqueHookMsg_2024") 取得系統唯一訊息代碼,避免與其他應用程式衝突。

常見問題與除錯

  • Hook 不觸發: 確認 DLL 位於 EXE 同目錄或系統路徑;LoadLibrary 成功;SetWindowsHookEx 回傳非 NULL (失敗可呼叫 GetLastError,常見錯誤 1428/1429 為 Hook 程序無效/權限不足)。
  • 系統卡頓/輸入失效: Hook 回呼執行過久、未呼叫 CallNextHookEx、在回呼執行阻塞操作 (MessageBox、檔案 I/O、Sleep)。回呼必須極速返回,僅 PostMessage 通知主程式。
  • 64 位元相容: 32 位元 DLL 無法注入 64 位元進程,反之亦然。C++ Builder 10.4+ 支援 64 位元編譯,需分別建置 32/64 位元 DLL 並由對應位元 EXE 載入。
  • UAC/完整性等級: 高完整性進程 (系統管理員) 不會載入低完整性進程的 Hook DLL。主程式若以管理員執行,Hook 只會生效於管理員進程。

進階替代方案:WH_KEYBOARD_LL (低層級 Hook)

  • **優點**:不需 DLL,可直接在 EXE 實作;捕獲原始硬體輸入,攔截能力最強。
  • **缺點**:Vista 之後若回呼超過 LowLevelHooksTimeout (預設 300ms) 系統會自動移除 Hook;需在獨立執行緒處理耗時邏輯;UAC 高權限進程需簽署驅動或關閉 UAC。

現代專案建議優先評估 WH_KEYBOARD_LL + 執行緒池架構;舊版相容或教學專案 WH_JOURNALRECORD + 共享記憶體 DLL 仍具參考價值。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved