现在的位置: 首页 > 综合 > 正文

《Undocumented Windows 2000 Secrets》翻译 — 第五章(3)

2012年09月13日 ⁄ 综合 ⁄ 共 4584字 ⁄ 字号 评论关闭

第五章  监控Native API调用

翻译:Kendiv( fcczj@263.net )

更新:Thursday, March 24, 2005

 

 

声明:转载请注明出处,并保证文章的完整性,本人保留译文的所有权利。

 

 

本书设计的hook机制的最大特色就是它是完全数据驱动的(data-driven)。只需简单的增加一个新的API符号表,该hook dispatcher就可适应Windows 2000的新版本。而且,通过向apdSdtFormats[]数组中加入新的API函数的格式化字符串就可在任何时候记录对这些附加的API函数的调用。这并不需要编写任何附加的代码---API Spy的动作可完全由一组字符串来确定!不过,在定义新的格式化字符串是必须要小心,因为w2k_spy.sys是运行于内核模式的驱动程序。因为在这一系统层次上,系统不能温和的处理发生错误。给Win32 API函数提供了一个无效的参数并不是问题-----你会收到一个错误提示窗口,同时程序会被系统自动终止。在内核模式下,一个微小的访问违规都会引发系统蓝屏。因此,一定要小心。在需要的地方如果没有出现一个正确的格式化控制ID或缺失了这一ID都会使你的系统彻底崩溃。即使一个简单的字符串有时都是致命的!

 

 

现在仅剩SpyHookInitializeEx()中的那一大块ASM代码还未讨论,这段代码由SpyHook2SpyHook9标识。这段代码的一个有趣的特性是:在SpyHookInitializeEx()被调用的时候,它们从来都不会被执行。在进入SpyHookInitializeEx()后,函数代码将跳过这一整段代码,然后在SpyHook9标签处开始恢复执行,此处包含aSpyHooks[]数组的初始化代码。这一大块ASM代码只有通过aSpyHooks[]数组中的Handler成员才能进入。稍候,我将展示这些进入点是如何连接到SDT的。

 

 

在设计这段ASM代码时,我的重要目标之一就是使其是完全非侵入式的。截获操作系统调用非常危险,因为你从来不会知道被调用的代码是否会依赖调用上下文(calling context)的某些未知特性。理论上来说,这些ASM代码完全符合__stdcall约定,但仍存在出错的可能性。我不得不选择将原始的Native API处理例程放入几乎完全相同的环境中,这意味着这些原始函数将使用最初的参数堆栈并且可以访问所有的CPU寄存器,就像它们被正常调用一样。当然,必须接受由于插入hook所带来的最低限度的危险,否则,监控将不可能实现。在这里,有意义的改动就是维护堆栈中的返回地址。如果你翻回到5-3,你会发现在进入函数时,调用者的返回地址并不位于堆栈的顶部。SpyHookInitializeEx()中的hook dispatcher占用了此地址,将它自己的SpyHook6标签的地址写在了这里。因此,原始Native API处理例程将被打断,然后进入SpyHook6中,这样hook dispatcher才能检查原始Native API处理例程的参数和它要返回的数据。

 

 

在调用原始处理例程之前,dispatcher将建立一个SPY_CALL(参见列表5-3)控制块,该控制块中包含它稍候将会用到的参数。其中的一些参数在正确记录API调用时会用到,另外一些则提供了有关调用者的信息,因此dispatcher可以在写完log后,把控制返回给调用者,就像什么都没有发生一样。Spy设备在它的全局数据块DEVICE_CONTEXT中维护着一个SPY_CALL结构的数组,可通过全局变量gpDeviceContext来访问。Hook Dispatcher通过检查SPY_CALL结构中的InUse成员来在数组中找到一个空的SPY_CALLHook Dispatcher使用CPUXCHG指令来加载和设置该成员的值(译注:XCHG指令可以保证此操作为原子操作)。这一点非常重要,因为当代码运行于多线程环境中时,读写全局数据时必须采取保护措施以避免条件竞争。如果在数组中找到了一个空的SPY_CALLdispatcher就会将调用者的线程ID(通过PsGetCurrentThreadId()获取)、与当前API函数相关的SPY_HOOK_ENTRY结构的地址以及整个参数堆栈保存到该SPY_CALL结构中。需要复制的参数的字节数取自KiArqumentTable数组,该数组保存在系统的SDT中。如果所有的SPY_CALL都被使用了,原始的API函数处理例程将被调用而不会产生任何日志记录。

 

 

