热门搜索:Photoshop 平面设计 Linux Vista Windows ASP.NET qq word 病毒 XP Excel 标志设计 

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

来源:佚名(读取中...) 2005-8-17 【字体: 】 切换为
通用解决方案的主要障碍是 C 语言的典型参数传递机制。就像你知道的, C 通常在调用函数的入口点之前会将函数参数传递到 CPU 堆栈中。根据函数需要的参数数量,参数堆栈的大小将有很大的差别。 windows 2000 的 248 个 Native API 函数需要的参数堆栈的大小位于 0 到 68 字节。

翻译: Kendiv( fcczj@263.net )

更新: Thursday, February 24, 2005

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

汇编语言的救援行动

通用解决方案的主要障碍是 C 语言的典型参数传递机制。就像你知道的, C 通常在调用函数的入口点之前会将函数参数传递到 CPU 堆栈中。根据函数需要的参数数量,参数堆栈的大小将有很大的差别。 windows 2000 的 248 个 Native API 函数需要的参数堆栈的大小位于 0 到 68 字节。这使得编写一个唯一的 hook 函数变得非常困难。微软的 Visual C/C++ 提供了一个完整的汇编( ASM )编译器,该编译器可处理复杂度适中的代码。具有讽刺意味的是,在我的解决方案中所使用的汇编语言的优点正是通常被认为是其最大缺点的特性:汇编语言不提供严格的类型检查机制。只要字节数正确就一切 OK 了,你可以在任何寄存器中存储几乎所有的东西,而且你可以调用任何地址,而不需要关心当前堆栈的内容是什么。尽管这在应用程序开发中是一种很危险的特性,但这确实最容易获取的:在汇编语言中,很容易以不同的参数堆栈调用同一个普通的入口点,稍后将介绍的 API hook Dispatcher 将采用这一特性。

通过将汇编代码放入以关键字 __asm 标记的分隔块中就可调用 Microsoft Visual C/C++ 嵌入式汇编程序。嵌入式汇编缺少宏定义以及 Microsoft's big Macro Assembler ( MASM )的评估能力,但这些并没有严重的限制它的可用性。嵌入式汇编的最佳特性是:它可以访问所有的 C 变量和类型定义,因此很容易混合 C 和 ASM 代码。不过,当在 C 函数中包含有 ASM 代码时,就必须遵守 C 编译器的某些重要的基本约定,以避免和 C 代码的冲突:

l C 函数调用者假定 CPU 寄存器 EBP 、 EBX 、 ESI 和 EDI 已经被保存了。

l 如果在单一函数中,将 ASM 代码和 C 代码混合在一起,则需要小心的保存 C 代码可能保存在寄存器中的中间值。总是保存和恢复在 __asm 语句中使用的所有寄存器。

l 8 位的函数结果( CHAR , BYTE 等)由寄存器 AL 返回。

l 16 位的函数结果( SHORT , WORD 等)由寄存器 AX 返回。

l 32 位的函数结果( INT , LONG , DWORD 等)由寄存器 EAX 返回。

l 64 位的函数结果( __int64 , LONGLONG , DWORDLONG 等)由寄存器对 EDX : EAX 返回。寄存器 EAX 包含 0 到 31 位, EDX 保存 32 到 63 位。

