- 相關推薦
監(jiān)視、控制計算機的使用
基于Windows NT/2000 的應用系統(tǒng)中,一些關鍵的人機交互工作站,常需要了解并記錄所有操作人員操作計算機的情況。如: 在工業(yè)控制領域,一些使用計算機對設備進行監(jiān)視和控制的工作站,需要非常高的可靠性和安全性。在這些工作站上通常要求只能運行系統(tǒng)所要求的應用程序,不能運行與系統(tǒng)無關的程序,同時要求對計算機操作人員的所有原始輸入進行記錄,以便出現(xiàn)事故(如操作故障、程序異常退出)的時候,用來分析是人為原因,還是系統(tǒng)原因造成的。 基于以上需求,我們必須解決兩個問題,一個是如何記錄操作人員的輸入,另一個是如何控制操作人員的輸入。在DOS、Windows 3.1、Windows 95/98中都可以編寫響應鍵盤和鼠標輸入的中斷處理程序,截取來自鍵盤和鼠標的輸入,記錄、分析后依情況分別處理。但是在Windows NT和 Windows 2000中,這樣的解決方法將不再行得通,這是因為Windows NT/2000操作系統(tǒng)為了提高系統(tǒng)的可靠性,不再允許應用程序直接對系統(tǒng)設備的底層進行操作。這樣,用戶的應用程序?qū)⒉荒軌驅(qū)τ嬎銠C的端口地址進行讀寫操作,所以在Windows NT/2000操作系統(tǒng)中對計算機端口的讀寫是無效的。另外一種方法能夠非常完美地解決這個問題,就是可以編寫操作系統(tǒng)的設備驅(qū)動程序來解決,但是要編寫系統(tǒng)的設備驅(qū)動程序,必須對Windows NT/2000的系統(tǒng)底層以及整個系統(tǒng)架構(gòu)有比較深入的了解。而且設備驅(qū)動程序的編寫、調(diào)試都比較困難,同時這方面的資料也比較少。所以本文沒有采取這種方法,而是采用微軟公布的標準Win32 函數(shù)和鉤子技術來解決這個問題,比較方便而且快捷。 在Windows NT/2000 操作系統(tǒng)中,稱各種輸入為事件(Event),所有的鍵盤、鼠標輸入事件以及其他事件都是通過消息傳遞處理機制來得到響應的?刂、監(jiān)視計算機實際上是控制、監(jiān)視事件消息流。Windows操作系統(tǒng)為這種應用提供鉤子(Hook)技術。這種技術的要點就是在操作系統(tǒng)的消息傳遞處理機制上外掛一個我們定義的函數(shù),可以使用這個函數(shù)來監(jiān)視、控制系統(tǒng)的事件消息流。本文采用的就是這種方法,這種方法要求將所有的程序代碼放入系統(tǒng)可以加載的動態(tài)鏈接庫中。下面我們以鍵盤輸入的監(jiān)視和控制為例詳細敘述這種方法。其總體思路如下: 首先,定義自己的鉤子函數(shù)。 其次,安裝自定義的鉤子函數(shù),此后鉤子函數(shù)在后臺開始工作。一旦系統(tǒng)發(fā)現(xiàn)擊鍵動作或者鼠標動作,系統(tǒng)將馬上調(diào)用該自定義的鉤子函數(shù),并將事件消息傳入,供程序分析判斷。它可以監(jiān)視所有的擊鍵和鼠標動作,與DOS 時代的中斷調(diào)用有非常相似的地方。 最后,卸載自定義的鉤子函數(shù)。 鉤子函數(shù) 的定義 微軟的鉤子技術的原理就是應用程序可以在系統(tǒng)的消息處理機制上外掛一個子程序,在消息尚未到達目的地之前,用該子程序來截獲此消息,以進行監(jiān)視和控制。我們這里使用的是WH_KEYBOARD_LL類型的鉤子函數(shù),這種類型的鉤子函數(shù)可以截獲所有的鍵盤事件,即敲擊鍵盤上的任何一個鍵,我們自定義的鉤子函數(shù)都可以知道。該類型鉤子函數(shù)要求安裝自定義的鉤子函數(shù)必須是以下原型: LRESULT CALLBACK LowLevelKeyboard- Proc( int nCode, WPARAM wParam, LPARAM lParam ); 其中各參數(shù)的含義如下: int nCode: 用來決定鉤子函數(shù)如何處理事件消息的代碼,參數(shù)的取值為HC_ACTION時,參數(shù)wParam、lParam包含了所需的鍵盤消息事件信息。 WPARAM wParam: 鍵盤消息事件的類型ID。該參數(shù)有四種可能的消息類型取值:WM_KEY- DOWN,WM_KEYUP,WM_SYSKEYDOWN, WM_SYSKEYUP. LPARAM lParam: 指向一個類型為KBDLLHOOKSTRUCT的結(jié)構(gòu)指針。該結(jié)構(gòu)容納了底層鍵盤輸入事件的詳細信息,它的定義如下: typedef struct tagKBDLLHOOKSTRUCT { DWORD vkCode; //一個范圍從1到254的虛擬鍵碼 DWORD scanCode; // 鍵盤的硬件掃描碼 DWORD flags; // 一系列的標志位 //0比特位指示該鍵是不是擴展鍵(如: 功能鍵,或數(shù)字小鍵盤上的鍵),1表示是,0表示否 //1~3比特位保留 //4比特位用來區(qū)分該事件是否來自Win32 函數(shù)keybd_event()調(diào)用,1表示是,0表示否 //5比特位為狀態(tài)描述碼,如果ALT鍵按下,該位是1,否則是0。 //6比特位保留。 //7比特位是變換狀態(tài)位,鍵被按下為0,鍵被釋放為1。 DWORD time; // 該消息事件的時間標記。 DWORD dwExtraInfo; // 該消息的其他擴展信息。 }KBDLLHOOKSTRUCT, FAR *LPKBDLLHOOK STRUCT, *PKBDLLHOOKSTRUCT; 實際的鉤子函數(shù)的框架如下: LRESULT CALLBACK MyLowLevelKeyboard Proc(int nCode,WPARAM wParam,LPARAM lParam) { BOOL bSkipThisEvent = FALSE; HWND hwndForeground; HWND hwndFocus; DWORD dwCurrentThreadId; DWORD dwWindowThreadId; if (nCode == HC_ACTION) { PKBDLLHOOKSTRUCT p = (PKBDLLHOOKSTRUCT) lParam; //系統(tǒng)傳遞來的鍵盤輸入事件信息指針 switch (wParam) { case WM_SYSKEYUP: case WM_KEYUP: //if key up /*這段代碼用來獲得當前擁有輸入焦點的窗口的窗口句柄,以便獲得該窗口的相關信息*/ /*獲得前端窗口(即用戶當前正在工作的窗口)的句柄,創(chuàng)建該窗口的線程通常擁有比其他線程稍微高一些的優(yōu)先級。*/ hwndForegroud=::GetForegroundWindow(); dwCurrentThreadId=::GetCurrentThreadId(); //當前線程的Id //獲得產(chǎn)生前端窗口hwndForeground的線程Id值,用來惟一表示一個線程 dwWindowThreadId=::GetWindowThread- ProcessId(hwndForegroud,NULL); /*下面這一行代碼非常重要,它的作用是使當前線程(dwCurrentThreadId)的輸入處理機制依附到創(chuàng)建前端窗口的線程(dwWindowThreadId)的輸入機制上,否則你將得不到當前擁有鍵盤輸入焦點的窗口句柄。這是因為在Windows NT/2000操作系統(tǒng)通常創(chuàng)建不同的線程來處理相互獨立的輸入過程,每一個輸入過程都擁有自己的輸入狀態(tài)(焦點、鍵盤狀態(tài)、隊列狀態(tài)等),通過AttachThreadInput調(diào)用,操作系統(tǒng)將允許調(diào)用線程獲得或者設置其他線程生成窗口的輸入狀態(tài)信息。只有執(zhí)行該系統(tǒng)調(diào)用,才能夠得到當前擁有鍵盤輸入焦點的窗口的窗口句柄,否則GetFocus()系統(tǒng)調(diào)用將返回NULL。在這一點上Windows NT/2000與Windows 9X操作系統(tǒng)有很大不同,這也正是Windows NT/2000比Windows 95/98 操作系統(tǒng)可靠性、安全性更好的一個原因*/ AttachThreadInput(dwCurrentThreadId , dwWindowThreadId,TRUE); // //獲得擁有鍵盤輸入焦點的窗口的窗口句柄 hwndFocus =::GetFocus(); if(hwndFocus || hwndForeground) { char wnm[256]; wnm[0]=0; //變量定義后,使用之前一定要初始化。 //獲得該窗口的窗口標題,就是在窗口標題欄上顯示的內(nèi)容 :: GetWindowText(hwndForeground,wnm,255); char clsnm[266]; clsnm[0]=0; //獲得該窗口的類名字 :: GetClassName(hwndFocus,clsnm,255); //獲得該窗口的風格 LONG style=::GetWindowLong(hwfocus,GWL_STYLE); /*如果你只對部分窗口感興趣的話,可以通過下面的形式進行過濾, 從而只處理你所感興趣的某些窗口的輸入事件*/ if((stricmp(clsnm,“edit”)==0 ) || (0x0020 &style) || strstr(wnm,“MyTest Wnd”)) {//你可以在此記錄供以后分析使用的相關信息 LLKEY_OutputToLog(hwfore,hwfocus,wnm,clsnm,p); } } case WM_KEYDOWN: case WM_SYSKEYDOWN: if (p->vkCode == VK_LWIN)
/*該行代碼用來將鍵盤上的左Win系統(tǒng)鍵(就是帶有微軟旗幟圖案的那個鍵,注意是左邊的那個)屏蔽掉,如果在代碼執(zhí)行后,用戶敲擊鍵盤上的左Win系統(tǒng)鍵,將不會彈出Windows的開始菜單。你可以根據(jù)你的需要屏蔽任何你所要屏蔽的鍵,從而達到控制計算機使用的目的。*/ bSkipThisEvent = TRUE; break; }end of switch }//end of if if(bSkipThisEvent)//如果是需要屏蔽的鍵,一定要返回1給操作系統(tǒng),切記。 return 1; else return CallNextHookEx(NULL,nCode,wParam,lParam); /*調(diào)用鉤子函數(shù)鏈,以便使其他應用程序能夠正常工作*/ } 這里需要注意的是,如果nCode 小于零,鉤子函數(shù)必須返回CallNextHookEx函數(shù)調(diào)用的返回值。如果nCode>=0,建議仍然返回CallNextHookEx函數(shù)調(diào)用的返回值,否則其他安裝了WH_KEYBOARD_LL鉤子函數(shù)的應用程序?qū)⑹詹坏较到y(tǒng)發(fā)送的鉤子通知,從而使其他應用程序功能發(fā)生異常。不過我們也可以利用系統(tǒng)的這一個特點,來屏蔽一些功能鍵,禁止某些系統(tǒng)特性,實現(xiàn)控制計算機使用的想法。 安裝 鉤子函數(shù) 接下來的工作就是將我們定義好的鉤子函數(shù)安裝到系統(tǒng)中。用來安裝用戶自定義鉤子函數(shù)的Win32函數(shù)是SetWindowsHookEx,該函數(shù)的原型如下: HHOOK SetWindowsHookEx( int idHook, //將要安裝的鉤子函數(shù)的鉤子類型 HOOKPROC lpfn, //我們自定義的鉤子函數(shù)的函數(shù)地址 HINSTANCE hMod, //應用程序的實例句柄,即容納了鉤子函數(shù)的動態(tài)鏈接庫的句柄。如果鉤子函數(shù)所在地址空間在當前進程的地址空間,hMod 應該為NULL. DWORD dwThreadId //鉤子起作用的線程Id,如果該值為零,則對系統(tǒng)中所有線程都起作用 ); 其中idHook指定了安裝的鉤子函數(shù)的類型,不同類型的鉤子函數(shù)可完成不同應用功能,而且不同版本的Windows操作系統(tǒng)支持的鉤子函數(shù)的種類也不盡相同,在本文我們主要介紹的是Windows NT/2000操作系統(tǒng)中目前公開支持的最底層的兩種鉤子類型:WH_KEYBOARD_LL和WH_MOUSE_LL。這兩種類型的鉤子函數(shù)在Windows NT Services Pack 3及其以后的版本,包括Windows 2000 Professional中得到很好的支持。這兩種類型的鉤子函數(shù)可以分別監(jiān)視底層的鍵盤和鼠標輸入事件,在系統(tǒng)將事件分發(fā)到相應的接收目的地之前將它截獲,交給用戶自定義的鉤子函數(shù)來處理。下面以鍵盤鉤子為例詳細說明,鼠標鉤子與此類似,不再贅談。 在這里,我們的安裝函數(shù)和我們定義的鉤子函數(shù)放在同一個動態(tài)鏈接庫中。其中g_hWin32NT_ DllHandle是我們定義的全局變量,類型是Handle,在系統(tǒng)調(diào)用動態(tài)鏈接庫的入口函數(shù)時,將hModule的值賦給g_hWin32NT_DllHandle。 //下面是動態(tài)鏈接庫的入口函數(shù) DllMain( HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ) { g_hWin32NT_DllHandle = hModule; /*保存該值,以后在安裝自定義鉤子函數(shù)的時候要使用該值*/ return TRUE; } //下面的函數(shù)用來安裝我們自定義的鉤子函數(shù) HHOOK g_hhkLowLevelKybd ; //以后在卸載自定義鉤子函數(shù)時,要用到該值 void StartMyHook(void) { g_hhkLowLevelKybd = SetWindowsHook Ex(WH_KEYBOARD_LL, ::MyLowLevelKeyboardProc, (HINSTANCE)g_hWin32NT_DllHandle, 0); } /*g_hWin32NT_DllHandle是動態(tài)鏈接庫的句柄,我們定義的鉤子函數(shù)放在該動態(tài)鏈接庫中。該句柄是在操作系統(tǒng)加載動態(tài)鏈接庫時,由操作系統(tǒng)調(diào)用DllMain()傳入的*/ 如果安裝成功,系統(tǒng)調(diào)用將返回一個鉤子函數(shù)的句柄;如果失敗,將返回NULL。將來在卸載我們自定義的鉤子函數(shù)時要使用該句柄。所以必須將返回值保存到一個全局變量中。安裝成功后,該函數(shù)返回。從現(xiàn)在開始,鍵盤的任何擊鍵動作都將被我們定義的鉤子函數(shù)捕捉到,包括各種系統(tǒng)功能鍵。操作系統(tǒng)在后臺將自動異步地調(diào)用我們自定義的鉤子函數(shù)進行處理,并且不會影響任何當前正在進行的各種工作,也不會對系統(tǒng)有任何不良影響。 卸載 鉤子函數(shù) 當我們的應用程序退出時,或者不再需要鉤子函數(shù)的處理時,必須卸載我們自定義的鉤子函數(shù)。 //下面的Win32函數(shù)用來卸載我們自定義的鉤子函數(shù) UnhookWindowsHookEx(g_hhkLowLevel Kybd); 至此,我們已經(jīng)比較完整地介紹了底層鍵盤鉤子技術的應用方法,對于鼠標輸入事件的控制與監(jiān)視的方法,與此完全類似,讀者可依照本文完成自己的功能。并且在你的應用程序中,可以根據(jù)需要,多次安裝、卸載鉤子函數(shù)。 不過有一點讀者要注意,就是在程序調(diào)試時,一定不要同時將這兩種類型的鉤子函數(shù)都進行設置,因為一旦程序處理上有錯誤,操作系統(tǒng)將不能獲得任何操作消息,計算機只有重新啟動。讀者可以將本文介紹的方法應用到許多場合,比如在運行某些關鍵的任務時,為避免人機交互的干擾,可以鎖定鍵盤和鼠標的輸入。待任務完成時,再恢復正常的鍵盤和鼠標的輸入。另外,屏蔽某些不想讓用戶使用的功能鍵等等。