不仅Win32 API可以挂钩,系统服务也可以挂钩。开发者为了捕获各种事件,可以挂钩文件创建函数CreateFile,注册表访问函数RegCreateKey。使用挂钩可以改变操作系统的行为,只要适当地改变挂钩的数据结构和上下文,足够引起新的行为。例如,通过挂钩NtCreateFile系统服务,可以保护敏感文件不被打开,尽管NTFS为文件提供了用户级的安全,但这些安全措施却不能用在FAT分区上。
系统服务就是由操作系统提供的一组函数,类似上层开发的Win32 API。
不仅Win32 API可以挂钩,系统服务也可以挂钩。开发者为了捕获各种事件,可以挂钩文件创建函数CreateFile,注册表访问函数RegCreateKey。使用挂钩可以改变操作系统的行为,只要适当地改变挂钩的数据结构和上下文,足够引起新的行为。例如,通过挂钩NtCreateFile系统服务,可以保护敏感文件不被打开,尽管NTFS为文件提供了用户级的安全,但这些安全措施却不能用在FAT分区上。
Kernel-Level Hooking
系统级挂钩要用驱动程序实现,此方法的缺点就是,需要确定传递给内核函数的参数,但多数情况下这些服务都是未公开的。所以,这类挂钩更难实现,但可以产生更好的效果。
User-Level Hooking
用户级的挂钩的优点是这些函数通常都是公开的,可以知道所需的参数。这类挂钩只能局限于用户模式而不能扩展到内核模式。
<!--[if !supportEmptyParas]--> <!--[endif]-->
下面主要要讨论Kernel-Level Hooking
这方面的知识可以阅读《Undocmented Windows 2000 Secrets》,在我的BLOG上可以下载由Kendiv大牛翻译的中文版,这是我看过关于挂钩系统服务最详细的书了。另外还有一本《Undocmented Windows NT》,其中第6章专门讲述这方面知识,后者可以作为补充教材,本书同样在我的BLOG上提供下载。除了这两本书以外,网上还有一些零星的文章介绍这个,比如《剖析Windows服务调用机制》等,很容易在网上搜索得到 。如果你要彻底掌握这些知识,建议把上面提到的书籍文章通通看一看。
我这里主要把重点提一下,以及自已学习过程中理解的东东,和理解后写出来的源码。不少文章有代码片段,但很多像我一样,刚刚接解内核开发,连DDK都没安装,看过那些代码片段总觉得太空洞。《Undocmented Windows 2000 Secrets》虽然提供代码(w2k_spy),但过于复杂,不利于初学者。
关于驱动程序如果编译安装,请参考有关资料。如果想要速学,建议看《Undocmented Windows 2000 Secrets》第三章。接下来,假设你已经懂得如何编译安装驱动,以及挂钩系统服务的基本知识了。
<!--[if !supportEmptyParas]--> <!--[endif]-->
系统服务是如何被调用的?
系统服务的用户接口是以包装函数的形式提供的,这些包装函数都在NTDLL.dll中。这些包装函数使用INT 2E指令来切换到内核模式并执行所需的系统服务:每一种系统服务都用一个service ID的标识,包装函数将所需的系统服务的service ID送入EAX寄存器,将指向堆栈帧的指针送入EDX寄存器,然后发出INT 2E指令。INT 2E的处理程序将参数从用户模式的堆栈拷贝到内核模式的堆栈,这个由ntosknrl.exe提供的INT 2E处理程序内部被称为KiSystemService()。其中有一个系统服务分派表,表中的每一个表项都包含service ID和其对应的函数地址。每个函数的代码都位于内核之中。
挂钩系统服务最简单的方法就是修改系统服务分派表中的函数指针,使之指向由开发者插入的某个函数。要做到这点只能通过内核驱动,因为这个系统服务分派表是受操作系统保护的,这表所在的页属性被设置成只有内核程序才能读写,用户级的应用程序不能读写这些内存单元。
如何访问系统服务分派表?
在2000/XP中默认存在两个系统服务分派表(SDT),它们对应了两类不同的系统服务。这两个分别是:KeServiceDescriptorTable和KeServiceDescriptorTableShadow,NT下只有前面那个。KeServiceDescriptorTable定义了在ntoskrln.exe中实现的系统服务,通常在kernel32.dll和advapi32.dll中提供的函数接口均是使用KeServiceDescriptorTable。同时存在的还有win32k.sys中实现的winuser和GDI函数,它们是属于另一类系统服务调用,与之相对应的分派表是KeServiceDescriptorTableShadow,它提供了内核模式的USER和GDI的服务。由服务KeAddSystEmServiceTable添加的服务会被同时复制到上面两个分派表中。
系统服务分派表的数据结构(SDT)
typedef struct _SERVICE_DESCRIPTOR_TABLE // SDT
{
SYSTEM_SERVICE_TABLE ntoskrnl ; // ntoskrnl.exe (Nativate API)
SYSTEM_SERVICE_TABLE win32k ; // win32k.sys (gdi/user support)
SYSTEM_SERVICE_TABLE table3 ; // not used
SYSTEM_SERVICE_TABLE table4 ; // not used
} SERVICE_DESCRIPTOR_TABLE ;
<!--[if !supportEmptyParas]--> <!--[endif]-->
typedef NTSTATUS (NTAPI *NTPROC)() ;
typedef NTPROC *PNTPROC ;
typedef struct _SYSTEM_SERVICE_TABLE // SSD
{
PNTPROC ServiceTable ; // array of entry points
PULONG CounterTable ; // array of usage counters . be NULL
ULONG ServiceLimit ; // number of table entries
UCHAR* ArgumentTable ; // array of bytes counts
<!--[if !supportEmptyParas]--> <!--[endif]-->
} SYSTEM_SERVICE_TABLE
<!--[if !supportEmptyParas]--> <!--[endif]-->
访问KeServiceDescriptorTable很简单,因为它由ntoskrnl.exe公开导出。
加上 extern PSERVICE_DESCRIPTOR_TABLE KeServiceDescriptorTable; 这行就可以直接访问这个数据结构。但KeServiceDescriptorTableShadow没有导出(NT下没有此表),而且2000和XP下,它的位置不一样。以后我们要挂钩的都是ntoskrnl.exe导出的函数,所以不用访问KeServiceDescriptorTableShadow。
KeServiceDescriptorTabe只有第1个成员 ntoskrnl 有用到,其它3个成员全部为NULL 。KeServiceDescriptorTabeShadow前面2个成员都用到,它们的ntoskrnl成员都是相同的,差别在后者多了一个有效的win32k成员,也就是win32k.sys的系统服务分派表。在2000下 ,KeServiceDescriptorTableShadow=KeServiceDescriptorTable+0xE0;
在XP下,KeServiceDescriptorTableShadow=KeServiceDescriptorTable-0x40。
如何得到要挂钩服务的ID?
Ntosknrl.exe的服务都保存在SYSTEM_SERVICE_TABLE.ServiceTable中,该成员是个线性数组,每个项保存一个服务的地址指针,只要把这个地址换成我们提供的函数地址指针就OK了。我们怎么知道要挂钩那个服务在数组中的ID(索引)呢?
ULONG ServiceIdFromFn (PVOID pfnHandler)
{
PUCHAR pbHandler = (PUCHAR) pfnHandler ;
ULONG ulService = * ((PULONG)(pbHandler + 1));
return ulService;
}
由这个服务函数的地址可以直接转化成ID ,如 ServiceIdFromFn( ZwCreateFile )就是返回 ZwCreateFile 的ID。而且,从这个ID我们就可以判断这个服务是由ntosknrl.exe,还是win32k.sys提供的。看ID二进制的 13,14位,如果全为0表示由ntosknrl.exe提供,如果13位为1,14位为0表示由win32k.sys提供。
<!--[if !supportEmptyParas]--> <!--[endif]-->
例子:
理论看得再多,还不如读一下代码!有的时候我更喜欢直接看代码。
我提供的代码再简洁不过,没有多余的代码,一切都只为了实现 ZwCreateFile 的挂钩。我的ZwCreateFile只是把文件名称打印出来,然后直接调用真正的ZwCreateFile 。如果你不熟悉驱动安装,又想检验一下实践的成果,可以:
<!--[if !supportLists]-->1. <!--[endif]-->把hooksys.sys文件复制到 c:/windows/system32/drivers 下
<!--[if !supportLists]-->2. <!--[endif]-->在命令提示符下,执行w2k_load c:/windows/system32/drivers/hooksys.sys安装
<!--[if !supportLists]-->3. <!--[endif]-->运行dbgview.exe ,我的BLOG上有提供下载。
<!--[if !supportLists]-->4. <!--[endif]-->在命令提示符下,执行net start hooksys 启动驱动
<!--[if !supportLists]-->5. <!--[endif]-->通过dbgview.exe 观察文件的创建情况
<!--[if !supportLists]-->6. <!--[endif]-->在命令提示符下,执行net stop hooksys 停止驱动
<!--[if !supportLists]-->7. <!--[endif]-->删除 hooksys.sys
<!--[if !supportEmptyParas]--> <!--[endif]-->
本代码在 XP sp2 下执行通过。例子下载