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

第五章  监控Native API调用

翻译:Kendiv( [email protected] )

更新: 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         结构体成员的值由“.”符号隔开

 

 

《Undocumented Windows 2000 Secrets》翻译 --- 第五章(3)_第1张图片

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,该句柄属于进程0x46c+46C.18)。因此,句柄计数从1增加到2

l         0x36行,type命令将文件/??/c:/boot.ini的前512个字节(n200)读入位于线性地址0x0012F5B4处的缓冲区中,并把从NtCreateFile()获取的句柄解析给NtReadFile()函数。系统成功的返回512字节(io.200)。

l         0x39行,将处理另一块512个字节的文件块。这一次,将到达文件的末尾,因此NtReadFile()仅返回了75个字节(io.4B)。显然,我的boot.ini文件的大小为:512+75=587字节。

l         0x3C行,NtClose()成功的释放了指向/??/c:/boot.ini的文件句柄(-46.18=”/??/c:/boot.ini”),因此,句柄计数将从2减少为1

 

 

现在,你应该已经明白Spy协议的API是如何构建的了,这会帮助你掌握协议生成机制的细节,接下来我们将讨论这一机制。在前面我曾提及过,用于日志记录的主要API函数是SpyHookProtocol()列表5-7给出了该函数,它将使用SPY_CALL结构中的数据来为每个API函数生成一个协议记录并将其写入一个环形缓冲区中,这里的SPY_CALL结构由Hook Dispatcher传入。一个Spy设备的客户端可以通过IOCTL调用来读去这一协议。每个记录项都是一行文本,每行都由单个行结束符(即C语言中的”/n”)表示行的结束。通过使用内核的Mutext KMUTEX kmProtcol来实现串行读去协议缓冲区,kmProtocol位于Spy设备的全局结构DEVICE_CONTEXT中。列表5-7中的SpyHookWait()SpyHookRelease()函数用于请求和释放此Mutext对象。所有对协议缓冲区的访问都必须由SpyHookWait()预处理并在结束时由SpyHookRelease()处理,SpyHookProtocol()函数展示了这种行为。

 

 

NTSTATUS SpyHookWait (void)

    {

    return MUTEX_WAIT (gpDeviceContext->kmProtocol);

    }

 

 

// -----------------------------------------------------------------

 

 

LONG SpyHookRelease (void)

    {

    return MUTEX_RELEASE (gpDeviceContext->kmProtocol);

    }

 

 

// -----------------------------------------------------------------

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

 

 

void SpyHookProtocol (PSPY_CALL psc)

    {

    LARGE_INTEGER liTime;

    PSPY_PROTOCOL psp = &gpDeviceContext->SpyProtocol;

 

 

    KeQuerySystemTime (&liTime);

 

 

    SpyHookWait ();

 

 

    if (SpyWriteFilter (psp, psc->pshe->pbFormat,

                             psc->adParameters,

                             psc->dParameters))

        {

        SpyWriteNumber (psp, 0, ++(psp->sh.dCalls));   // <#>:

        SpyWriteChar   (psp, 0, ':');

                                                  // <status>=

        SpyWriteFormat (psp, psc->pshe->pbFormat, //  <function>

                             psc->adParameters);  //   (<arguments>)

 

 

        SpyWriteLarge  (psp, 0, &liTime);              // <time>,

        SpyWriteChar   (psp, 0, ',');

 

 

        SpyWriteNumber (psp, 0, (DWORD) psc->hThread); // <thread>,

        SpyWriteChar   (psp, 0, ',');

 

 

        SpyWriteNumber (psp, 0, psp->sh.dHandles);     // <handles>

        SpyWriteChar   (psp, 0, '/n');

        }

    SpyHookRelease ();

    return;

    }

列表5-7.  主要的Hook协议函数SpyHookProtocol()

 

 

如果你比较一下列表5-7给出的SpyHookProtocol()函数的主要部分和示列5-2给出的协议项的一般格式,将很容易找出那个语句生成了协议项中的哪一个域(field)。这样一来一切就很清楚了为什么列表5-6中的协议字符串没有说明整个数据项---有些独立于功能的数据将由SpyHookProtocol()添加,而这将不需要格式字符串的帮助。SpyHookProtocl()的核心调用是SpyWriteFormat(),该函数生成<status>=<function>[<arguments>]部分,这依赖于与要记录的当前API函数相关的格式字符串。请参考位于随书光盘的/src/w2k_spy目录下的源文件w2k_spy.cw2k_spy.h,以获取Spy设备驱动程序中使用的SpyWrite*()函数的更多实现信息。

 

 

请注意,这些代码稍微有些危险。这些代码编写与1997年是针对Windows NT 4.0的。在移植到Windows 2000之后,当hook工作一段较长时间后会偶尔引发蓝屏。更糟糕的是,有些特殊的操作将立即引发蓝屏,例如,在My Favoriter文本编辑器的File/Open对话框中打开我的电脑时。在分析过多过crash dump后,我发现是由于将NULL指针传递给了某些函数从而导致了系统崩溃。一但Spy设备试图使用这些指针中的某个来记录该指针引用的数据时,系统就会崩溃。典型的就是,指向IO_STATUS_BLOCK结构的指针,在UNICODE_STRINGOBJECT_ATTRIBUTES结构中存在无效的字符串指针。我还发现某些有Buffer成员的UNICODE_STRING结构没有/0结束符。因此,我再次强调你不应该假定所有的UNICODE_STRING结构都以/0结束。在不能确定时,请使用Length成员,它总能正确地告诉你在Buffer中存放的有效的字节数。

 

 

为了修正这一问题,我为所有使用客户指针的日志函数增加了指针有效性检查。在结束时,我使用第四章讨论过的SpyMemoryTestAddress()函数来检验一个线性地址指针是否指向一个有效的页表项(PTE)。更详细的信息请参考列表4-22列表4-24。另一种可能的替代方案是使用结构化异常(__try/__except)。

 

 

…………….待续………………

你可能感兴趣的:(thread,windows,api,function,hook,attributes)