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

第五章  监控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,该句柄属于进程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)。

 

 

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
This book documents what goes on under the covers in Windows NT. Three experts share what they've dug up on NT through years of hands-on research and programming experience. The authors dissect the Win32 interface, deconstruct the underlying APIs, and decipher the Memory Management architecture to help you understand operations, fix flaws, and enhance performance. Table of Contents Chapter 1: Windows NT: An Inside Look EVALUATING WINDOWS NT DELVING INTO THE WINDOWS NT ARCHITECTURE SUMMARY Chapter 2: Writing Windows NT Device Drivers PREREQUISITES TO WRITING NT DEVICE DRIVERS DRIVER BUILD PROCEDURE STRUCTURE OF A DEVICE DRIVER SUMMARY Chapter 3: Win32 Implementations: A Comparative Look WIN32 API IMPLEMENTATION ON WINDOWS 95 WIN32 API IMPLEMENTATION ON WINDOWS NT WIN32 IMPLEMENTATION DIFFERENCES SUMMARY Chapter 4: Memory Management MEMORY MODELS IN MICROSOFT OPERATING SYSTEMS WINDOWS NT MEMORY MANAGEMENT OVERVIEW BELOW THE OPERATING SYSTEM THE INSIDE LOOK VIRTUAL MEMORY MANAGEMENT VIRTUAL ADDRESS DESCRIPTORS IMPACT ON HOOKING SWITCHING CONTEXT DIFFERENCES BETWEEN WINDOWS NT AND WINDOWS 95/98 SUMMARY Chapter 5: Reverse Engineering Techniques HOW TO PREPARE FOR REVERSE ENGINEERING HOW TO REVERSE ENGINEER UNDERSTANDING CODE GENERATION PATTERNS HOW WINDOWS NT PROVIDES DEBUGGING INFORMATION HOW TO DECIPHER THE PARAMETERS PASSED TO AN UNDOCUMENTED FUNCTION TYPICAL ASSEMBLY LANGUAGE PATTERNS AND THEIR MEANINGS THE PRACTICAL APPLICATION OF REVERSE ENGINEERING SUMMARY Chapter 6: Hooking Windows NT System Services SYSTEM SERVICES: THE LONG VIEW NEED FOR HOOKING SYSTEM SERVICES TYPES OF HOOKS IMPLEMENTATIONS OF HOOKS WINDOWS NT SYSTEM SERVICES HOOKING NT SYSTEM SERVICES SUMMARY Chapter 7: Adding New System Services to the Windows NT Kernal DETAILED IMPLEMENTATION OF A SYSTEM SERVICE IN WINDOWS NT ADDING NEW SYSTEM SERVICES EXAMPLE OF ADDING A NEW SYSTEM SERVICE SUMMARY Chapter 8: Local Procedure Call THE ORIGIN OF THE SUBSYSTEMS LOCAL PROCEDURE CALL PORT-RELATED FUNCTIONS LPC SAMPLE PROGRAMS QUICK LPC SUMMARY Chapter 9: Hooking Software Interrupts WHAT ARE INTERRUPTS? HOW OPERATING SYSTEMS USE SOFTWARE INTERRUPTS WHY SOFTWARE INTERRUPTS NEED TO BE HOOKED HOW TO HOOK SOFTWARE INTERRUPTS SUMMARY Chapter 10: Adding New Software Interrupts WHAT HAPPENS WHEN A 32-BIT APPLICATION EXECUTES AN INT NN INSTRUCTION? ADDING NEW SOFTWARE INTERRUPTS TO THE WINDOWS NT KERNEL USING CALLGATES TO EXECUTE PRIVILEGED CODE HOW TO USE THE CALLGATE TECHNIQUE PAGING ISSUES SUMMARY Chapter 11: Portable Executable File Format OVERVIEW OF A PE FILE STRUCTURE OF A PE FILE RELATIVE VIRTUAL ADDRESS DETAILS OF THE PE FORMAT INDICES IN THE DATA DIRECTORY LOADING PROCEDURE SUMMARY
Table of Contents Chapter 1: Windows NT: An Inside Look EVALUATING WINDOWS NT DELVING INTO THE WINDOWS NT ARCHITECTURE SUMMARY Chapter 2: Writing Windows NT Device Drivers PREREQUISITES TO WRITING NT DEVICE DRIVERS DRIVER BUILD PROCEDURE STRUCTURE OF A DEVICE DRIVER SUMMARY Chapter 3: Win32 Implementations: A Comparative Look WIN32 API IMPLEMENTATION ON WINDOWS 95 WIN32 API IMPLEMENTATION ON WINDOWS NT WIN32 IMPLEMENTATION DIFFERENCES SUMMARY Chapter 4: Memory Management MEMORY MODELS IN MICROSOFT OPERATING SYSTEMS WINDOWS NT MEMORY MANAGEMENT OVERVIEW BELOW THE OPERATING SYSTEM THE INSIDE LOOK VIRTUAL MEMORY MANAGEMENT VIRTUAL ADDRESS DESCRIPTORS IMPACT ON HOOKING SWITCHING CONTEXT DIFFERENCES BETWEEN WINDOWS NT AND WINDOWS 95/98 SUMMARY Chapter 5: Reverse Engineering Techniques HOW TO PREPARE FOR REVERSE ENGINEERING HOW TO REVERSE ENGINEER UNDERSTANDING CODE GENERATION PATTERNS HOW WINDOWS NT PROVIDES DEBUGGING INFORMATION HOW TO DECIPHER THE PARAMETERS PASSED TO AN UNDOCUMENTED FUNCTION TYPICAL ASSEMBLY LANGUAGE PATTERNS AND THEIR MEANINGS THE PRACTICAL APPLICATION OF REVERSE ENGINEERING SUMMARY Chapter 6: Hooking Windows NT System Services SYSTEM SERVICES: THE LONG VIEW NEED FOR HOOKING SYSTEM SERVICES TYPES OF HOOKS IMPLEMENTATIONS OF HOOKS WINDOWS NT SYSTEM SERVICES HOOKING NT SYSTEM SERVICES SUMMARY Chapter 7: Adding New System Services to the Windows NT Kernal DETAILED IMPLEMENTATION OF A SYSTEM SERVICE IN WINDOWS NT ADDING NEW SYSTEM SERVICES EXAMPLE OF ADDING A NEW SYSTEM SERVICE SUMMARY Chapter 8: Local Procedure Call THE ORIGIN OF THE SUBSYSTEMS LOCAL PROCEDURE CALL PORT-RELATED FUNCTIONS LPC SAMPLE PROGRAMS QUICK LPC SUMMARY Chapter 9: Hooking Software Interrupts WHAT ARE INTERRUPTS? HOW OPERATING SYSTEMS USE SOFTWARE INTERRUPTS WHY SOFTWARE INTERRUPTS NEED TO BE HOOKED HOW TO HOOK SOFTWARE INTERRUPTS SUMMARY Chapter 10: Adding New Software Interrupts WHAT HAPPENS WHEN A 32-BIT APPLICATION EXECUTES AN INT NN INSTRUCTION? ADDING NEW SOFTWARE INTERRUPTS TO THE WINDOWS NT KERNEL USING CALLGATES TO EXECUTE PRIVILEGED CODE HOW TO USE THE CALLGATE TECHNIQUE PAGING ISSUES SUMMARY Chapter 11: Portable Executable File Format OVERVIEW OF A PE FILE STRUCTURE OF A PE FILE RELATIVE VIRTUAL ADDRESS DETAILS OF THE PE FORMAT INDICES IN THE DATA DIRECTORY LOADING PROCEDURE SUMMARY

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值