穆文烨
基于Windows NT/2000 的应用系统中,一些关键的人机交互工作站,常需要了解并记录所有操作人员操作计算机的情况。如: 在工业控制领域,一些使用计算机对设备进行监视和控制的工作站,需要非常高的可靠性和安全性。在这些工作站上通常要求只能运行系统所要求的应用程序,不能运行与系统无关的程序,同时要求对计算机操作人员的所有原始输入进行记录,以便出现事故(如操作故障、程序异常退出)的时候,用来分析是人为原因,还是系统原因造成的。
基于以上需求,我们必须解决两个问题,一个是如何记录操作人员的输入,另一个是如何控制操作人员的输入。在DOS、Windows 3.1、Windows 95/98中都可以编写响应键盘和鼠标输入的中断处理程序,截取来自键盘和鼠标的输入,记录、分析后依情况分别处理。但是在Windows NT和 Windows 2000中,这样的解决方法将不再行得通,这是因为Windows NT/2000操作系统为了提高系统的可靠性,不再允许应用程序直接对系统设备的底层进行操作。这样,用户的应用程序将不能够对计算机的端口地址进行读写操作,所以在Windows NT/2000操作系统中对计算机端口的读写是无效的。另外一种方法能够非常完美地解决这个问题,就是可以编写操作系统的设备驱动程序来解决,但是要编写系统的设备驱动程序,必须对Windows NT/2000的系统底层以及整个系统架构有比较深入的了解。而且设备驱动程序的编写、调试都比较困难,同时这方面的资料也比较少。所以本文没有采取这种方法,而是采用微软公布的标准Win32 函数和钩子技术来解决这个问题,比较方便而且快捷。
在Windows NT/2000 操作系统中,称各种输入为事件(Event),所有的键盘、鼠标输入事件以及其他事件都是通过消息传递处理机制来得到响应的。控制、监视计算机实际上是控制、监视事件消息流。Windows操作系统为这种应用提供钩子(Hook)技术。这种技术的要点就是在操作系统的消息传递处理机制上外挂一个我们定义的函数,可以使用这个函数来监视、控制系统的事件消息流。本文采用的就是这种方法,这种方法要求将所有的程序代码放入系统可以加载的动态链接库中。下面我们以键盘输入的监视和控制为例详细叙述这种方法。其总体思路如下:
首先,定义自己的钩子函数。
其次,安装自定义的钩子函数,此后钩子函数在后台开始工作。一旦系统发现击键动作或者鼠标动作,系统将马上调用该自定义的钩子函数,并将事件消息传入,供程序分析判断。它可以监视所有的击键和鼠标动作,与DOS 时代的中断调用有非常相似的地方。
最后,卸载自定义的钩子函数。
钩子函数 的定义
微软的钩子技术的原理就是应用程序可以在系统的消息处理机制上外挂一个子程序,在消息尚未到达目的地之前,用该子程序来截获此消息,以进行监视和控制。我们这里使用的是WH_KEYBOARD_LL类型的钩子函数,这种类型的钩子函数可以截获所有的键盘事件,即敲击键盘上的任何一个键,我们自定义的钩子函数都可以知道。该类型钩子函数要求安装自定义的钩子函数必须是以下原型:
LRESULT CALLBACK LowLevelKeyboard-
Proc(
int nCode,
WPARAM wParam,
LPARAM lParam
);
其中各参数的含义如下:
int nCode: 用来决定钩子函数如何处理事件消息的代码,参数的取值为HC_ACTION时,参数wParam、lParam包含了所需的键盘消息事件信息。
WPARAM wParam: 键盘消息事件的类型ID。该参数有四种可能的消息类型取值:WM_KEY-
DOWN,WM_KEYUP,WM_SYSKEYDOWN, WM_SYSKEYUP.
LPARAM lParam: 指向一个类型为KBDLLHOOKSTRUCT的结构指针。该结构容纳了底层键盘输入事件的详细信息,它的定义如下:
typedef struct tagKBDLLHOOKSTRUCT {
DWORD vkCode;
//一个范围从1到254的虚拟键码
DWORD scanCode;
// 键盘的硬件扫描码
DWORD flags;
// 一系列的标志位
//0比特位指示该键是不是扩展键(如: 功能键,或数字小键盘上的键),1表示是,0表示否
//1~3比特位保留
//4比特位用来区分该事件是否来自Win32 函数keybd_event()调用,1表示是,0表示否
//5比特位为状态描述码,如果ALT键按下,该位是1,否则是0。
//6比特位保留。
//7比特位是变换状态位,键被按下为0,键被释放为1。
DWORD time;
// 该消息事件的时间标记。
DWORD dwExtraInfo;
// 该消息的其他扩展信息。
}KBDLLHOOKSTRUCT, FAR *LPKBDLLHOOK
STRUCT, *PKBDLLHOOKSTRUCT;
实际的钩子函数的框架如下:
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;
//系统传递来的键盘输入事件信息指针
switch (wParam) {
case WM_SYSKEYUP:
case WM_KEYUP: //if key up
/*这段代码用来获得当前拥有输入焦点的窗口的窗口句柄,以便获得该窗口的相关信息*/
/*获得前端窗口(即用户当前正在工作的窗口)的句柄,创建该窗口的线程通常拥有比其他线程稍微高一些的优先级。*/
hwndForegroud=::GetForegroundWindow(); dwCurrentThreadId=::GetCurrentThreadId(); //当前线程的Id
//获得产生前端窗口hwndForeground的线程Id值,用来惟一表示一个线程
dwWindowThreadId=::GetWindowThread-
ProcessId(hwndForegroud,NULL);
/*下面这一行代码非常重要,它的作用是使当前线程(dwCurrentThreadId)的输入处理机制依附到创建前端窗口的线程(dwWindowThreadId)的输入机制上,否则你将得不到当前拥有键盘输入焦点的窗口句柄。这是因为在Windows NT/2000操作系统通常创建不同的线程来处理相互独立的输入过程,每一个输入过程都拥有自己的输入状态(焦点、键盘状态、队列状态等),通过AttachThreadInput调用,操作系统将允许调用线程获得或者设置其他线程生成窗口的输入状态信息。只有执行该系统调用,才能够得到当前拥有键盘输入焦点的窗口的窗口句柄,否则GetFocus()系统调用将返回NULL。在这一点上Windows NT/2000与Windows 9X操作系统有很大不同,这也正是Windows NT/2000比Windows 95/98 操作系统可靠性、安全性更好的一个原因*/
AttachThreadInput(dwCurrentThreadId , dwWindowThreadId,TRUE); //
//获得拥有键盘输入焦点的窗口的窗口句柄
hwndFocus =::GetFocus();
if(hwndFocus || hwndForeground)
{
char wnm[256];
wnm[0]=0;
//变量定义后,使用之前一定要初始化。
//获得该窗口的窗口标题,就是在窗口标题栏上显示的内容
:: 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系统键(就是带有微软旗帜图案的那个键,注意是左边的那个)屏蔽掉,如果在代码执行后,用户敲击键盘上的左Win系统键,将不会弹出Windows的开始菜单。你可以根据你的需要屏蔽任何你所要屏蔽的键,从而达到控制计算机使用的目的。*/
bSkipThisEvent = TRUE;
break;
}end of switch
}//end of if
if(bSkipThisEvent)//如果是需要屏蔽的键,一定要返回1给操作系统,切记。
return 1;
else
return CallNextHookEx(NULL,nCode,wParam,lParam);
/*调用钩子函数链,以便使其他应用程序能够正常工作*/
}
这里需要注意的是,如果nCode 小于零,钩子函数必须返回CallNextHookEx函数调用的返回值。如果nCode>=0,建议仍然返回CallNextHookEx函数调用的返回值,否则其他安装了WH_KEYBOARD_LL钩子函数的应用程序将收不到系统发送的钩子通知,从而使其他应用程序功能发生异常。不过我们也可以利用系统的这一个特点,来屏蔽一些功能键,禁止某些系统特性,实现控制计算机使用的想法。
安装 钩子函数
接下来的工作就是将我们定义好的钩子函数安装到系统中。用来安装用户自定义钩子函数的Win32函数是SetWindowsHookEx,该函数的原型如下:
HHOOK SetWindowsHookEx(
int idHook,
//将要安装的钩子函数的钩子类型
HOOKPROC lpfn,
//我们自定义的钩子函数的函数地址
HINSTANCE hMod,
//应用程序的实例句柄,即容纳了钩子函数的动态链接库的句柄。如果钩子函数所在地址空间在当前进程的地址空间,hMod 应该为NULL.
DWORD dwThreadId
//钩子起作用的线程Id,如果该值为零,则对系统中所有线程都起作用
);
其中idHook指定了安装的钩子函数的类型,不同类型的钩子函数可完成不同应用功能,而且不同版本的Windows操作系统支持的钩子函数的种类也不尽相同,在本文我们主要介绍的是Windows NT/2000操作系统中目前公开支持的最底层的两种钩子类型:WH_KEYBOARD_LL和WH_MOUSE_LL。这两种类型的钩子函数在Windows NT Services Pack 3及其以后的版本,包括Windows 2000 Professional中得到很好的支持。这两种类型的钩子函数可以分别监视底层的键盘和鼠标输入事件,在系统将事件分发到相应的接收目的地之前将它截获,交给用户自定义的钩子函数来处理。下面以键盘钩子为例详细说明,鼠标钩子与此类似,不再赘谈。
在这里,我们的安装函数和我们定义的钩子函数放在同一个动态链接库中。其中g_hWin32NT_
DllHandle是我们定义的全局变量,类型是Handle,在系统调用动态链接库的入口函数时,将hModule的值赋给g_hWin32NT_DllHandle。
//下面是动态链接库的入口函数
DllMain( HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved )
{
g_hWin32NT_DllHandle = hModule;
/*保存该值,以后在安装自定义钩子函数的时候要使用该值*/
return TRUE;
}
//下面的函数用来安装我们自定义的钩子函数
HHOOK g_hhkLowLevelKybd ;
//以后在卸载自定义钩子函数时,要用到该值
void StartMyHook(void)
{
g_hhkLowLevelKybd = SetWindowsHook
Ex(WH_KEYBOARD_LL,
::MyLowLevelKeyboardProc, (HINSTANCE)g_hWin32NT_DllHandle, 0);
}
/*g_hWin32NT_DllHandle是动态链接库的句柄,我们定义的钩子函数放在该动态链接库中。该句柄是在操作系统加载动态链接库时,由操作系统调用DllMain()传入的*/
如果安装成功,系统调用将返回一个钩子函数的句柄;如果失败,将返回NULL。将来在卸载我们自定义的钩子函数时要使用该句柄。所以必须将返回值保存到一个全局变量中。安装成功后,该函数返回。从现在开始,键盘的任何击键动作都将被我们定义的钩子函数捕捉到,包括各种系统功能键。操作系统在后台将自动异步地调用我们自定义的钩子函数进行处理,并且不会影响任何当前正在进行的各种工作,也不会对系统有任何不良影响。
卸载 钩子函数
当我们的应用程序退出时,或者不再需要钩子函数的处理时,必须卸载我们自定义的钩子函数。
//下面的Win32函数用来卸载我们自定义的钩子函数
UnhookWindowsHookEx(g_hhkLowLevel
Kybd);
至此,我们已经比较完整地介绍了底层键盘钩子技术的应用方法,对于鼠标输入事件的控制与监视的方法,与此完全类似,读者可依照本文完成自己的功能。并且在你的应用程序中,可以根据需要,多次安装、卸载钩子函数。 不过有一点读者要注意,就是在程序调试时,一定不要同时将这两种类型的钩子函数都进行设置,因为一旦程序处理上有错误,操作系统将不能获得任何操作消息,计算机只有重新启动。读者可以将本文介绍的方法应用到许多场合,比如在运行某些关键的任务时,为避免人机交互的干扰,可以锁定键盘和鼠标的输入。待任务完成时,再恢复正常的键盘和鼠标的输入。另外,屏蔽某些不想让用户使用的功能键等等。