必须采用SPY_CALL数组是因为Windows 2000的多线程本性。当Native API函数被暂停(suspended)时,这种情况就会经常出现----此时,另一个线程将获得控制权,然后在它自己的时间片(time slice)内调用另一个Native API函数。这意味着Spy设备的Hook Dispatcher必须允许在任何时间和任何执行点上的重进入(reenter)。如果Hook Dispatcher有单一的全局SPY_CALL存储区域,它就可能在处于等待状态的线程使用完之前被当前运行的线程覆写(overwritten)。而这种情况正是蓝屏的最佳候选人。为了进一步了解Native API的嵌套,我在SpyDEVICE_CONTEXT结构中增加了dLeveldMisses成员。无论何时只要重进入hook dispatcher(如,向SPY_CALL数组中增加一个新的SPY_CALLdLevel都不会累加一个1。如果超过了最大嵌套层数(如,SPY_CALL数组已满),dMisses就会累加一个1,来标识丢失了一个日志记录。根据我的观察,在实际环境下,可以很容易的发现嵌套层达到4。这表示即时在高负载(heavy-load)的情况下,Native API也会被重进入,因此,我将嵌套层数的上限设为256

 

 

在调用原始的API处理例程之前,Hook Dispatcher会保存所有的CPU寄存器(包括EFLAGS),随后执行路径将导向函数的进入点。这会在列表5-3中的SpyHook5标签之前立即完成。此时,SpyHook6将位于栈顶,仅随其后的是调用者的参数。一旦API处理例程推出了,控制将被传回到hook dispatcherSpyHook6标签。从此处开始执行的代码也被设计为非入侵的。此时,主要目标是允许调用者可以看到调用上下文,这和原始API函数建立的上下文几乎完全一致。Dispatcher的主要问题是要能立即找到保存有当前API调用信息的SPY_CALL结构。唯一可以依赖的就是调用者的线程ID,该ID保存在SPY_CALL结构的hThread成员中。因此,Dispatcher循环遍历整个SPY_CALL数组以寻找匹配的线程ID。注意,代码不会关心fmuse标志的值;这并不是必须的,因为数组中所有未使用的SPY_CALL结构的hThread都被设为了0,这是系统空闲线程的ID。循环会在到达数组结尾时终止。否则的话(译注:即没有找到匹配的线程ID),Dispatcher不会将控制返回给调用者,因为这样做将是致命的。在这种情况下,代码的选择余地很小,因此,它会进入KeBugCheck(),这样做的结果当然是使系统以受控的方式终止。不过这种情况应该从来不会发生,但如果它发生了,那表示系统必然出现了很严重的错误,因此,使系统终止是最佳解决方案。

 

 

如果发现了匹配的SPY_CALLHook Dispatcher将结束它的工作。最后的动作是调用日志记录函数SpyHookProtocol(),需要给该函数传入一个指向SPY_CALL结构的指针。日志记录所需的信息都保存在该结构中。当SpyHookProtocol()返回后,Dispatcher就释放它刚才使用的SPY_CALL,恢复所有的CPU寄存器,然后返回到调用者。

 

 

 

 

API HOOK协议

一个好的API Spy应该可以在原始函数被调用后还能察看它使用的参数,因为函数可能会通过传入的缓冲区返回附加的数据。因此,日志函数SpyHookProtocol()hook例程结束时将被调用,而此时API函数还未返回到调用者。在讨论它的实现秘诀之前,请先看看下面给出的两个示例性的协议(Protocol),它们会为你提供一个大概的方向。5-6是在命令行下执行dir c:/时产生的日志文件的快照。

 

 

请对比5-6中列出的日志项和列表5-6给出的协议格式化字符串。在示列5-1中,NtOpenFile()NtClose()的格式化字符串分别对应5-6中的第一行和第四行。它们有着惊人的相似处;每一个格式化控制ID都紧随在一个%号后(参考5-2),与其相关的参数项将包含在协议中。不过,协议还包含一些附加的信息,这些信息明显不属于格式字符串。稍后我将解释这种差异的原因。

 

 

示例5-2给出了一个协议项的一般格式。每一项包含相同个数的域,这些域采用分隔符隔开。这样分隔可以使程序很容易的解析它。这些域按照如下的一组简单的基本规则来构建:

l         所有的数字都已十六进制表示,没有0前缀或常见的前缀“0x

l         函数的多个参数由逗号隔开

l         字符串参数将位于一对双引号中

l         结构体成员的值由“.”符号隔开

 

 


5-6.  命令dir c:/的示列协议

 

 

"%s=NtOpenFile(%+,%n,%o,%i,%n, %n) "

18:sO=NtOpenFile(+46C.18,nl00001,o"/??/C:/",i0.1,n3,n4021)lBFEE5AE05B6710,278,2

 

 

"%s=NtClose(%-l)"

lB:sO=NtClose(-46C.18="/??/C:/")lBFEE5AE05B6710,278,l

示列5-1.  比较格式化字符串和协议项

 

 

<#> : <status>=<function> (<arguments>) <time> , <thread>, <handles>

示列5-2.  协议项的一般格式

 

 

l         与句柄相关的对象名称和句柄的值采用“=”进行分割。

l         日期/时间的stamp1601-01-01至今逝去的毫秒数,其格式依赖Windows 2000的基本时间格式,精度可达到1/10毫秒。

l         线程ID是调用API函数的线程的唯一数字标识。

l         句柄计数的状态表示当前注册到Spy设备句柄列表中的句柄的数量。协议函数使用该列表查找与对象名称相关的句柄。

 

 

5-7.  命令type c:/boot.ini的示列协议

 

 

5-7是在控制台中执行:type c:/boot.ini命令产生的API Spy协议结果。下面给出日志项中的某些列的含义:

l         0x31行,调用了NtCreateFile()来打开/??/c:/boot.ini文件。(o”/??/c:/boot.ini”)该函数返回的NTSTATUS的值为0s0),即STATUS_SUCCESS,并分配了一个新的文件句柄,其值为08,该句柄属于进程

抱歉!评论已关闭.