{三}简单的消息Spy的实现
凡写过一些程序的人,大都用过VS的Spy++工具,非常好用。Delphi也有一个类似的工具叫WinSignt32,说实话,比Spy++可是差远了。这一篇将介绍如何实现一个简单的消息Spy工具,其功能大概类似于Spy++的Spy Message,以此来学习全局钩子的用法。说点题外话,这些知识都是笔者经过实践学习而得来,其间不乏屡遭碰壁者,因此想写出来,帮助有兴趣者更快地学习这些知识。
首先得介绍于消息相关的几个钩子,主要是:
WH_GETMESSAGE:应用程序使用WH_GETMESSAGE Hook来监视从GetMessage 或者 PeekMessage函数返回的消息。你可以使用WH_GETMESSAGE Hook去监视鼠标和键盘输入,以及其他发送到消息队列中的消息。
WH_CALLWNDPROC和WH_CALLWNDPROCRET:WH_CALLWNDPROC和WH_CALLWNDPROCRET Hooks使你可以监视发送到窗口过程的消息。系统在消息发送到接收窗口过程之前调用WH_CALLWNDPROC函数过程,并且在窗口过程处理完消息之后调用WH_CALLWNDPROCRET函数过程。
上面说得很明白了,WH_GETMESSAGE相当于钩住了PostMessage发送的消息,而另外两个相当于钩住了SendMessage发送的消息。
有了这三个钩子,已经足够Spy所有的消息了。至于三个钩子的钩子过滤函数,可以参见MSDN的GetMsgProc和CallWndProc以及CallWndRetProc的说明,相信通过上面的讲解,应用容易这几个过程的参数的含义。而这并不是这一篇的重点。
我们知道的一点常识是,全局钩子必须安装在DLL中,但说到实际的应用,其实也并非这么简单。最好了解一下一些运行机制:
在Win16环境中,DLL的全局数据对每个载入它的进程来说都是相同的;而在Win32环境中,情况却发生了变化,DLL函数中的代码所创建的任何对象(包括变量)都归调用它的线程或进程所有。当进程在载入DLL时,操作系统自动把DLL地址映射到该进程的私有空间,也就是进程的虚拟地址空间,而且也复制该DLL的全局数据的一份拷贝到该进程空间。也就是说每个进程所拥有的相同的DLL的全局数据,它们的名称相同,但其值却并不一定是相同的,而且是互不干涉的。
好好的理解一下上面的这一段话。全局钩子安装在DLL中,我们假设DLL中有一个API叫StartHook,其中调用了SetWindowsHookEx函数。当某一个进程调用DLL中的StartHook时,钩子开始安装,实际上它是把DLL注入到了所有的进程中(DLL注入中的一个方法即是利用全局钩子的技术。详见《Window高级编程》),这样全局钩子才能够监视到系统级别的消息。而上面的话说得很明白,每一个进程都自己的地址空间,且互不干涉。
这就带来了一个难题,即数据共享。让我来举个例子。假设我们要监视某一个窗口的消息,则假设调用DLL中的一个函数:StartSpyMessage,向这个函数传入将被监视的窗口的句柄。而StartSpyMessage函数中将该句柄保存在DLL中的一个全局变量中,接着在钩子过程里判断截获的消息结构中的句柄值是否和该全局变量相等,相等则发送消息通知外部。这一个操作过程是在某一个进程中完成的。现在问题来了,当DLL被注入到其他进程中时,保存窗口句柄的变量并没有得到共享,它是0。因此,就形成了一个现象:当监视自已程序的窗口时,没有问题,因为自己进程的DLL的句柄变量得到赋值,所以可以判断;但监视其他程序的窗口时,不行,因为其他进程的该变量是0,判断时就不对了。就好象不是全局钩子一样。
说了这么多,不知读者是否明白。总之,我们需要一种共享技术,让DLL中的变量在所有进程中都是一样的。另人羡慕的是:C++中#pragma data_seg预处理指令用于设置共享数据段。但Delphi没有,我们只能用内存映射文件来达到目的。
利用CreateFileMapping,OpenFileMapping,MapViewOfFile等几个API取得一段共享内存(具体用法看帮助说明吧,这里实在没有篇幅来讲了),让所有进程都能共享它们。最后用完,要用UnMapViewOfFile,CloseHandle等API释放共享内存。
解决了第一个数据共享问题,还有另一个问题等解决。当截获了一个消息时,怎么样把这个消息包成一个数据包发送到显示的窗口呢。如果是同一进程,那倒好办,因为都在同一地址空间中吗,你New了一个内存,然后用一个自定义消息发送过去没有问题。但如果是截获其他进程的消息呢。此时New的内存地址只是相对于该进程而言,而当发送消息到显示的进程时,那个内存地址已经完成不同了。所以我们还需要一个进程间通信的技术。好在并不是很难。
用WM_COPYDATA消息即可以完成,wParam表示传递该消息的窗口,这个参数我们可以忽略。lParam是指向COPYDATASTRUCT结构的指针。看看其声明:
typedef struct tagCOPYDATASTRUCT { // cds
DWORD dwData;
DWORD cbData;
PVOID lpData;
} COPYDATASTRUCT;
dwData指定一个32的值传递过去。这个对于我们不重要。
cbData确定lpData指向的内存的大小
lpData指向一块内存。
这里最重要的无疑就是lpData了,可以设定一个消息结构来保存截获得到的消息的信息,然后将其挂到lpData上面,让WM_COPYDATA消息载着这一块内存送到显示的窗口去。OK了。
所有重点技术都解决了,其他的就没有什么了。希望读者能够看得懂。