我們知道Windows中的窗口程序是基於消息,由事件驅動的,在某些情況下可能需要捕獲或者修改消息,從而完成一些特殊的功能(MFC框架就利用Windows鉤子對消息進行引導)。對於捕獲消息而言,無法使用IAT或Inline Hook之類的方式去進行捕獲,這就要用到接下來要介紹的Windows提供的專門用於處理消息的鉤子函數。
1. 掛鉤原理
Windows下的應用程序大部分都是基於消息機制的,它們都會有一個消息過程函數,根據不同的消息完成不同的功能。Windows操作系統提供的鉤子機制的作用就是用來截獲和監視這些系統中的消息。Windows鉤子琳琅滿目,可以用來應對各種不同的消息。
按照鉤子作用的範圍不同,又可以分爲局部鉤子和全局鉤子。局部鉤子是針對某個線程的;而全局鉤子則是作用於整個系統中基於消息的應用。全局鉤子需要使用DLL文件,在DLL中實現相應的鉤子函數。在操作系統中安裝全局鉤子後,只要進程接收到可以發出鉤子的消息,全局鉤子的DLL文件就會被操作系統自動或強行地加載到該進程中。因此,設置消息鉤子,也可以達到DLL注入的目的。
2. 鉤子函數
- HHOOK SetWindowsHookEx(
- int idHook,
- HOOKPROC lpfn,
- HINSTANCE hMod,
- DWORD dwThreadId
- );
HHOOK SetWindowsHookEx(
int idHook,
HOOKPROC lpfn,
HINSTANCE hMod,
DWORD dwThreadId
);
該函數的返回值是一個鉤子句柄。參數介紹如下:
lpfn:指定Hook函數的地址。如果dwThreadId參數被賦值爲0,或者被設置爲一個其他進程中的線程ID,那麼lpfn屬於DLL中的函數過程。如果dwThreadId爲當前進程中的一個線程ID,那麼lpfn可以使指向當前進程模塊中的函數,當然,也可以使DLL模塊中的函數。
hMod:該參數指定鉤子函數所在模塊的模塊句柄。即lpfn所在的模塊句柄。如果dwThreadId爲當前進程中的線程ID,且lpfn所指函數在當前進程中,則該參數被設置爲NULL。
dwThreadId:指定需要被掛鉤的線程ID號。如果設置爲0,表示在所有基於消息的線程中掛鉤;如果設置了具體的線程ID,表示在指定線程中掛鉤。該參數影響上面兩個參數的取值,同時也決定了該鉤子是全局鉤子還是局部鉤子。
idHook:該參數表示鉤子的類型。常用的幾種如下:
※ WH_GETMESSAGE
按照該鉤子的作用是監視被投遞到消息隊列中的消息。也就是當調用GetMessage或PeekMessage函數時,函數從程序的消息隊列中獲取一個消息後調用該鉤子。
WH_GETMESSAGEG的鉤子函數如下:
- LRESULT CALLBACK GetMsgProc(
- int code, //hook code
- WPARAM wParam, //removal option
- LPARAM lParam //message
- );
LRESULT CALLBACK GetMsgProc(
int code, //hook code
WPARAM wParam, //removal option
LPARAM lParam //message
);
※ WH_MOUSE
該鉤子用於監視鼠標消息。鉤子函數如下:
- LRESULT CALLBACK MouseProc(
- int nCode, //hook code
- WPARAM wParam, //message identifier
- LPARAM lParam //mouse coordinates
- );
LRESULT CALLBACK MouseProc(
int nCode, //hook code
WPARAM wParam, //message identifier
LPARAM lParam //mouse coordinates
);
※ WH_KEYBOARD
該鉤子用於監視鍵盤消息。鉤子函數如下:
- LRESULT CALLBACK KeyboardProc(
- int code, //hook code
- WPARAM wParam, //virtual-key code
- LPARAM lParam //keystroke-message information
- );
LRESULT CALLBACK KeyboardProc(
int code, //hook code
WPARAM wParam, //virtual-key code
LPARAM lParam //keystroke-message information
);
※ WH_DEBUG
用於調試其它鉤子。鉤子函數如下:
- LRESULT CALLBACK DebugProc(
- int nCode, //hook code
- WPARAM wParam, //hook type
- LPARAM lParam //debugging information
- );
LRESULT CALLBACK DebugProc(
int nCode, //hook code
WPARAM wParam, //hook type
LPARAM lParam //debugging information
);
對於以上鉤子函數的詳情還請各位看客老爺們自行挪步到MSDN了。
移除先前用SetWindowsHookEx安裝的鉤子:
- BOOL UnhookWindowsHookEx(
- HHOOK hhk
- );
BOOL UnhookWindowsHookEx(
HHOOK hhk
);
唯一的參數是待移除的鉤子句柄。
在實際應用中,可以多次調用SetWindowsHookEx函數來安裝鉤子,而且可以安裝多個同樣類型的鉤子。這樣,鉤子就會形成一條鉤子鏈,最後安裝的鉤子會首先截獲到消息。當該鉤子對消息處理完畢後,可以選擇返回或者把消息繼續傳遞下去。如果是爲了屏蔽某消息,可以在安裝的鉤子函數中直接返回非零值。如果希望我們的鉤子函數處理完消息後可以繼續傳遞給目標窗口,則必須選擇將消息繼續傳遞。繼續傳遞消息的函數定義如下:
- LRESULT CallNextHookEx(
- HHOOK hhk, //handle to current hook
- int nCode, //hook code passed to hook procedure
- WPARAM wParam, //value passed to hook procedure
- LPARAM lParam //value passed to hook procedure
- );
LRESULT CallNextHookEx(
HHOOK hhk, //handle to current hook
int nCode, //hook code passed to hook procedure
WPARAM wParam, //value passed to hook procedure
LPARAM lParam //value passed to hook procedure
);
第一個參數是鉤子句柄,就是調用SetWindowsHookEx函數的返回值;後面3個參數是鉤子的參數,直接一次copy即可。例如:
- HHOOK g_Hook = SetWindowsHookEx(…);
- LRESULT CALLBACK GetMsgProc(
- int code, //hook code
- WPARAM wParam, //removal option
- LPARAM lParam //message
- )
- {
- return CallNextHookEx(g_Hook, code, wParam, lParam);
- }
HHOOK g_Hook = SetWindowsHookEx(…);
LRESULT CALLBACK GetMsgProc(
int code, //hook code
WPARAM wParam, //removal option
LPARAM lParam //message
)
{
return CallNextHookEx(g_Hook, code, wParam, lParam);
}
3. 鉤子實例
Windows鉤子的使用場景比較廣泛,我們就幾種比較常見的情況做一個應用示例。
3.1全局鍵盤鉤子
先新建一個DLL程序(這個不會可以看我以前的博客,這裏就不重複了),我們在頭文件中增加兩個導出函數和兩個全局。
- #define MY_API __declspec(dllexport)
- extern "C" MY_API VOID SetHookOn();
- extern "C" MY_API VOID SetHookOff();
- HHOOK g_Hook = NULL; //鉤子句柄
- HINSTANCE g_Inst = NULL; //DLL模塊句柄
#define MY_API __declspec(dllexport)
extern "C" MY_API VOID SetHookOn();
extern "C" MY_API VOID SetHookOff();
HHOOK g_Hook = NULL; //鉤子句柄
HINSTANCE g_Inst = NULL; //DLL模塊句柄
在DllMain中保存該DLL模塊的句柄,以方便安裝全局鉤子。
- BOOL APIENTRY DllMain( HANDLE hModule,
- DWORD ul_reason_for_call,
- LPVOID lpReserved
- )
- {
- //保存DLL模塊句柄
- g_Inst = (HINSTANCE)hModule;
- return TRUE;
- }
BOOL APIENTRY DllMain( HANDLE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
//保存DLL模塊句柄
g_Inst = (HINSTANCE)hModule;
return TRUE;
}
安裝與卸載鉤子的函數如下:
- VOID SetHookOn()
- {
- //安裝鉤子
- g_Hook = SetWindowsHookEx(WH_KEYBOARD, KeyboardProc, g_Inst, 0);
- }
- VOID SetHookOff()
- {
- //卸載鉤子
- UnhookWindowsHookEx(g_Hook);
- }
VOID SetHookOn()
{
//安裝鉤子
g_Hook = SetWindowsHookEx(WH_KEYBOARD, KeyboardProc, g_Inst, 0);
}
VOID SetHookOff()
{
//卸載鉤子
UnhookWindowsHookEx(g_Hook);
}
鉤子函數的實現如下:
- //鉤子函數
- LRESULT CALLBACK KeyboardProc(int code, WPARAM wParam, LPARAM lParam)
- {
- if(code < 0)
- {
- //如果code小於0,必須調用CallNextHookEx傳遞消息,不處理該消息,並返回CallNextHookEx的返回值。
- return CallNextHookEx(g_Hook, code, wParam, lParam);
- }
- if(code == HC_ACTION && lParam > 0)
- {
- //code等於HC_ACTION,表示消息中包含按鍵消息
- //如果爲WM_KEYDOWN,則顯示按鍵對應的文本
- char szBuf[MAXBYTE] = {0};
- GetKeyNameText(lParam, szBuf, MAXBYTE);
- MessageBox(NULL, szBuf, "提示", MB_OK);
- }
- return CallNextHookEx(g_Hook, code, wParam, lParam);
- }
//鉤子函數
LRESULT CALLBACK KeyboardProc(int code, WPARAM wParam, LPARAM lParam)
{
if(code < 0)
{
//如果code小於0,必須調用CallNextHookEx傳遞消息,不處理該消息,並返回CallNextHookEx的返回值。
return CallNextHookEx(g_Hook, code, wParam, lParam);
}
if(code == HC_ACTION && lParam > 0)
{
//code等於HC_ACTION,表示消息中包含按鍵消息
//如果爲WM_KEYDOWN,則顯示按鍵對應的文本
char szBuf[MAXBYTE] = {0};
GetKeyNameText(lParam, szBuf, MAXBYTE);
MessageBox(NULL, szBuf, "提示", MB_OK);
}
return CallNextHookEx(g_Hook, code, wParam, lParam);
}
編譯鏈接後產生我們需要的.dll和.lib文件,然後新建一個項目來導入動態庫內容調用相關函數。
新建項目如下:
首先導入庫:
- #pragma comment (lib, "全局鉤子.lib")
#pragma comment (lib, "全局鉤子.lib")
聲明將要調用的函數(不聲明鏈接時將報錯):
- extern "C" VOID SetHookOn();
- extern "C" VOID SetHookOff();
extern "C" VOID SetHookOn();
extern "C" VOID SetHookOff();
在按鈕事件中調用導出函數:
- void CHookDebugDlg::OnHookon()
- {
- SetHookOn();
- }
- void CHookDebugDlg::OnHookoff()
- {
- SetHookOff();
- }
void CHookDebugDlg::OnHookon()
{
SetHookOn();
}
void CHookDebugDlg::OnHookoff()
{
SetHookOff();
}
執行結果如下:
3.2低級鍵盤鉤子
數據防泄漏軟件通常會精緻PrintScreen鍵,防止通過截屏將數據保存爲圖片而導致數據泄密。下面我們也可以模仿一下,簡單的實現該功能。這裏需要注意的是,普通的鍵盤鉤子(WH_KEYBOARD)是無法過濾一些系統按鍵的,得通過安裝低級鍵盤鉤子(WH_KEYBOARD_LL)來達到目的。
在低級鍵盤鉤子的回調函數中,判斷是否爲PrintScreen鍵,如果是,則直接返回TRUE;如果不是,則傳遞給下一個鉤子處理。
具體DLL中的實現代碼如下:
- BOOL SetHookOn()
- {
- if(g_Hook != NULL)
- {
- return FALSE;
- }
- //安裝鉤子
- g_Hook = SetWindowsHookEx(WH_KEYBOARD_LL, LowLevelKeyboardProc, g_Inst, 0);
- if(NULL == g_Hook)
- {
- MessageBox(NULL, "安裝鉤子出錯!", "ERROR", MB_ICONSTOP);
- return FALSE;
- }
- return TRUE;
- }
- BOOL SetHookOff()
- {
- if(g_Hook == NULL)
- {
- return FALSE;
- }
- //卸載鉤子
- UnhookWindowsHookEx(g_Hook);
- g_Hook = NULL;
- return TRUE;
- }
- //鉤子函數
- LRESULT CALLBACK LowLevelKeyboardProc(int code, WPARAM wParam, LPARAM lParam)
- {
- KBDLLHOOKSTRUCT *Key_Info = (KBDLLHOOKSTRUCT *)lParam;
- if(HC_ACTION == code)
- {
- if(WM_KEYDOWN == wParam || WM_SYSKEYDOWN == wParam)
- {
- if(Key_Info->vkCode == VK_SNAPSHOT)
- {
- MessageBox(NULL, "該鍵已禁用!", "ERROR", MB_ICONSTOP);
- return TRUE;
- }
- }
- }
- return CallNextHookEx(g_Hook, code, wParam, lParam);
- }
BOOL SetHookOn()
{
if(g_Hook != NULL)
{
return FALSE;
}
//安裝鉤子
g_Hook = SetWindowsHookEx(WH_KEYBOARD_LL, LowLevelKeyboardProc, g_Inst, 0);
if(NULL == g_Hook)
{
MessageBox(NULL, "安裝鉤子出錯!", "ERROR", MB_ICONSTOP);
return FALSE;
}
return TRUE;
}
BOOL SetHookOff()
{
if(g_Hook == NULL)
{
return FALSE;
}
//卸載鉤子
UnhookWindowsHookEx(g_Hook);
g_Hook = NULL;
return TRUE;
}
//鉤子函數
LRESULT CALLBACK LowLevelKeyboardProc(int code, WPARAM wParam, LPARAM lParam)
{
KBDLLHOOKSTRUCT *Key_Info = (KBDLLHOOKSTRUCT *)lParam;
if(HC_ACTION == code)
{
if(WM_KEYDOWN == wParam || WM_SYSKEYDOWN == wParam)
{
if(Key_Info->vkCode == VK_SNAPSHOT)
{
MessageBox(NULL, "該鍵已禁用!", "ERROR", MB_ICONSTOP);
return TRUE;
}
}
}
return CallNextHookEx(g_Hook, code, wParam, lParam);
}
依然利用前面的小程序,執行後按下PrintScreen鍵,效果如下:
可能在編譯時會報錯,說WH_KEYBOARD_LL和KBDLLHOOKSTRUCT未定義,此時可以在文件開頭加上如下代碼:
- #define WH_KEYBOARD_LL 13
- typedef struct tagKBDLLHOOKSTRUCT {
- DWORD vkCode;
- DWORD scanCode;
- DWORD flags;
- DWORD time;
- DWORD dwExtraInfo;
- } KBDLLHOOKSTRUCT, FAR *LPKBDLLHOOKSTRUCT, *PKBDLLHOOKSTRUCT;
#define WH_KEYBOARD_LL 13
typedef struct tagKBDLLHOOKSTRUCT {
DWORD vkCode;
DWORD scanCode;
DWORD flags;
DWORD time;
DWORD dwExtraInfo;
} KBDLLHOOKSTRUCT, FAR *LPKBDLLHOOKSTRUCT, *PKBDLLHOOKSTRUCT;
其實在winuser.h中已有定義,但是可能是兼容的緣故用不了。
3.3鉤子注入DLL
利用WH_GETMESSAGE鉤子,可以方便地將DLL文件注入到所有基於消息機制的程序中。因爲有時候可能需要DLL文件完成一些工作,但是工作時需要DLL在目標進程的空間中。這個時候,就可以將DLL注入目標進程來完成相關的功能。
主要的代碼如下:
- BOOL APIENTRY DllMain( HANDLE hModule,
- DWORD ul_reason_for_call,
- LPVOID lpReserved
- )
- {
- //保存DLL模塊句柄
- g_Inst = (HINSTANCE)hModule;
- switch (ul_reason_for_call)
- {
- case DLL_PROCESS_ATTACH:
- {
- DoSomething();
- break;
- }
- case DLL_THREAD_ATTACH:
- case DLL_THREAD_DETACH:
- case DLL_PROCESS_DETACH:
- break;
- }
- return TRUE;
- }
- VOID SetHookOn()
- {
- g_Hook = SetWindowsHookEx(WH_GETMESSAGE, GetMsgProc, g_Inst, 0);
- }
- VOID SetHookOff()
- {
- UnhookWindowsHookEx(g_Hook);
- }
- LRESULT CALLBACK GetMsgProc(int code, WPARAM wParam, LPARAM lParam)
- {
- return CallNextHookEx(g_Hook, code, wParam, lParam);
- }
- VOID DoSomething()
- {
- MessageBox(NULL, "Hello,我被執行了!", "提示", MB_OK);
- }
BOOL APIENTRY DllMain( HANDLE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
//保存DLL模塊句柄
g_Inst = (HINSTANCE)hModule;
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
{
DoSomething();
break;
}
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
VOID SetHookOn()
{
g_Hook = SetWindowsHookEx(WH_GETMESSAGE, GetMsgProc, g_Inst, 0);
}
VOID SetHookOff()
{
UnhookWindowsHookEx(g_Hook);
}
LRESULT CALLBACK GetMsgProc(int code, WPARAM wParam, LPARAM lParam)
{
return CallNextHookEx(g_Hook, code, wParam, lParam);
}
VOID DoSomething()
{
MessageBox(NULL, "Hello,我被執行了!", "提示", MB_OK);
}
執行效果圖:
需要注意的是,此處執行的DoSomething並不是導出函數哦。