l 有确定参数的函数通常按照 __stdcall 约定进行参数的传递。从调用者的角度来看,这意味着在函数调用之前参数必须以相反的顺序压入堆栈中,被调用的函数负责在返回前从堆栈中移除它们。从被调用的函数的角度来看,这意味着堆栈指针 ESP 指向调用者的返回地址,该地址紧随最后一个参数(按照原始顺序)。( 译注 :这意味着,最先被压入堆栈的是函数的返回地址 )参数的原始顺序被保留下来,因为堆栈是向下增长的,从高位线性地址到低位线性地址。因此,调用者压入堆栈的最后一个参数(即,参数 #1 )将是由 ESP 指向的数组中的第一个参数。

l 某些有确定参数的 API 函数,如著名的 C 运行时库函数(由 ntdll.dll 和 ntoskrnl.exe 导出),通常使用 __cdecl 调用约定,该约定采用与 __stdcall 相同的参数顺序,但强制调用者清理参数堆栈。

l 由 __fastcall 修饰的函数声明,则希望前两个参数位于 CPU 寄存器 ECX 和 EDX 中。如果还需要更多的参数,它们将按照相反的顺序传入堆栈,最后由被调用者清理堆栈,这和 __stdcall 相同。

; this is the function's prologue

push ebp ; save current value ebp

mov ebp, esp ; set stack frame base address

sub esp, SizeOfLocalStorage ; create local storage area

; this is the function's epilogue

mov esp, ebp ; destroy local storage area

pop ebp ; restore value of ebp

ret

列表 5-2. 堆栈帧,序言和尾声

l 很多 C 编译器在进入函数后,会立即针对函数参数构建一个堆栈帧,这需要使用 CPU 的基地址指针寄存器 EBP 。 列表 5-2 给出了此代码,这通常被称为函数的“序言”和“尾声”。有些编译器采用更简洁的 i386 的 ENTER 和 LEAVE 操作符,在“序言被执行后,堆栈将如 5-3 所示。 EBP 寄存器作为一分割点将函数的参数堆栈划分为两部分:( 1 )局部存储区域,该区域中包含所有定义于函数范围内的局部变量( 2 )调用者堆栈,其中保存有 EBP 的备份和返回地址。注意,微软的 Visual C/C++ 的最新版中默认不使用堆栈帧。替代的是,代码通过 ESP 寄存器访问堆栈中的值,不过这需要指定变量相对于当前栈顶的偏移量。这种类型的代码非常难以阅读,因为每个 PUSH 和 POP 指令都会影响 ESP 的值和所有参数的偏移量。在此种情况下不再需要 EBP ,它将作为一个附加的通用寄存器。

l 在访问 C 变量时必须非常小心。经常出现在嵌入式 ASM 中的 bug 是:你将一个变量的地址而不是它的值加载到了寄存器中。使用 ptr 和 offset 地址操作符存在潜在的二义性。例如,指令: mov eax , dword ptr SomeVariable 将加载 DWORD 类型的 SomeVariable 变量的值到 EAX 寄存器,但是, mov eax , offset SomeVariable 将加载它的线性地址到 EAX 中。

《Undocumented

图 5-3. 堆栈帧的典型布局

Hook 分派程序(Hook Dispatcher)

这部分的代码将较难理解。编写它们花费了我很多时间,而且在这一过程中我还欣赏了无数的蓝屏。我最初的方法是提供一个完全用汇编语言编写的模块。不过,这个方法在链接阶时带来了很大的麻烦,因此,我改为在 C 模块中使用嵌入式汇编。为了避免创建另一个内核模式的驱动程序,我决定将 hook 代码整合到 Spy 设备驱动程序中。还记得在 4-2 底部列出的形如 SPY_IO_HOOK_* 的 IOCTL 函数吗?现在我们将和它们来一次亲密接触。后面的示列代码来自 w2k_spy.c 和 w2k_spy.h ,可以在随书 CD 的 \src\w2k_spy 中找到它们。

列表 5-3 的核心部分是 Native API Hook 机制的实现代码。该列表开始处是一对常量和结构体定义,后面的 aSpyHooks[] 需要它们。紧随这个数组的是一个宏,该宏实际上是三行嵌入式汇编语句,这三行汇编语句非常重要,稍后我将介绍它们。 列表 5-3 的最后一部分用来建立 SpyHookInitializeEx() 函数。猛地一看,这个函数的功能似乎很难理解。该函数组合了一下两个功能:

1. SpyHookInitializeEx() 的表面部分包括一段用来设置 aSpyHooks[] 数组的 C 代码,这部分代码用 Spy 设备的 Hook 函数指针以及与之相关联的字符串格式协议来初始化 aSpyHooks[] 数组。 SpyHookInitializeEx() 函数可被分割为两部分:第一部分到第一个 __asm 语句后的 jmp SpyHook9 指令。第二部分显然是从 ASM 标签 ----SpyHook9 开始,该部分位于第二个 __asm 语句块的最后。

2. SpyHookInitializeEx() 的内部部分包括位于两块 C 代码段之间的所有代码。这部分在一开始大量使用了 SpyHook 宏,紧随其后的是一大块复杂的汇编代码。可能你已经猜到了,这些汇编代码就是前面提到的通用 Hook 例程。

[1] [2] [3] 下一页

关注此文的读者还看过:
    用户评论
评论内容:不能超过100字,需审核,请自觉遵守互联网相关政策法规。
发表评论: 匿名发表 用户名: loading 位网友发表了评论 查看评论
(0/100)