一、引言
Windows系统是建立在事件驱动的机制上的,每一个事件就是一个消息,每个运行中的程序,也就是所谓的进程,都维护者一个或多个消息队列,消息队列的个数取决于进程内包含的线程的个数。由于一个进程至少要拥有一个线程,所以进程至少要有一个消息队列。虽然Windows系统的消息分派是以线程为单位的,但并不是所有的线程都有消息队列,一个新创建的线程是没有消息队列的,只有当线程第一次调用GDI或USER32库函数的时候Windows才 为线程创建消息队列。消息最终由属于线程的窗口来处理,普通的应用程序只能获取本线程的消息队列中的消息,也就是只能获得系统分派的、属于本线程的消息, 换句话说,一个线程在运行过程中是不知道其它线程发生了什么事情的。但是有一类特殊的程序却可以访问其他线程的消息队列,那就是钩子程序。
编写钩子程序是Windows系统提供给用户的一种对Windows运行过程进行干预的机制,通过钩子程序,Windows将 内部流动的消息暴露给用户,使用户能够在消息被窗口管理器分派之前对其进行特殊的处理,比如在调试程序的时候跟踪消息流程。但是,任何事情都有其两面性, 一些密码窃取工具就是利用系统键盘钩子截获其他程序的键盘消息,从而获取用户输入的密码,可见非法的钩子程序对计算机信息安全具有极大的危害性。本文针对 钩子程序安装和运行的特点,设计了一种检测系统中安装的钩子程序的方案,并开发了一个检测钩子程序的开源软件AntiHook。
二、钩子检测的原理
在开始分析钩子检测的原理之前先要了解一下钩子程序。Windows系统的钩子程序根据作用范围可以分为两类:一类是只能获取本进程内某个线程消息的局部钩子(Thread Local Hook),另一类是可以获取当前系统中所有线程消息的全局钩子(Global Hook 或 System Hook)。局部钩子的程序代码既可以位于线程相关的EXE可执行文件中,也可以位于DLL动态链接库中,全局钩子则只能是DLL动态链接库的形式,这是由全局钩子的加载方式所决定的,本文稍后将详细介绍原因。钩子程序根据定义方式与实现目的又可分为键盘钩子、鼠标钩子、系统Shell钩子以及消息过滤钩子等类型,查阅MSDN中关于SetWindowsHookEx()函数的说明可以了解这些不同类型钩子的详细信息。
对 于局部钩子来说,它所能够访问到的消息仅限于它所在的进程中的消息队列,在安装钩子的时候需要指明是要截取哪个线程的消息。与之相对应的全局钩子则没有范 围的限制,它可以截取整个桌面环境下所有线程中的消息,用来窃取密码的键盘钩子通常就是将自己安装成全局钩子。安装局部钩子和全局钩子使用的是同一个API函数:SetWindowsHookEx(),只是传递的参数不同,关于这个API函数的用法不是本文的重点,此处就不详细介绍了,对钩子程序感兴趣的朋友可以参考MSDN或其它相关文档。
一般来讲,应用程序安装的局部线程钩子对其他程序没有影响,而危害比较大的是全局钩子,因为只有全局钩子有能力“染指”其它程序的消息队列,系统的安全检测也以检测全局钩子为主要目的。全局钩子检测的原理其实也非常简单,这是由Windows操作系统加载全局钩子的方式所决定的,所以我们先来了解一下全局钩子的加载方式。
32位的Windows程序都是运行在保护模式,每一个进程都有独立的进程空间,进程之间不能直接共享内存地址,也就是说,进程之间的资源是严格受保护的,一个进程不能直接访问另一个进程中的资源,当然也包括消息队列。既然Windows保护地如此严格,钩子程序又是如何做到这一点呢,难道它能够凌驾于操作系统之上?当然不是,钩子程序和普通的Windows应用程序一样只能运行在Ring3安全级别上,它的诡秘之处在于Windows对钩子程序的加载方式。当钩子安装程序调用SetWindowsHookEx()函数在系统中安装一个全局钩子后,Windows对钩子程序做的特殊处理就是将其加载到每个应用程序单独的进程空间中。也就是说,系统中每一个程序(包括系统程序)的进程空间中都被Windows“强行”挂接了一个钩子程序(模块)的副本,这就使钩子模块和应用程序所倚赖的其它模块一样运行在这个程序的进程空间之中,如此一来钩子程序就能够访问这个进程中所有线程的消息队列了。
本文前面提到,全局钩子必须做成DLL动 态链接库的形式,这里就解释了原因--因为全局钩子不是独立运行的程序,它是作为其它程序的一部分被加载运行的。原理就是如此的简单,当一个全局钩子安装 后,系统中运行的每个进程都被强行加载了一个钩子程序的模块,这就为检测钩子提供了一个思路,那就是检测程序作为系统中运行的普通程序也会被强行加载钩子 模块,只要钩子检测程序能够发现自己进程空间中被强行加载的不明模块,就可以怀疑系统中运行有钩子程序。
现在的问题是如何使钩子检测程序能够发现加载到自己进程空间中的不明模块。使用API HOOK介 入模块的运行,直接分析二进制代码可能是最直接、最有效的方法,但是且不说这种方法容易破坏系统运行的稳定性,单就二进制代码的逻辑分析就不是少量代码能 够实现的。那么有没有简单一点的方法呢?其实,绕开这些技术层面上的问题的纠缠,还有更简单的方法能够检测不明模块,那就是“模块比较法”。和普通的Windows应用程序一样,钩子检测程序运行时也需要很多系统模块的支持,这些模块是运行钩子检测程序所必须的,也被认为是安全的模块。而被Windows “强行”加载的钩子模块则不是钩子检测程序运行必须的模块,所以被认为是不明模块。钩子检测程序一旦编译完成,就已经决定了它在运行中需要哪些支持模块,有一点需要注意,那就是模块的名称和数量在Windows 95/98/Me系统下和基于Windows NT技术构建的Windows 2000/XP系统下有很大的不同,但是对于特定的Windows 版 本来说是一定的,这也是“模块比较法”的主要理论依据。“模块比较法”的原理就是定期查看钩子检测程序进程中加载的模块列表,将这个列表与安全模块列表做 对比,检查是否有不属于安全模块的不明模块被加载到检测程序的进程空间中,如果发现不明模块就发出告警,提示用户作出相应的处理。剩下的问题就是创建安全 模块列表并将其保存到程序配置文件中,利用Depends工具或进程模块查看工具可以很容易地获取钩子检测程序所必须的支持模块(安全模块),唯一需要注意的是要在一个“干净”的Windows 系统上执行这些操作。
“模块比较法”的关键是查找进程加载的所有模块,在Windows平台上有很多查看进程信息的方法,比如Toolhelp系列API、PSAPI、CDPH API,甚至是使用Windows NT的Native API,由于钩子检测程序不需要详细的模块信息,所以使用Toolhelp系列API和PSAPI两种方法就足够了。使用PSAPI遍历进程和模块信息比使用Toolhelp系列API效率高,但是PSAPI不支持较早的Windows系统,比如Windows 98,所以需要Toolhelp系列API提供对Windows 98这样的操作系统的支持。使用Toolhelp API遍历进程模块信息首先要调用CreateToolhelp32Snapshot得到进程加载的所有模块信息的一个“快照”,然后使用Module32First和Module32Next配合遍历“快照中的信息”,下面的代码演示了如何使用Toolhelp API遍历进程模块信息:
MODULEENTRY32 module;
DWORD dwCurProcID = ::GetCurrentProcessId();
//给当前进程的模块做个快照
HANDLE hModEnum = ::CreateToolhelp32Snapshot(TH32CS_SNAPMODULE,dwCurProcID);
if(hModEnum != INVALID_HANDLE_VALUE)
{
module.dwSize=sizeof(MODULEENTRY32);
if(::Module32First(hModEnum,&module))
{
do
{
//对module中的模块信息进行处理,比如判断是否是不明模块等等
}while(::Module32Next(hModEnum,&module));
}
::CloseHandle(hModEnum);//关闭快照,释放资源
}
http://blog.csdn.net/chinawash/article/details/1143729