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

第五章 监控 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 代码还未讨论,这段代码由 SpyHook2 和 SpyHook9 标识 。这段代码的一个有趣的特性是:在 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_CALL。Hook Dispatcher 使用 CPU 的 XCHG 指令来加载和设置该成员的值(译注: XCHG 指令可以保证此操作为原子操作) 。这一点非常重要,因为当代码运行于多线程环境中时,读写全局数据时必须采取保护措施以避免条件竞争 。如果在数组中找到了一个空的 SPY_CALL ,dispatcher 就会将调用者的线程 ID (通过 PsGetCurrentThreadId() 获取)、与当前 API 函数相关的 SPY_HOOK_ENTRY 结构的地址以及整个参数堆栈保存到该 SPY_CALL 结构中 。需要复制的参数的字节数取自 KiArqumentTable 数组,该数组保存在系统的 SDT 中 。如果所有的 SPY_CALL 都被使用了,原始的 API 函数处理例程将被调用而不会产生任何日志记录 。

推荐阅读