楼主 い卋玑┾宝珼 |
要讲钩子(HOOK),就要了解WINDOWS的消息机制,还要初步会进行API编程,因此,我们首先简要的讲讲API和WINDOWS消息机制。 文中的措辞,特别是API编程,都可以写一本书的,本文知识给予API初学者使用,用最通俗的语言去讲解,如有不当,也请各位指正。API很多东西都是C语言过来的,理解不当也请指正。 一、 API基础知识 (一)WINDOWS API WINDOWS API是WINDOWS自带的一套函数集,可以直接访问操作系统的底层,学API的东西,VBA的等级首先要为中级,也就是对VBA有个系统性的了解后,方适宜介入。这些函数,可以在,http://www.vbgood.com/api.html,等资源里面,进行查询。 (二)动态链接库(DLL) API的很多函数保存在哪?存放在DLL文件中,一旦应用程序要调用的时候,可以引用链接某个DLL文件,所有应用程序都有自己的私有空间,每个进程的空间都是相互独立的,当进程在载入DLL时,系统自动把DLL地址映射到该进程的私有空间,这样,调用这些函数就非常方便。 (三)API声明 VBA中,声明格式如下: Declare Function 函数别名 Lib “DLL文件名” Alisa “函数名” (ByVal/ByRef 变量名 As 类型,…) As 输出数据类型 在模块头部声明一下,就可以在VBA里面使用这些函数了,ByVal/ByRef这个事VBA基础的老概念啦,呵呵,自行复习吧。VBA里面数据类型很多是一个自定义数据结构,这个就要复习Type语句啦,呵呵。另外,有时候这个数据类型是个指针,那VBA貌似不支持指针,其实不然。 (四)了解指针 指针是啥?最通俗的话讲,指针就是数据的内存地址。在32位WINDOWS中(帖子最后来得及的话,后面讲讲64位的区别吧,变动不是很大,现在还是蛮多机子是32位的,因此从32位讲),指针/内存地址就是32位长的。那储存这个内存地址的变量,就是指针变量。 由于指针访问的就是数据的内存地址,因此,这数据交换等操作中,速度比通常的通过一般变量名赋值,快得多。 VBA中,和指针最靠近的概念就是ByRef,就是VBA中默认的参数按内存地址传递,即传递实际参数的地址/指针。简而言之,例如,在调用FUNCTION的时候,用ByRef传递参数,实参和形参指向的是同一个内存地址,形参和实参是一起变化的。如果采用ByVal,形参的改变就和实参没关系了,因为是传值,传递的是数据的副本,也就数据的值。这些都是VBA基础,不多讲了。 VBA如何获取指针呢,很简单,一个函数——VarPtr,这个函数,其实VBA是隐藏函数。 试看看下面的代码:
结合上面所说的,大家看懂以下的程序,就没问题了。
Str其实是个指针变量,指向一个Unicode数组,这个数组内存位置的前面4个字节长的字段,是用来储存字符串的字节长度。另外,这个数组,是以两个字节的空字符串结束的。注意,这个指针是指向字符数组的开始,而不是指向前面那个长度字段。另外,我们还可以用Strptr这个隐藏函数来获取这个字符数组的起始地址看看下面的案例:
很明显地能看出,VBA默认储存文本的方式是Unicode字符数组,当需要传送UNICODE文本数组给予API函数时,其实,VBA将强制做了个ANSI数组版本的转换拷贝,然后才将这个转换后ANSI版本的字符数组指针传送给API函数,所以返回的结果是不一样的。其实不单单是传出的过程,传回的时候也是一样的,把先前那个ANSI字符数组回转回原来的UNICODE数组。 (六)过程/函数的指针 在VBA中一个过程可以调用另一个过程/函数,一样的,API函数也可以调用我们VBA里面的过程。但是,要在API里面传递过程/函数,也是通过过程/函数的指针实现的。VBA里面怎么办呢,用:AddressOf 过程/函数名,AddressOf将其后面的过程/函数的地址传递给一个 API函数。上面这些都是API要注意的,特别在传递参数的时候。下面讲讲与钩子(HOOK)非常相关的,WINDOWS消息传递机制的概念和WINDOWS的一些基础知识。 |
2楼 い卋玑┾宝珼 |
二、WINDOWS一些基础概念 (一)进程和线程 进程是一个正在运行的WINDOWS应用程序的实例和系统分配给它一组必要的资源(内存等)。有些晦涩吧,我们从程序的运行过程开始说吧,比较清晰易懂。 当我们运行一个程序是,首先,WINDOWS会为这个程序创建一个进程空间,并且为其分配一些资源。然后将可执行文件按照一定规则加载到该进程空间内,操作系统还会根据可执行文件的“说明”,将相应的动态链接库(DLL)映射进程空间内。接下来,WINDOWS会为该进程空间创建一个主线程,为该主线程分配一些资源,主线程从这个exe文件指定的位置开始执行代码,程序开始运行。这么看,进程就是WINDOWS负责为一个程序构造一个独立的环境,这个环境内的资源可以相互直接访问,不同的进程之间则无法直接打交道。当一个进程创建之后,操作系统会自动为它创建一个主线程,用来执行程序代码,那线程又是什么呢? 线程则是CPU实际执行任务的环境,负责执行进程空间里的代码,一个进程空间可以有多个线程。每个线程包含一组状态,其中一些用来记录和恢复CPU的执行状态,另一些则是由程序的自行记录的内容,还有一些则是给系统内核使用的。WINDOWS内核会进行线程调度,按照一定规则给每个线程分配CPU时间。一般情况下,同级别的每个线程都会获得一小段很短的时间片,使各个任务看起来像在“同时”运行,直到执行完毕或是采用某种方式暂停或退出为止。 另外,线程中如果创建了窗口,就叫GUI线程,否则就是工作线程。 (二)窗口 大部分应用程序都有它的图形界面,这个就是它的窗口。窗口里面还有很多文本框和按钮等,这些就是控件。窗口句柄就是一个32的数值,用来系统识别这个窗口。那窗口里面的控件,子窗口啊,也一样,也拥有自己的句柄。 注意,窗口的查找过程,要先用FINDWINDOW函数(自己翻查API手册吧)查找到顶层父窗口的句柄,然后才能用FINDWINDOWEX查询,这个窗口下属的控件或子窗口的句柄。大家看附件WinExplorer.rar这个工具,就是一个高手用VB做的,用来查找窗口句柄的。我们可以用这个工具,和我们程序里面获得的句柄语句进行比对,校验句柄是否正确之用。 (三)WINDOWS消息 1、啥系WINDOWS消息 WINDOWS是基于事件驱动的,当事件触发了(例如移动鼠标等),WINDOWS系统产生一个消息进行通讯。消息可以是系统产生,也可以是应用程序产生。例如,当应用程序改变了自己窗口大小的时候,系统也会产生消息。 消息构成如下: Hwnd:接受该消息的窗口句柄 Message:消息常量标识符,也就是我们通常所说的消息类型 wParam:32位消息的特定附加信息,通常是一个与消息有关的常量值,也可能是窗口或控件的句柄。 lParam:32位消息的特定附加信息,通常是一个指向内存中数据结构(类似VBA中的TYPE自定义结构)的指针 time:消息创建时的时间 pt:消息创建时的鼠标/光标在屏幕坐标系中的位置 2、这个消息有何用? 产生的消息,将根据消息的窗口句柄,传给相应的窗口,每个窗口都有一个叫做窗口过程的函数,用来处理发送过来的消息。当窗口过程接收到消息之后,他就会使用消息标识符来决定如何处理消息,如果他不处理,它会将消息传回到执行默认的处理。窗口过程通过调用DefWindowProc来做这个默认处理。窗口过程函数处理完消息后就会把控制权转给操作系统。处理的结果,就是我们鼠标点击等后的效果。 3、消息的类别 消息主要有系统消息和应用程序消息。当系统要和应用程序通讯的时候,他就会发出或者投递一个系统消息,它使用这些消息来控制应用程序的操作和为应用程序提供输入或者其他信息。应用程序也可以发动和投递系统定义消息,应用程序一般使用这些消息来控制某些控件的动作。另外,应用程序可以创建消息以供他自己的窗口或者和其他进程的窗口通讯而用。 4、消息的投递过程 系统使用两个方法来传递消息给窗口过程:一种是投递到消息队列,还有一种就是投递消息到系统定义的一个内存对象临时存储,并且直接把这个消息发送给窗口过程(这个方法不会进入消息队列)。需要投递到消息队列的消息称为队列化消息。他们主要是用户通过键盘或者鼠标输入。其他直接发送给窗口过程的消息称为非队列化消息。消息队列是啥呢?得从他的工作机制进行讲解。 5、何为消息队列 系统维护着一个系统级的消息队列,同时为每个GUI线程(前面线程讲了哈)维护着一个线程级的消息队列。所有线程在最初创建的时候是没有消息队列的。只有当线程首次调用用户用户函数或者图形设备接口函数的时候系统才会为线程创建一个消息队列。所以只有GUI线程有消息队列。 只要用户移动鼠标,单击鼠标按钮或者敲击键盘的时候,设备驱动程序都会为鼠标或者键盘把输入转换成消息,并且把这个消息放在系统消息队列中。系统一次一个的从系统消息队列中取出消息,分析确定目标窗口,接着把这个消息投递到创建目标窗口的线程消息队列中。线程从他的消息队列中取出消息,让系统发送这个消息给对应的窗口过程函数进行处理。 一般而言,系统总是把消息投递到队列的尾部,这样可以确保窗体遵循先入先出的顺序接收到输入消息,但是消息是有优先等级的,优先等级高的消息先行处理。 6、消息传递的大致编程原理 首先,应用程序通过调用GetMessage函数从线程中的消息队列中取出一个消息,然后使用DispatchMessage函数触发系统把这个消息发送给窗口过程函数进行处理。当消息队列中没有消息的时候,线程可以使用WaitMessage函数来把控制权让给其他线程。这个函数可以把当前线程挂起,直到线程消息队列中有新的消息被放置进来,那么线程才会继续执行。 非队列化消息会绕过系统消息队列和线程消息队列直接发动给目标窗口过程函数,一般系统发送非队列化消息是为了通知受到事件影响的窗体。例如,当用户激活了一个新的应用程序窗口,系统会发送一系列的消息。非队列化消息也可能是应用程序调用某个系统函数导致的结果。 7、动手玩玩 了解了上面的这些基础理论之后,我们就可以进行一下简单的消息发送。玩玩常见的SendMessage吧。
这个函数主要是向一个或多个窗口发送一条消息,一直等到消息被处理之后才会返回。父窗口和子窗口经常使用这个发送消息的方式互相通讯。不过需要注意的是,如果接收消息的窗口是同一个应用程序的一部分,那么这个窗口的窗口函数就被作为一个子程序马上被调用;如果接收消息的窗口是被另外的线程所创建的,那么系统就切换到相应的线程并且调用相应的窗口函数,这条消息不会被放进目标应用程序队列中。函数的返回值是由接收消息的窗口的窗口函数返回的。 假设我们要去除EXCEL界面的左上角的图表,他对应的消息是什么呢,通过搜索我们的WINDOWS消息大全的附件,会查到,WM_SETICON这个消息,进行上网搜索这个消息,得到: 参数: wParam:指定图标的类型。该参数可以为下列值之一: ICON_BIG 为窗口设置大图标 ICON_SMALL 为窗口设置小图标 lParam:新的大、小图标的句柄。如果该参数为NULL,由wParam参数指定的图标将会移除。 因此,我们只要把lParam参数设为0即可。整个程序就这么短。如下:
WINDOWS消息大全.zip 讲完了消息传递机制,终于可以开始讲钩子(HOOK) |
3楼 い卋玑┾宝珼 |
三、钩子HOOK (一)什么是钩子(hook) 钩子(hook)是一种特殊的消息处理机制,可以监视系统或进程中的各种事件消息,截获发往某窗口的消息,然后进行处理。主要功能是监视,然后处理消息。说白了,就是一个Windows消息的拦截机制。 钩子的种类很多,后面我们再详细讲,每种钩子可以截获并处理相应的消息,如键盘钩子可以截获键盘消息等。 钩子还可以分为线程钩子和系统钩子, 线程钩子监视指定线程的事件消息, 系统钩子监视系统中的所有线程的事件消息。 (二)工作原理 在使用钩子前,我们先理解钩子的工作原理。当创建一个钩子时,WINDOWS会先在内存中创建一个数据结构,该数据结构包含了钩子的相关信息,然后把该结构体加到已经存在的钩子链表中去(系统或特定线程中,是允许同时存在多个钩子的,这样就形成了一个链)。新的钩子将加到老的前面。当一个事件发生时,如果安装的是一个线程钩子,钩子相关的函数将被调用。如果是一个系统钩子,系统就必须把钩子相关的函数插入到其它进程的地址空间,要做到这一点要求该函数必须在一个动态链接库中。 需要注意: (1) 如果对于同一事件(如鼠标消息)既安装了线程钩子又安装了系统钩子,那么系统会自动先调用线程钩子,然后调用系统钩子。 (2) 对同一事件消息可安装多个钩子处理过程,这些钩子处理过程形成了钩子链。当前钩子处理结束后应把钩子信息传递给下一个钩子的函数。而且最近安装的钩子放在链的开始,而最早安装的钩子放在最后,也就是后加入的先获得控制权。 (3) 钩子特别是系统钩子会消耗消息处理时间,降低系统性能。只有在必要的时候才安装钩子,在使用完毕后要及时卸载。 (三)制作钩子的标准流程和相关API函数说明 1、制作钩子挂钩的函数 钩子函数指钩子在拦截了消息后,进行对应消息处理的函数,也可以通过返回其值TRUE直接抛弃消息,其格式为: 函数名(nCodeas long, wParam as long,lParam as Any) 参数说明: nCode:包含所拦截的消息本身的内含信息。(附件钩子函数有详细说明) wParam:消息标识,用于判断该消息是那种消息,如WM_MOUSEMOVE,WM_NCMOUSEMOVE lParam:包含所钩消息的信息指针,比如鼠标位置、状态,键盘按键等。 2、如何创建钩子 需要API SetWindowsHookEx函数:
这函数的返回值是这个钩子的句柄 参数说明: idHook:钩子的拦截消息类型 lpfn:拦截消息的函数指针,若第四个参数dwThreadId为0或者指向了一个其他进程创建的线程之标识符,则参数lpfn必须指向一个动态链接中的函数。反之,指向的是本进程内的线程,参数lpfn可以指向一个与当前进程相关的代码中定义的挂钩处理函数,VBA中表示为ADDRESSOF 函数名 hMod:钩子函数所在的动态链接的句柄。若参数dwThreadId指示的线程由当前进程创建,并且相应的挂钩处理函数定义于当前进程相关的代码中,则参数hMod必须被设置为0&。 dwThreadId:钩子所监视的线程的线程标识,可通过GetCurrentThreadId()获得线程号。对于系统钩子,该参数为0&。 3、创建钩子后,通过钩子函数处理消息后,需要将信息传递给下一个钩子:
hHook:钩子句柄。 nCode、wParam和lParam 是钩子函数对应的参数。 4、卸载钩子函数 当不再使用钩子时,必须及时卸载。
参数hHook就是钩子的句柄。 示例HOOK.zip (四)例子 我们举一个,网上问得多的一个案例,代码也是某个高手写的,我拿出来做案例。 例如,当我们在编辑单元格的时候,在状态栏显示,“正在编辑单元格…”,有点像我们的QQ窗口,当聊天时,对方处于输入状态,我们的QQ窗口提示的“正在输入中…” 分析:当单元格处于编辑状态,也就是单元格获得了键盘焦点,查阅附录,钩子的类型,我们应该选择WH_CBT类型,再查阅附件,“钩子函数”,我们发现,ncode参数要判断的是HCBT_SETFOCUS这个类型,继续查找,将发现wParam参数,是获得键盘焦点的窗口句柄。那我们用GetClassName函数,通过句柄查找窗口的类型名称,用附件的WINEXPLORE可以简单的知道,EXCEL处于单元格的窗口类名称是“EXCEL6”。当然,编辑单元格,也可以在编辑框进行,这个编辑框的类名称就是“EXCEL<”。如图: WinExplorer.rar 最后,书写代码:
附:钩子拦截消息类型(相关的结构,可以在MSDN查询,也可以查看附件HOOK常用数据结构.zip,不过是C语言的,使用时候翻译过来就是) 1、WH_CALLWNDPROC(常量为4)和WH_CALLWNDPROCRET(常量为12)钩子 WH_CALLWNDPROC和WH_CALLWNDPROCRET Hooks使你可以监视发送到窗口过程的消息。系统在消息发送到接收窗口过程之前调用WH_CALLWNDPROC Hook子程,并且在窗口过程处理完消息之后调用WH_CALLWNDPROCRET Hook子程。 WH_CALLWNDPROCRET Hook传递指针到CWPRETSTRUCT结构,再传递到Hook子程。 CWPRETSTRUCT结构包含了来自处理消息的窗口过程的返回值,同样也包括了与这个消息关联的消息参数。 2、WH_CBT 钩子(常量为5) 在以下事件之前,系统都会调用WH_CBTHook子程,这些事件包括: 1)激活,建立,销毁,最小化,最大化,移动,改变尺寸等窗口事件; 2)完成系统指令; 3)来自系统消息队列中的移动鼠标,键盘事件; 4)设置输入焦点事件; 5)同步系统消息队列事件。 Hook子程的返回值确定系统是否允许或者防止这些操作中的一个。 3、WH_DEBUG 钩子(常量为9) 在系统调用系统中与其他Hook关联的Hook子程之前,系统会调用WH_DEBUG Hook子程。你可以使用这个Hook来决定是否允许系统调用与其他Hook关联的Hook子程。 4、WH_FOREGROUNDIDLE 钩子(常量为11) 当应用程序的前台线程处于空闲状态时,可以使用WH_FOREGROUNDIDLEHook执行低优先级的任务。当应用程序的前台线程大概要变成空闲状态时,系统就会调用WH_FOREGROUNDIDLE Hook子程。 5、WH_GETMESSAGE 钩子(常量为3) 应用程序使用WH_GETMESSAGEHook来监视从GetMessage orPeekMessage函数返回的消息。你可以使用WH_GETMESSAGEHook去监视鼠标和键盘输入,以及其他发送到消息队列中的消息。 6、WH_JOURNALPLAYBACK 钩子(常量为1) WH_JOURNALPLAYBACKHook使应用程序可以插入消息到系统消息队列。可以使用这个Hook回放通过使用WH_JOURNALRECORDHook记录下来的连续的鼠标和键盘事件。只要WH_JOURNALPLAYBACKHook已经安装,正常的鼠标和键盘事件就是无效的。 WH_JOURNALPLAYBACKHook是全局Hook,它不能象线程特定Hook一样使用。 WH_JOURNALPLAYBACKHook返回超时值,这个值告诉系统在处理来自回放Hook当前消息之前需要等待多长时间(毫秒)。这就使Hook可以控制实时事件的回放。 WH_JOURNALPLAYBACK是system-wide local hooks,它们不会被注射到任何行程位址空间。 7、WH_JOURNALRECORD 钩子(常量为0) WH_JOURNALRECORD Hook用来监视和记录输入事件。典型的,可以使用这个Hook记录连续的鼠标和键盘事件,然后通过使用WH_JOURNALPLAYBACKHook来回放。 WH_JOURNALRECORD Hook是全局Hook,它不能象线程特定Hook一样使用。 WH_JOURNALRECORD是system-wide local hooks,它们不会被注射到任何行程位址空间。 8、WH_KEYBOARD 钩子(常量为2) 在应用程序中,WH_KEYBOARDHook用来监视WM_KEYDOWN andWM_KEYUP消息,这些消息通过GetMessage orPeekMessage function返回。可以使用这个Hook来监视输入到消息队列中的键盘消息。 9、WH_KEYBOARD_LL 钩子(常量为13) WH_KEYBOARD_LL Hook监视输入到线程消息队列中的键盘消息。 10、WH_MOUSE 钩子(常量为7) WH_MOUSE Hook监视从GetMessage 或者 PeekMessage 函数返回的鼠标消息。使用这个Hook监视输入到消息队列中的鼠标消息。 11、WH_MOUSE_LL 钩子(常量为14) WH_MOUSE_LL Hook监视输入到线程消息队列中的鼠标消息。 12、WH_MSGFILTER(常量为-1)和 WH_SYSMSGFILTER 钩子(常量为6) WH_MSGFILTER 和 WH_SYSMSGFILTER Hooks使我们可以监视菜单,滚动条,消息框,对话框消息并且发现用户使用ALT+TAB or ALT+ESC 组合键切换窗口。WH_MSGFILTERHook只能监视传递到菜单,滚动条,消息框的消息,以及传递到通过安装了Hook子程的应用程序建立的对话框的消息。WH_SYSMSGFILTERHook监视所有应用程序消息。 WH_MSGFILTER 和 WH_SYSMSGFILTER Hooks使我们可以在模式循环期间过滤消息,这等价于在主消息循环中过滤消息。 通过调用CallMsgFilterfunction可以直接的调用WH_MSGFILTER Hook。通过使用这个函数,应用程序能够在模式循环期间使用相同的代码去过滤消息,如同在主消息循环里一样。 13、WH_SHELL 钩子(常量为10) 外壳应用程序可以使用WH_SHELLHook去接收重要的通知。当外壳应用程序是激活的并且当顶层窗口建立或者销毁时,系统调用WH_SHELL Hook子程。 WH_SHELL 共有5钟情况: 1)只要有个top-level、unowned 窗口被产生、起作用、或是被摧毁; 2)当Taskbar需要重画某个按钮; 3)当系统需要显示关于Taskbar的一个程序的最小化形式; 4)当目前的键盘布局状态改变; 5)当使用者按Ctrl+Esc去执行Task Manager(或相同级别的程序)。 按照惯例,外壳应用程序都不接收WH_SHELL消息。所以,在应用程序能够接收WH_SHELL消息之前,应用程序必须调用SystemParametersInfofunction注册它自己。 钩子函数.zip |
4楼 い卋玑┾宝珼 |
新添加示例:拦截保护工作表的对话框 拦截.zip 如果不使用拦截,则会弹出以下对话框 有了这个示例,我们就可以随心所欲的拦截对话框了 参考代码如下:
以及参考文档见此楼 钩子函数.zip WinExplorer.rar 示例HOOK.zip 一篇外文文献.zip 帖子.zip HOOK常用数据结构.zip WINDOWS消息大全.zip |
5楼 lnt1231 |
占楼围观,有能力的时候再学 |
6楼 DJ_Soo |
谢谢分享!有些还是看不懂呀,得慢慢消化. 有个问题请教一下: 关于三楼的代码,可以用什么方法获取编辑框的内容吗? 个人感觉如果能获取编辑框内容的话,可以搞出很多有用的东西.(在编辑状态下获取) |
7楼 い卋玑┾宝珼 |
哪里的编辑框是Excel单 元格的编辑框还是? |
8楼 DJ_Soo |
这个都可以的呀,就是正在编辑的那个单元格没回车之前的内容. 也就是当前激活的那个编辑框里面的内容. |
9楼 BianChengNan |
必须精华啊,真心学习了 |
10楼 lslly |
留记号,以后学习了 |
11楼 纵鹤擒龙水中月 |
有能力的时候再学 |
12楼 avel |
这个帖子精品。。 要好好学习的 |
13楼 水星钓鱼 |
原来这里还有个这么牛的帖子啊。 |
14楼 老糊涂 |
收藏了 |
15楼 sunxm |
谢谢分享! |