最近自己碰到一個(gè)需求: 想通過按鍵給某個(gè)非焦點(diǎn)窗口發(fā)送鍵盤指令. 正好復(fù)習(xí)了一下Windows編程和Hook的相關(guān)知識, 寫了一個(gè)可以用的鉤子工具, 微軟的官方文檔入口: https://docs.microsoft.com/en-us/windows/win32/winmsg/hooks
Windows消息機(jī)制和Hook
首先, Windows操作系統(tǒng)負(fù)責(zé)維護(hù)兩個(gè)消息隊(duì)列(Message Queue): 系統(tǒng)消息隊(duì)列和線程消息隊(duì)列. 操作系統(tǒng)會(huì)為每一個(gè)有窗口的線程創(chuàng)建消息隊(duì)列, 包括控制臺程序. 當(dāng)用戶有一個(gè)輸入以后, 比如按鍵或者鼠標(biāo), 系統(tǒng)會(huì)創(chuàng)建一個(gè)事件消息首先壓入到系統(tǒng)消息隊(duì)列, 然后分發(fā)到對應(yīng)的線程消息隊(duì)列.
每一個(gè)線程需要維護(hù)一個(gè)消息循環(huán)(Message Loop), 用來輪詢獲取消息隊(duì)列中的相關(guān)消息和調(diào)用對應(yīng)的窗口處理函數(shù), 對應(yīng)的API是
GetMessageA(...)
每一個(gè)線程只能監(jiān)聽和響應(yīng)跟自己相關(guān)的事件. 如果這個(gè)線程需要獲取全局的事件響應(yīng), 這個(gè)時(shí)候就要用到Hook了. 基本過程如下:
- 通過
SetWindowsHookExA(...)
設(shè)置鉤子(local或者DLL)- 系統(tǒng)注入DLL或者直接發(fā)送底層消息到Hook線程
- Hook回調(diào)函數(shù)對目標(biāo)事件做出響應(yīng), 在DLL中就發(fā)送消息到Hook線程或者local直接響應(yīng)
- local或者DLL的不同由Hook的種類決定, 有些Hook只能用DLL, 下面會(huì)詳細(xì)介紹
-
Windows提供兩種鍵盤鉤子: WH_KEYBOARD_LL 和 WH_KEYBOARD. 援引MSDN的解釋如下:
鍵盤鉤子.PNGWH_KEYBOARD_LL是一個(gè)Low-Level的鉤子, 當(dāng)Raw Input Thread(RIT)決定從系統(tǒng)消息隊(duì)列中分發(fā)消息之前, 就已經(jīng)截獲了這個(gè)消息進(jìn)行了處理. 所以WH_KEYBOARD_LL甚至?xí)缬谙到y(tǒng)線程來處理消息, 比如
ctrl + alt + del
都可以截獲, 并且WH_KEYBOARD_LL讓系統(tǒng)不需要通過DLL來動(dòng)態(tài)注入所有進(jìn)程了,系統(tǒng)只會(huì)把消息發(fā)送到Hook線程. WH_KEYBOARD的層級比較高, 屬于應(yīng)用程序級別的, 所以系統(tǒng)線程會(huì)早于這個(gè)Hook響應(yīng), 并且只能截獲系統(tǒng)發(fā)送到應(yīng)用線程messag queue
的消息, 但是優(yōu)先執(zhí)行該Hook的回調(diào)函數(shù),如果Hook回調(diào)返回false才會(huì)繼續(xù)處理當(dāng)前應(yīng)用的消息響應(yīng)函數(shù). 所以總結(jié)以下幾點(diǎn):- 這兩種鉤子都可以作為全局鉤子使用, 只需要設(shè)置鉤子的時(shí)候注入線程設(shè)成 0
- WH_KEYBOARD_LL 在消息發(fā)送之前就已經(jīng)處理了, 而 WH_KEYBOARD是發(fā)送到注入線程以后
- WH_KEYBOARD_LL 無需系統(tǒng)注入DLL執(zhí)行, 而 WH_KEYBOARD 需要DLL來注入所有進(jìn)程空間
定位需要響應(yīng)事件的窗口句柄
- 首先獲取窗口所屬進(jìn)程的PID:
快照系統(tǒng)進(jìn)程, 然后輪詢進(jìn)程列表,根據(jù)進(jìn)程名稱找到對應(yīng)的ID
DWORD getPIDFromProcessName(string sProcessName)
{
PROCESSENTRY32 pe;
pe.dwSize = sizeof(PROCESSENTRY32);
HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (!Process32First(hSnapshot, &pe))
{
return false;
}
string sCur(pe.szExeFile);
do
{
if (GetLastError() == ERROR_NO_MORE_FILES)
{
break;
}
sCur = pe.szExeFile;
cout << sCur.c_str() << endl;
if (sCur == sProcessName)
{
printf("Found");
return pe.th32ProcessID;
}
} while (Process32Next(hSnapshot, &pe));
return 0;
}
- 鎖定目標(biāo)進(jìn)程需要響應(yīng)事件的窗口句柄:
EnumWindows(YourCallBack, (LPARAM)pid);
設(shè)置鉤子
- 如果是使用WH_KEYBOARD_LL, 直接寫在Hook線程就可以了:
//HookLocal.cpp
LRESULT CALLBACK HookCallback(int code, WPARAM wParam, LPARAM lParam)
...
SetWindowsHookExA(WH_KEYBOARD_LL,HookCallback,0,0);
...
- 如果是使用WH_KEYBOARD, 要寫在DLL中, 通過注入線程來調(diào)用, 這里要注意傳入Hook線程的窗口句柄,用來Post一個(gè)消息通知Hook線程來處理消息:
//HookDLL.cpp
DWORD g_hwnd;
LRESULT CALLBACK HookCallback(int code, WPARAM wParam, LPARAM lParam)
{
PostMessage(g_hwnd, ...); //發(fā)送消息到Hook線程的消息隊(duì)列
return true;
}
extern "C" _declspec(dllexport) void setHook(HWND hookWindow)
{
HMODULE mod = GetModuleHandle(L"YourDLL.dll");
//注入hook
SetWindowsHookExA(WH_KEYBOARD, HookCallback, mod, 0);
//保存Hook線程窗口句柄
g_hwnd = hookWindow;
}
消息循環(huán)
在Hook線程中, 無論是用的哪種鉤子,都需要一個(gè)消息循環(huán)輪詢當(dāng)前線程中的消息隊(duì)列:
MSG msg;
while(GetMessageA(&msg,0,0,0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}