網(wǎng)絡(luò)安全編程:Windows鉤子函數(shù)
Windows下的窗口應(yīng)用程序是基于消息驅(qū)動(dòng)的,但是在某些情況下需要捕獲或者修改消息,從而完成一些特殊的功能。對(duì)于捕獲消息而言,無(wú)法使用IAT或Inline Hook之類(lèi)的方式去進(jìn)行捕獲,不過(guò)Windows提供了專門(mén)用于處理消息的鉤子函數(shù)。
01 鉤子原理
Windows下的應(yīng)用程序大部分是基于消息模式機(jī)制的,一些CUI的程序不是基于消息的。Windows下的應(yīng)用程序都有一個(gè)消息過(guò)程函數(shù),根據(jù)不同的消息來(lái)完成不同的功能。Windows操作系統(tǒng)提供的鉤子機(jī)制的作用是用來(lái)截獲、監(jiān)視系統(tǒng)中的消息。Windows操作系統(tǒng)提供了很多不同種類(lèi)的鉤子,可以處理不同的消息。
Windows系統(tǒng)提供的鉤子按照掛鉤范圍分為局部鉤子和全局鉤子。局部鉤子是針對(duì)一個(gè)線程的,而全局鉤子則是針對(duì)整個(gè)操作系統(tǒng)內(nèi)基于消息機(jī)制的應(yīng)用程序的。全局鉤子需要使用DLL文件,DLL文件里存放了鉤子函數(shù)的代碼。
在操作系統(tǒng)中安裝全局鉤子以后,只要進(jìn)程接收到可以發(fā)出鉤子的消息后,全局鉤子的DLL文件會(huì)被操作系統(tǒng)自動(dòng)或強(qiáng)行地加載到該進(jìn)程中。由此可見(jiàn),設(shè)置消息鉤子也是一種可以進(jìn)行DLL注入的方法。
02 鉤子函數(shù)
鉤子函數(shù)主要有3個(gè),分別是SetWindowsHookEx()、CallNextHookEx()和UnhookWindowsHookEx()。下面介紹這些函數(shù)的使用方法。
SetWindowsHookEx()函數(shù)的定義如下:
- HHOOK SetWindowsHookEx(
- int idHook,
- HOOKPROC lpfn,
- HINSTANCE hMod,
- DWORD dwThreadId
- );
該函數(shù)的返回值為一個(gè)鉤子句柄。這個(gè)函數(shù)有4個(gè)參數(shù),下面分別進(jìn)行介紹。
lpfn:該參數(shù)指定為 Hook 函數(shù)的地址。如果 dwThreadId 參數(shù)被賦值為 0,或者被設(shè)置為一個(gè)其他進(jìn)程中的線程 ID,那么 lpfn 則屬于 DLL 中的函數(shù)過(guò)程。如果 dwThreadId 為當(dāng)前進(jìn)程中的線程 ID,那么 lpfn 可以是指向當(dāng)前進(jìn)程中的函數(shù)過(guò)程,也可以是屬于 DLL 中的函數(shù)過(guò)程。
hMod:該參數(shù)指定鉤子函數(shù)所在模塊的模塊句柄。該模塊句柄就是 lpfn 所在的模塊的句柄。如果 dwThreadId 為當(dāng)前進(jìn)程中的線程 ID,而且 lpfn 所指向的函數(shù)在當(dāng)前進(jìn)程中,那么 hMod 將被設(shè)置為 NULL。
dwThreadId:該參數(shù)設(shè)置為需要被掛鉤的線程的 ID 號(hào)。如果設(shè)置為 0,表示在所有的線程中掛鉤(這里的“所有的線程”表示基于消息機(jī)制的所有的線程)。如果指定為具體的線程的 ID 號(hào),表示要在指定的線程中進(jìn)行掛鉤。該參數(shù)影響上面兩個(gè)參數(shù)的取值。該參數(shù)的取值決定了該鉤子屬于全局鉤子,還是局部鉤子。
idHook:該參數(shù)表示鉤子的類(lèi)型。由于鉤子的類(lèi)型非常多,因此放在所有的參數(shù)后面進(jìn)行介紹。下面介紹幾個(gè)常用到的鉤子,也可能是大家比較關(guān)心的幾個(gè)類(lèi)型。
1. WH_GETMESSAGE
安裝該鉤子的作用是監(jiān)視被投遞到消息隊(duì)列中的消息。也就是當(dāng)調(diào)用GetMessage()或PeekMessage()函數(shù)時(shí),函數(shù)從程序的消息隊(duì)列中獲取一個(gè)消息后調(diào)用該鉤子。
WH_GETMESSAGE鉤子函數(shù)的定義如下:
- LRESULT CALLBACK GetMsgProc(
- int code, // 鉤子編碼
- WPARAM wParam, // 移除選項(xiàng)
- LPARAM lParam // 消息
- );
2. WH_MOUSE
安裝該鉤子的作用是監(jiān)視鼠標(biāo)消息。該鉤子函數(shù)的定義如下:
- LRESULT CALLBACK MouseProc(
- int nCode, // 鉤子編碼
- WPARAM wParam, // 消息標(biāo)識(shí)符
- LPARAM lParam // 鼠標(biāo)的坐標(biāo)
- );
3. WH_KEYBOARD
安裝該鉤子的作用是監(jiān)視鍵盤(pán)消息。該鉤子函數(shù)的定義如下:
- LRESULT CALLBACK KeyboardProc(
- int code, // 鉤子編碼
- WPARAM wParam, // 虛擬鍵編碼
- LPARAM lParam // 按鍵信息
- );
4. WH_DEBUG
安裝該鉤子的作用是調(diào)試其他鉤子的鉤子函數(shù)。該鉤子函數(shù)的定義如下:
- LRESULT CALLBACK DebugProc(
- int nCode, // 鉤子編碼
- WPARAM wParam, // 鉤子類(lèi)型
- LPARAM lParam // 調(diào)試信息
- );
從上面的這些鉤子函數(shù)定義可以看出,每個(gè)鉤子函數(shù)的定義都是一樣的。每種類(lèi)型的鉤子監(jiān)視、截獲的消息不同。雖然它們的定義都是相同的,但是函數(shù)參數(shù)的意義是不同的。
接著介紹跟鉤子有關(guān)的另外一個(gè)函數(shù):UnhookWindowsHookEx(),其定義如下:
- BOOL UnhookWindowsHookEx(
- HHOOK hhk // 鉤子函數(shù)的句柄
- );
這個(gè)函數(shù)是用來(lái)移除先前用SetWindowsHookEx()安裝的鉤子。該函數(shù)只有一個(gè)參數(shù),是鉤子句柄,也就是調(diào)用該函數(shù)通過(guò)指定的鉤子句柄來(lái)移除與其相應(yīng)的鉤子。
在操作系統(tǒng)中,可以多次反復(fù)地使用SetWindowsHookEx()函數(shù)來(lái)安裝鉤子,而且可以安裝多個(gè)同樣類(lèi)型的鉤子。這樣,鉤子就會(huì)形成一條鉤子鏈,最后安裝的鉤子會(huì)首先截獲到消息。當(dāng)該鉤子對(duì)消息處理完畢以后,會(huì)選擇返回,或者選擇把消息繼續(xù)傳遞下去。在通常情況下,如果為了屏蔽消息,則直接在鉤子函數(shù)中返回一個(gè)非零值。比如要在自己程序中屏蔽鼠標(biāo)消息,則在安裝的鼠標(biāo)鉤子函數(shù)中直接返回非零值即可。如果為了消息在經(jīng)過(guò)鉤子函數(shù)后可以繼續(xù)傳達(dá)到目標(biāo)窗口,必須選擇將消息繼續(xù)傳遞。使消息能繼續(xù)傳遞的函數(shù)的定義如下:
- LRESULT CallNextHookEx(
- HHOOK hhk, // 當(dāng)前鉤子的句柄
- int nCode, // 傳遞給釣子函數(shù)的釣子編碼
- WPARAM wParam, // 傳遞給釣子函數(shù)的值
- LPARAM lParam // 傳遞給釣子函數(shù)的值
- );
該函數(shù)有4個(gè)參數(shù)。第一個(gè)參數(shù)是鉤子句柄,就是調(diào)用SetWindowsHookEx()函數(shù)的返回值;后面3個(gè)參數(shù)是鉤子函數(shù)的參數(shù),直接依次抄過(guò)來(lái)即可。例如:
- HHOOK g_Hook = SetWindowsHook(……);
- LRESULT CALLBACK GetMsgProc(
- int code, // 鉤子編碼
- WPARAM wParam, // 移除選項(xiàng)
- LPARAM lParam // 消息
- )
- {
- return CallNextHookEx(g_Hook, code, wParam, lParam);
- }
03 鉤子實(shí)例
Windows鉤子的應(yīng)用比較廣。無(wú)論是安全產(chǎn)品還是惡意軟件,甚至是常規(guī)軟件,都會(huì)用到Windows提供的鉤子功能。
1. 全局鍵盤(pán)鉤子
下面來(lái)寫(xiě)一個(gè)可以截獲鍵盤(pán)消息的鉤子程序,其功能非常簡(jiǎn)單,就是把按下的鍵對(duì)應(yīng)的字符顯示出來(lái)。既然要截獲鍵盤(pán)消息,那么肯定是截獲系統(tǒng)范圍內(nèi)的鍵盤(pán)消息,因此需要安裝全局鉤子,這樣就需要DLL文件的支持。先來(lái)新建一個(gè)DLL文件,在該DLL文件中需要定義兩個(gè)導(dǎo)出函數(shù)和兩個(gè)全局變量,定義如下:
- extern "C" __declspec(dllexport) VOID SetHookOn();
- extern "C" __declspec(dllexport) VOID SetHookOff();
- // 鉤子句柄
- HHOOK g_Hook = NULL;
- // DLL 模塊句柄
- HINSTANCE g_Inst = NULL;
在DllMain()函數(shù)中,需要保存該DLL模塊的句柄,以方便安裝全局鉤子。代碼如下:
- BOOL APIENTRY DllMain( HANDLE hModule,
- DWORD ul_reason_for_call,
- LPVOID lpReserved
- )
- {
- // 保存 DLL 的模塊句柄
- g_Inst = (HINSTANCE)hModule;
- return TRUE;
- }
安裝與卸載鉤子的函數(shù)如下:
- VOID SetHookOn()
- {
- // 安裝鉤子
- g_Hook = SetWindowsHookEx(WH_KEYBOARD, KeyboardProc, g_Inst, 0);
- }
- VOID SetHookOff()
- {
- // 卸載鉤子
- UnhookWindowsHookEx(g_Hook);
- }
對(duì)于Windows鉤子來(lái)說(shuō),上面的這些步驟基本上都是必需的,或者是差別不大,關(guān)鍵在于鉤子函數(shù)的實(shí)現(xiàn)。這里是為了獲取鍵盤(pán)按下的鍵,鉤子函數(shù)如下:
- // 鉤子函數(shù)
- LRESULT CALLBACK KeyboardProc(
- int code, // 鉤子編碼
- WPARAM wParam, // 虛擬鍵編碼
- LPARAM lParam // 按建信息
- )
- {
- if ( code < 0 )
- {
- return CallNextHookEx(g_Hook, code, wParam, lParam);
- }
- if ( code == HC_ACTION && lParam > 0 )
- {
- char szBuf[MAXBYTE] = { 0 };
- GetKeyNameText(lParam, szBuf, MAXBYTE);
- MessageBox(NULL, szBuf, NULL, MB_OK);
- }
- return CallNextHookEx(g_Hook, code, wParam, lParam);
- }
關(guān)于鉤子函數(shù),這里簡(jiǎn)單地解釋一下,首先是進(jìn)入鉤子函數(shù)的第一個(gè)判斷。
- if ( code < 0 )
- {
- return CallNextHookEx(g_Hook, code, wParam, lParam);
- }
如果code的值小于0,則必須調(diào)用CallNextHookEx(),將消息繼續(xù)傳遞下去,不對(duì)該消息進(jìn)行處理,并返回CallNextHookEx()函數(shù)的返回值。這一點(diǎn)是MSDN上要求這么做的。
- if ( code == HC_ACTION && lParam > 0 )
- {
- char szBuf[MAXBYTE] = { 0 };
- GetKeyNameText(lParam, szBuf, MAXBYTE);
- MessageBox(NULL, szBuf, NULL, MB_OK);
- }
如果code等于HC_ACTION,表示消息中包含按鍵消息;如果為WM_KEYDOWN,則顯示按鍵對(duì)應(yīng)的文本。
將該DLL文件編譯連接。為了測(cè)試該DLL文件,新建一個(gè)MFC的Dialog工程,添加兩個(gè)按鈕,如圖1所示。
圖1 鍵盤(pán)鉤子的測(cè)試程序
分別對(duì)這兩個(gè)按鈕添加代碼,具體如下:
- void CHookTestDlg::OnButton1()
- {
- // 在這里添加控制通知的處理程序
- SetHookOn();
- }
- void CHookTestDlg::OnButton2()
- {
- // 在這里添加控制通知的處理程序
- SetHookOff();
- }
直接調(diào)用DLL文件導(dǎo)出這兩個(gè)函數(shù),不過(guò)在使用之前要先對(duì)這兩個(gè)函數(shù)進(jìn)行聲明,否則編譯器因無(wú)法找到這兩個(gè)函數(shù)的原型而導(dǎo)致連接失敗。定義如下:
- extern "C" VOID SetHookOn();
- extern "C" VOID SetHookOff();
進(jìn)行編譯連接,提示出錯(cuò),內(nèi)容如下:
- Linking...
- HookTestDlg.obj : error LNK2001: unresolved external symbol _SetHookOn
- HookTestDlg.obj : error LNK2001: unresolved external symbol _SetHookOff
- Debug/HookTest.exe : fatal error LNK1120: 2 unresolved externals
- Error executing link.exe.
- HookTest.exe - 3 error(s), 0 warning(s)
從給出的提示可以看出是連接錯(cuò)誤,找不到外部的符號(hào)。將DLL編譯連接后生成的DLL文件和LIB文件都復(fù)制到測(cè)試工程的目錄下,并將LIB文件添加到工程中。在代碼中添加如下語(yǔ)句:
- #pragma comment (lib, KeyBoradHookTest)
再次連接,成功!
運(yùn)行測(cè)試程序,并單擊“HookOn”按鈕,隨便按下鍵盤(pán)上的任意一個(gè)鍵,會(huì)出現(xiàn)提示對(duì)話框,如圖2所示。
圖2 截獲到的鍵盤(pán)輸入
從圖2中可以看出,當(dāng)按下鍵盤(pán)上的按鍵時(shí),程序?qū)⒉东@到按鍵。到此,鍵盤(pán)鉤子的例子程序就完成了。
2. 低級(jí)鍵盤(pán)鉤子
數(shù)據(jù)防泄露軟件通常會(huì)禁止PrintScreen鍵,防止通過(guò)截屏將數(shù)據(jù)保存為圖片而導(dǎo)致泄密。這類(lèi)軟件想要實(shí)現(xiàn)是比較簡(jiǎn)單的,但是想要將功能做得強(qiáng)大些,還是需要下功夫的。數(shù)據(jù)防泄露的軟件除了要有兼容性好的底層驅(qū)動(dòng)的設(shè)計(jì),還要有完善的規(guī)則設(shè)置。此外就是需要軟件安全人員對(duì)其進(jìn)行各種各樣的攻擊,避免數(shù)據(jù)防泄露軟件因?yàn)楦鞣N各樣的原因而被心懷惡意者突破。
這里介紹如何禁止PrintScreen鍵。其實(shí)很簡(jiǎn)單,只要安裝低級(jí)鍵盤(pán)鉤子(WH_KEYBO ARD_LL)就可以搞定。普通的鍵盤(pán)鉤子(WH_KEYBOARD)是無(wú)法過(guò)濾一些系統(tǒng)按鍵的。在低級(jí)鍵盤(pán)鉤子的回調(diào)函數(shù)中,判斷是否為PrintScreen鍵,如果是,則直接返回TRUE(前面提到,如果想屏蔽某個(gè)消息的話,那么在鉤子函數(shù)中對(duì)該消息進(jìn)行處理后,直接返回一個(gè)非零值),如果不是,則傳遞給鉤子鏈的下一處。
代碼如下:
- extern "C" __declspec(dllexport) BOOL SetHookOn()
- {
- if ( g_hHook != NULL )
- {
- return FALSE;
- }
- g_hHook = SetWindowsHookEx(WH_KEYBOARD_LL, LowLevelKeyboardProc, g_hIns, NULL);
- if ( NULL == g_hHook )
- {
- MessageBox(NULL, "安裝鉤子出錯(cuò) !", "error", MB_ICONSTOP);
- return FALSE;
- }
- return TRUE;
- }
- extern "C" __declspec(dllexport) BOOL SetHookOff()
- {
- if ( g_hHook == NULL )
- {
- return FALSE;
- }
- UnhookWindowsHookEx(g_hHook);
- g_hHook = NULL;
- return TRUE;
- }
- LRESULT CALLBACK LowLevelKeyboardProc(
- int nCode, WPARAM wParam, LPARAM lParam)
- {
- KBDLLHOOKSTRUCT *Key_Info = (KBDLLHOOKSTRUCT*)lParam;
- if ( HC_ACTION == nCode )
- {
- if ( WM_KEYDOWN == wParam || WM_SYSKEYDOWN == wParam )
- {
- if ( Key_Info->vkCode == VK_SNAPSHOT )
- {
- return TRUE;
- }
- }
- }
- return CallNextHookEx(g_hHook, nCode, wParam, lParam);
- }
代碼量非常短,然而,就是這短短的代碼阻止了數(shù)據(jù)的泄露。當(dāng)然,對(duì)于一個(gè)攻擊者來(lái)說(shuō),這個(gè)代碼無(wú)法保護(hù)數(shù)據(jù),這種保護(hù)也就很脆弱了。任何的保護(hù)都有突破的辦法,攻擊無(wú)處不在,攻擊者會(huì)嘗試任何手段突破所有的保護(hù)。
3. 使用鉤子進(jìn)行DLL注入
Windows提供的鉤子類(lèi)型非常多,其中一種類(lèi)型的鉤子非常實(shí)用,那就是WH_GETME SSAGE鉤子。它可以很方便地將DLL文件注入到所有的基于消息機(jī)制的程序中。
在有些情況下,需要DLL文件完成一些功能,但是完成功能時(shí)需要DLL在目標(biāo)進(jìn)程的空間中。這時(shí),就需要使用WH_GETMESSAGE消息把DLL注入到目標(biāo)的進(jìn)程中。代碼非常簡(jiǎn)單,這里直接給出DLL文件的代碼,具體如下:
- #include <windows.h>
- extern "C" __declspec(dllexport) VOID SetHookOn();
- extern "C" __declspec(dllexport) VOID SetHookOff();
- HHOOK g_HHook = NULL;
- HINSTANCE g_hInst = NULL;
- VOID DoSomeThing()
- {
- /*
- ……
- 自己要實(shí)現(xiàn)功能的代碼
- ……
- */
- }
- BOOL WINAPI DllMain(
- HINSTANCE hinstDLL, // DLL 模塊的句柄
- DWORD fdwReason, // 調(diào)用函數(shù)的原因
- LPVOID lpvReserved // 保留的
- )
- {
- switch ( fdwReason )
- {
- case DLL_PROCESS_ATTACH:
- {
- g_hInst = hinstDLL;
- DoSomeThing();
- break;
- }
- }
- return TRUE;
- }
- LRESULT CALLBACK GetMsgProc(
- int code, // 鉤子編碼
- WPARAM wParam, // 移除選項(xiàng)
- LPARAM lParam // 消息
- )
- {
- return CallNextHookEx(g_HHook, code, wParam, lParam);
- }
- VOID SetHookOn()
- {
- g_HHook = SetWindowsHookEx(WH_GETMESSAGE, GetMsgProc, g_hInst, 0);
- }
- VOID SetHookOff()
- {
- UnhookWindowsHookEx(g_HHook);
- }
整個(gè)代碼就是這樣。只要知道,在需要DLL大范圍地注入到基于消息的進(jìn)程中時(shí),可以使用這種方法。