Windows Hook之原理篇
1. Windows系统中HOOK机制的概念
Windows系统是建立在消息驱动机制上的,整个系统都是通过消息的传递来实现的。而HOOK( 钩子)是Windows系统中非常重要的系统接口,用它可以截获并处理送给其他应用程序的消息,然后完成普通应用程序难以实现的功能。钩子可以监视系统或进程中的各种事件消息,截获并发往目标窗口的消息并进行处理。因此,就可以通过在系统中安装自定义的钩子,监视系统中特定事件的产生,完成特定的功能,比如截获键盘/鼠标的输入、屏幕取词、日志监视等。
首先我们应该掌握以下几点关于钩子的内容:
(1)钩子是用来截获系统中的消息流的。利用钩子,可以处理任何消息,包括其他进程的消息;
(2)截获消息后,用于处理消息的子程序叫做钩子函数,它是应用程序自定义的一个函数,在安装钩子时要把这个函数的地址告诉Windows;
(3)系统中同一时间可能有多个进程安装了钩子,多个钩子函数一起组成钩子链,该钩子链是由Windows系统维护的。在处理截获到的消息时,应该把消息事件传递下去,以便其他钩子也有机会处理这一消息。而且最新安装的钩子放在链的开始,而最早安装的钩子放在最后,也就是后加入的先获得控制权;
(4)钩子函数有很多类型。按事件类型分类,主要有键盘钩子、鼠标钩子、外壳钩子、日志钩子和窗口过程钩子等,这些钩子的用途会在后面详细阐述;按使用范围分类,主要有线程钩子和系统钩子。线程钩子监视制定线程的事件消息,系统钩子监视系统中的所有线程的事件消息。因为系统钩子会影响系统中所有的应用程序,所以钩子函数必须放在独立的动态链接库(DLL)中。如果对于同一事件既安装了线程钩子又安装了系统钩子,那么系统会自动调用线程钩子,然后调用系统钩子;
(5)钩子会使系统变慢,因为它增加了系统对每个消息的处理量。
2. Windows系统中HOOK机制的使用步骤
使用HOOK机制可以简单地分成定义钩子函数、安装钩子和卸载钩子三步:
(1)定义钩子函数
钩子函数式一种特殊的回调函数,有关回调函数在这里先不做撰述。钩子监视的特定事件发生后,系统会调用钩子函数进行处理。不同事件的钩子函数的形式各不相同,我们以鼠标钩子函数举例说明钩子函数的原型:
LRESULT CALLBACK HookProc( int nCode, WPARAM wParam, LPARAM IParam );
参数wParam和IParam包含所钩消息的信息,比如鼠标位置/状态、键盘按键等。nCode包含有关消息本身的信息,比如是否从消息队列中移出。该函数的具体功能是先从钩子函数中实现自定义的功能,然后调用函数CallNextHookEx,把钩子信息传递给钩子链的下一个钩子函数。
(2)安装钩子
在程序初始化的时候,调用函数SetWindowsHookEx安装钩子。其函数原型为:
HHook SetWindowsHookEx( int idHook, HOOKPROC lpfn, HINSTANCE hMod, DWORD dwThreadId );
参数idHook表示钩子类型,和钩子函数类型对应。比如WH_KEYBOARD表示键盘钩子等。lpfn是钩子函数所在的地址,也就是(1)中HookProc函数的地址。hMod是钩子函数所在的实例的句柄,对于系统钩子,该参数为钩子函数所在的DLL句柄。dwThreadId指定钩子所监视的线程的线程号。对于系统钩子,该参数为NULL。
SetWindowsHookEx返回所安装的钩子句柄。
(3)卸载钩子
当不再使用钩子时,必须及时卸载。简单地调用函数BOOL UnhookWindowsHookEx( HHook hhk );
参数hhk就是(2)中SetWindowsHookEx返回所安装的钩子句柄。
3. HOOK API技术
HOOK API是指截获特定进程或系统对某个API函数的调用,使得API的执行流程转向指定的代码。Windows下的应用程序都有自己的地址空间,它们只能调用自己地址空间中的函数,所以在HOOK API之前,必须将一个可以代替API执行的自定义函数(一般称为代理函数)的执行代码注入到目标进程,然后再想办法将目标进程对该API的调用改为对注入到目标进程中的自定义函数的调用,这样即可实现拦截API函数。Windows下的应用程序都建立在API函数之上,所以截获API是一项相当有用的技术,使得用户有机会干预其他应用程序的程序流程。
下面先简单介绍一种系统钩子注入DLL的方法:
对系统钩子而言,当安装钩子的函数SetWindowsHookEx调用成功后,Windows在系统内部对系统中的所有进程自动调用LoadLibrary函数,强迫它们加载钩子函数执行代码的模块,这就是这些进程能够访问钩子函数的原因。我们可以创建一个系统钩子,并将包含钩子函数执行代码的模块编译成DLL(该DLL中包含代码函数的代码),就可以实现DLL的自动注入。
使用Windows钩子注入特定DLL到其他进程,一般都安装WH_GETMESSAGE钩子,这是因为Windows下的应用程序大部分都需要调用GetMessage或PeekMessage函数从消息队列中获取消息,所以它们都会加载钩子函数所在的DLL。
由于在此处安装WH_GETMESSAGE钩子的目的仅仅是让其他进程加载够子函数所在的DLL,所以一般仅在钩子函数总调用CallNextHookEx函数即可,如代码所示:
LRESULT WINAPI GetMsgPro( int code, WPARAM wParam, LPARAM lParam )
{
return ::CallNextHookEx( g_hHook, code, wParam, lParam );
}
如果要将DLL注入到特定进程中,一般是将该进程中主线程的线程ID传递给SetWindowsHookEx函数;
而如果要将DLL注入到所有进程中,安装一个系统范围内的钩子即可(将0作为线程ID传递给SetWindowsHookEx函数)。
4. Windows消息钩子类型
按事件分类,有如下的几种常用类型:
(1)键盘钩子和低级键盘钩子可以监视各种键盘消息。
(2)鼠标钩子和低级鼠标钩子可以监视各种鼠标消息。
(3)外壳钩子可以监视各种Shell事件消息。比如启动和关闭应用程序。
(4)日志钩子可以记录从系统消息队列中取出的各种事件消息。
(5)窗口过程钩子监视所有从系统消息队列发往目标窗口的消息。
下面具体描述一下上述各类钩子的用途:
1> WH_CALLWNDPROC和WH_CALLWNDPROCRET Hooks
WH_CALLWNDPROC和WH_CALLWNDPROCRET Hooks使你可以监视发送到窗口过程的消息。系统在消息发送到接收窗口过程之前调用WH_CALLWNDPROC Hook子程,并且在窗口过程处理完消息之后调用WH_CALLWNDPROCRET Hook子程。WH_CALLWNDPROCRET Hook传递指针到CWPRETSTRUCT结构,再传递到Hook子程。CWPRETSTRUCT结构包含了来自处理消息的窗口过程的返回值,同样也包括了与这个消息关联的消息参数。
2> WH_CBT Hook
在以下事件之前,系统都会调用WH_CBT Hook子程,这些事件包括:
1. 激活,建立,销毁,最小化,最大化,移动,改变尺寸等窗口事件;
2. 完成系统指令;
3. 来自系统消息队列中的移动鼠标,键盘事件;
4. 设置输入焦点事件;
5. 同步系统消息队列事件。
Hook子程的返回值确定系统是否允许或者防止这些操作中的一个。
3> WH_DEBUG Hook
在系统调用系统中与其他Hook关联的Hook子程之前,系统会调用WH_DEBUG Hook子程。你可以使用这个Hook来决定是否允许系统调用与其他Hook关联的Hook子程。
4> WH_FOREGROUNDIDLE Hook
当应用程序的前台线程处于空闲状态时,可以使用WH_FOREGROUNDIDLE Hook执行低优先级的任务。当应用程序的前台线程大概要变成空闲状态时,系统就会调用WH_FOREGROUNDIDLE Hook子程。
5> WH_GETMESSAGE Hook
应用程序使用WH_GETMESSAGE Hook来监视从GetMessage or PeekMessage函数返回的消息。你可以使用WH_GETMESSAGE Hook去监视鼠标和键盘输入,以及其他发送到消息队列中的消息。
6> WH_JOURNALPLAYBACK Hook
WH_JOURNALPLAYBACK Hook使应用程序可以插入消息到系统消息队列。可以使用这个Hook回放通过使用WH_JOURNALRECORD Hook记录下来的连续的鼠标和键盘事件。只要WH_JOURNALPLAYBACK Hook已经安装,正常的鼠标和键盘事件就是无效的。WH_JOURNALPLAYBACK Hook是全局Hook,它不能象线程特定Hook一样使用。WH_JOURNALPLAYBACK Hook返回超时值,这个值告诉系统在处理来自回放Hook当前消息之前需要等待多长时间(毫秒)。这就使Hook可以控制实时事件的回放。WH_JOURNALPLAYBACK是system-wide local hooks,它們不會被注射到任何行程位址空間。
7> WH_JOURNALRECORD Hook
WH_JOURNALRECORD Hook用来监视和记录输入事件。典型的,可以使用这个Hook记录连续的鼠标和键盘事件,然后通过使用WH_JOURNALPLAYBACK Hook来回放。WH_JOURNALRECORD Hook是全局Hook,它不能象线程特定Hook一样使用。WH_JOURNALRECORD是system-wide local hooks,它們不會被注射到任何行程位址空間。
8> WH_KEYBOARD Hook
在应用程序中,WH_KEYBOARD Hook用来监视WM_KEYDOWN and WM_KEYUP消息,这些消息通过GetMessage or PeekMessage function返回。可以使用这个Hook来监视输入到消息队列中的键盘消息。
9> WH_KEYBOARD_LL Hook
WH_KEYBOARD_LL Hook监视输入到线程消息队列中的键盘消息。
10> WH_MOUSE Hook
WH_MOUSE Hook监视从GetMessage 或者 PeekMessage 函数返回的鼠标消息。使用这个Hook监视输入到消息队列中的鼠标消息。
11> WH_MOUSE_LL Hook
WH_MOUSE_LL Hook监视输入到线程消息队列中的鼠标消息。
12> WH_MSGFILTER 和 WH_SYSMSGFILTER Hooks
WH_MSGFILTER 和 WH_SYSMSGFILTER Hooks使我们可以监视菜单,滚动条,消息框,对话框消息并且发现用户使用ALT+TAB or ALT+ESC 组合键切换窗口。WH_MSGFILTER Hook只能监视传递到菜单,滚动条,消息框的消息,以及传递到通过安装了Hook子程的应用程序建立的对话框的消息。
WH_SYSMSGFILTER Hook监视所有应用程序消息。WH_MSGFILTER 和 WH_SYSMSGFILTER Hooks使我们可以在模式循环期间过滤消息,这等价于在主消息循环中过滤消息。通过调用CallMsgFilter function可以直接的调用WH_MSGFILTER Hook。通过使用这个函数,应用程序能够在模式循环期间使用相同的代码去过滤消息,如同在主消息循环里一样。
13> WH_SHELL Hook
外壳应用程序可以使用WH_SHELL Hook去接收重要的通知。当外壳应用程序是激活的并且当顶层窗口建立或者销毁时,系统调用WH_SHELL Hook子程。
WH_SHELL 共有5钟情況:
1. 只要有个top-level、unowned 窗口被产生、起作用、或是被摧毁;
2. 当Taskbar需要重画某个按钮;
3. 当系统需要显示关于Taskbar的一个程序的最小化形式;
4. 当目前的键盘布局状态改变;
5. 当使用者按Ctrl+Esc去执行Task Manager(或相同级别的程序)。
5. API HOOK 框架
API 钩子系统一般框架通常,我们把拦截API的调用的这个过程称为是安装一个API钩子(API Hook)。一个API钩子至少有两个模块组成:一个是钩子服务器(Hook Server)模块,一般为EXE的形式;一个是钩子驱动器(Hook Driver)模块,一般为DLL的形式。
服务器主要负责向目标进程注入驱动器,使得驱动器工作在目标进程的地址空间中,这是关键的第一步。驱动器则负责实际的API拦截工作,以便在我们所关心的API函数调用的前后能做一些我们需要的工作。
API HOOK 框架图如下所示:
原理篇就讲到这里了,请继续关注《Windows Hook之实现篇》。