第五章 监控Native API调用
翻译:Kendiv( fcczj@263.net )
更新:
声明:转载请注明出处,并保证文章的完整性,本人保留译文的所有权利。
拦截系统调用在任何时候都是程序员们的最爱。这种大众化爱好的动机也是多种多样的:代码性能测试(Code Profiling)和优化,逆向工程,用户活动记录等等。所有这些都有一个共同的目的:将控制传递给一块特殊的代码,这样无论一个应用程序何时调用系统服务,都可以发现哪个服务被调用了,接收了什么参数,返回的结果是什么以及执行它花费了多少时间。根据最初由Mark Russinovich和Bryce Cogswell提出的技巧,本章将介绍一个可以hook任意Native API函数的通用框架。这里使用的方法完全是数据驱动(data-driven)的,因此,它可以很容易被扩展,并能适应其他Windows NT/2000版本。所有进程的API调用产生的数据都被写入一个环状缓冲区中,客户端程序可以通过设备I/O控制来读取该缓冲区。采用的数据协议(protocol data)的格式是以行为导向的ANSI文本流,它符合严格的格式化规则,应用程序可以很容易的再次处理它们(postprocessing)。为了示范此种客户端程序的基本框架,本章还提供了一个示例性的数据协议察看器,该程序运行于控制台窗口中。
译注:
profiling 一般是指对程序做性能方面的测试, 主要是速度上的。 在翻译时,可译为:剖析,最好是根据上文环境进行翻译。
修改服务描述符表
对比“原始”的操作系统,如DOS或Windows 3.x,它们对程序员在API中加入hook的限制很少,而Win32系统,如Windows 2000/NT和Windows 9.x则很难驾驭,因为它们使用了巧妙的保护机制把不相关的代码分离出来。在Win32 API上设置一个系统范围的hook绝不是一个小任务。幸运的是,我们有像Matt Pietrek和Jeffery Richter这样的Win32向导,他们做了大量的工作来向我们展示如何完成这一任务,尽管事实上,并没有简单和优雅的解决方案。在1997年,Russinovich和Cogswell介绍了一种可在Windows NT上实现系统范围hook的完全不同的方法,可在更低一层上拦截系统调用(Russinovich和Cogswell 1997)。他们提议向Native API Dispatcher中注入日志机制,这仅比用户模式和内核模式之间的边界低一些,在这个位置上Windows NT暴露出一个“瓶颈”:所有用户模式的线程必须通过此处,才能使用操作系统内核提供的服务。
服务和参数表
就像在第二章讨论过的,发生在用户模式下的Native API调用必须通过INT 2eh接口,该接口提供一个i386的中断门来改变特权级别。你可能还记得所有INT 2eh调用都是由内部函数KiSystemService()在内核模式下处理的,该函数使用系统服务描述符表(SDT)来查找Native API处理例程的入口地址。图5-1给出了这种分派机制的基本组件之间的相互关系。列表5-1再次给出了SERVICE_DESCRIPTOR_TABLE结构及其子结构的正式定义,在第二章中,已经给出过它们的定义(列表2-1)。
调用KiSystemService()时,需提供两个参数,由INT 2eh的调用者通过EAX和EDX寄存器传入。EAX包含从0开始的索引,该索引可用于一个API函数指针的数组,EDX指向调用者的参数堆栈。KiSystemService()通过读取ServiceTable的一个成员的值(该成员是ntoskrnl.exe的一个公开数据结构:KeServiceDescriptorTable,图5-1的左面列出了该结构)来获取函数数组的基地址。实际上,KeServiceDescriptorTable指向一个包含四个服务表的结构,但默认情况下,仅有第一个服务表是有效的。KiSystemService()通过EAX中的索引值进入内部结构KiServiceTable,以查找可处理此API调用的函数的入口地址。在调用目标函数之前,KiSystemServie()以相同的方式查询KiArgumentTable结构,以找出调用者在参数堆栈中传入了多少字节,然后使用这个值将参数复制到当前内核堆栈中。此后,就只需要一个简单的汇编指令:CALL来执行API处理例程了。这里涉及的所有函数都符合__stdcall标准。
图5-1. KeServiceDescriptorTable的结构图
typedef NTSTATUS (NTAPI*NTPROC)();
typedef NTPROC* PNTPROC;
#define NTPROC_ sizeof(NTPROC)
typedef struct _SYSTEM_SERVICE_TABLE
{
PNTPROC ServiceTable; // array of entry points
PDOWRD CounterTable; // array of usage counters
DWORD ServiceLimit; // number of table entries
PBYTE ArgumentTable; // array of byte counts
}
SYSTEM_SERVICE_TABLE,
*PSYSTEM_SERVICE_TABLE,
**PPSYSTEM_SERVICE_TABLE;
//-----------------------------------------------------------------------------------------------------------
typedef struct _SERVICE_DESCRIPTOR_TABLE
{
SYSTEM_SERVICE_TABLE ntoskrnl; // ntoskrnl.exe ( native api )
SYSTEM_SERVICE_TABLE win32k; // win32k.sys (gdi/user support)
SYSTEM_SERVICE_TABLE Table3; // not used
SYSTEM_SERVICE_TABLE Table4; // not used
}
SYSTEM_DESCRIPTOR_TABLE,
*PSYSTEM_DESCRIPTOR_TABLE,
**PPSYSTEM_DESCRIPTOR_TABLE;
列表5-1. SERVICE_DESCRIPTOR_TABLE结构的定义
Windows 2000还提供了另一个服务描述符表参数块----KeServiceDescriptorTableShadow。不过,KeServiceDescriptorTable已经由ntoskrnl.exe公开的导出了,因此,内核模式的驱动程序可以很容易的访问它,而KeServiceDescriptorTableShadow则不行。在Windows 2000下,KeServiceDescriptorTableShadow紧随KeServiceDescriptorTable之后,但是你不能在Windows NT中以这样的方法找到它,因为,这一规则并不使用于Windows NT。可能在Windows 2000的后续版本的中,这种方法也不行。这两个参数块的不同之处在于:KeServiceDescriptorTableShadow中的第二个项被系统使用了,用来指向内部的W32pServiceTable和w32pArgumentTable结构,Win32的内核模式组件Win32K.sys使用这两个结构来分派自己的API调用,如图5-2所示。KiSystemService()通过检查EAX中索引值的第12和13位来确认是不是应该由Win32K.sys处理API调用。如果这两个位都是0,则是由ntoskrnl.exe处理的Native API调用,因此KiSystemService()使用第一个SDT。如果第12位为1并且第13位为0,KiSystemService()使用第二个SDT,这个SDT并没有被当前系统使用。这意味着Native API调用的索引值的潜在范围是:0x0000 --- 0x0FFFF,Win32K.sys调用使用的索引范围是:0x1000 --- 0x1FFF。因此,0x2000 --- 0x2FFF和0x3000 --- 0x3FFF保留给剩下的两个SDT。在Windows 2000中,Native API服务表包含248个项,Win32K.sys表包含639个项。
图5-2. KeServiceDescriptorTableShadow的结构图
Russinovich和Cogswell的独具特色的方法是:通过简单的向KiServiceTable数组中放入一个不同的处理例程来hook所有API调用。这个处理例程最终会调用位于ntoskrnl.exe中的原始处理例程,但它有机会察看一下被调用函数的输入/输出参数。这个方法非常强大却又如此简单。因为所有用户模式的线程必须经过这个“针眼”才能获得Native API的服务,安装一个全局hook来简单的替换一个函数指针的方法,在启动一个新的进程和线程的情况下,也能很稳定的工作。这并不需要一种通讯机制来通知新加入或将要移除的进程/线程。
不幸的是,系统服务表在不同Windows NT版本上不相同。表5-1比较了Windows NT/2000的KiServiceTable。很显然,不仅是处理例程的号码从211增加到了248,而且新的处理例程并不是直接添加到列表的末尾,而是被插入到了各个地方!因此,一个服务函数索引,如0x20在Windows 2000中指向NtCreateFile(),而在Windows NT中却指向NtCreateProfile()。所以,通过操作服务函数表进行hook的API调用监控器必须小心的检查它所在的Windows NT的版本。这可以通过如下几个方式来完成:
l 一种可能性是,检查由ntoskrnl.exe导出的公开变量:NtBuildNumber,就像Russinovich和Cogswell在他们的原文中所作的那样。Windows NT 4.0为所有Service Pack提供的Build Number是:1381。Windows 2000的当前Build Number是:2195。看来有希望,这个版本号在Windows NT的早期版本中很稳定。
l 另一个可能性是,检查SharedUserData结构中的NtMajorVersion和NtMinorVersion成员,该结构定义与Windows 2000头文件ntddk.h中。Windows NT 4.0的所有Service Pack都将SharedUserData->NtMajorVersion设为4,将SharedUserData->NtMinorVersion设为0。Windows 2000的当前版本为Windows NT Version 5.0。
l 本章给出的代码采用了另一中替代方法:它测试SDT的ServiceLimit成员是否和它的预期值相匹配,该预期值是211(0xD3)针对Windows NT 4.0和248(0xF8)针对Windows 2000。
表5-1. Windows 2000/NT 服务表对比
|
Windows 2000 |
索引 |
Windows NT 4.0 |
|
NtAcceptConnectPort |
0x00 |
NtAcceptConnectPort |
|
NtAccessCheck |
0x01 |
NtAccessCheck |
|
NtAccessCheckAndAuditAlarm |
0x02 |
NtAccessCheckAndAuditAlarm |
|
NtAccessCheckByType |
0x03 |
NtAddAtom |
|
NtAccessCheckByTypeAndAuditAlarm |
0x04 |
NtAdjustGroupsToken |
|
NtAccessCheckByTypeResultList |
0x05 |
NtAdjustPrivilegesToken |
|
NtAccessCheckByTypeResultListAndAuditAlarm |
0x06 |
NtAlertResumeThread |
|
NtAccessCheckByTypeResultListAndAuditAlarmByHandle |
0x07 |
NtAlertThread |
|
NtAddAtom |
0x08 |
NtAllocateLocallyUniqueld |
|
NtAdjustGroupsToken |
0x09 |
NtAllocateUuids |
|
NtAdjustPrivilegesToken |
0x0A |
NtAllocateVirtualMemory |
|
NtAlertResumeThread |
0x0B |
NtCallbackReturn |
|
NtAlertThread |
0x0C |
NtCancelloFile |
|
NtAllocateLocallyUniqueld |
0x0D |
NtCancelTimer |
|
NtAllocateUserPhysicalPages |
0x0E |
NtClearEvent |
|
NtAllocateUuids |
0x0F |
NtClose |
|
NtAllocateVirtualMemory |
0x10 |
NtCloseObjectAuditAlarm |
|
NtAreMappedFilesTheSame |
0x11 |
NtCompleteConnectPort |
|
NtAssignProcessToJobObject |
0x12 |
NtConnectPort |
|
NtCallbackReturn |
0x13 |
NtContinue |
|
NtCancelloFile |
0x14 |
NtCreateDirectoryObject |
|
NtCancelTi mer |
0x15 |
NtCreateEvent |
|
NtCancelDeviceWakeupRequest |
0x16 |
NtCreateEventPair |
|
NtClearEvent |
0x17 |
NtCreateFile |
|
NtClose |
0x18 |
NtCreateloCompletion |
|
NtCloseObjectAuditAlarm |
0x19 |
NtCreateKey |
|
NtCompleteConnectPort |
0x1A |
NtCreateMailslotFile |
|
NtConnectPort |
0x1B |
NtCreateMutant |
|
NtContinue |
0x1C |
NtCreateNamedPipeFile |
|
NtCreateDirectoryObject |
0x1D |
NtCreatePagingFile |
|
NtCreateEvent |
0x1E |
NtCreatePort |
|
NtCreateEventPair |
0x1F |
NtCreateProcess |
|
NtCreateFile |
0x20 |
NtCreateProfile |
|
NtCreateloCompletion |
0x21 |
NtCreateSection |
|
NtCreateJobObject |
0x22 |
NtCreateSemaphore |
|
NtCreateKey |
0x23 |
NtCreateSymbolicLinkObject |
|
NtCreateMailslotFile |
0x24 |
NtCreateThread |
|
NtCreateMutant |
0x25 |
NtCreateTimer |
|
NtCreateNamedPipeFile |
0x26 |
NtCreateToken |
|
NtCreatePagingFile |
0x27 |
NtDelayExecution |
|
NtCreatePort |
0x28 |
NtDeleteAtom |
|
NtCreateProcess |
0x29 |
NtDeleteFile |
|
NtCreateProfile |
0x2A |
NtDeleteKey |
|
NtCreateSection |