第五章 监控Native API调用
翻译:Kendiv( fcczj@263.net )
更新:
声明:转载请注明出处,并保证文章的完整性,本人保留译文的所有权利。
汇编语言的救援行动
通用解决方案的主要障碍是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中。

图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例程。
#define SPY_CALLS 0x00000100 // max api call nesting level
#define SDT_SYMBOLS_NT4 0xD3
#define SDT_SYMBOLS_NT5 0xF8
#define SDT_SYMBOLS_MAX SDT_SYMBOLS_NT5
// -----------------------------------------------------------------
typedef struct _SPY_HOOK_ENTRY
{
NTPROC Handler;
PBYTE pbFormat;
}
SPY_HOOK_ENTRY, *PSPY_HOOK_ENTRY, **PPSPY_HOOK_ENTRY;
#define SPY_HOOK_ENTRY_ sizeof (SPY_HOOK_ENTRY)
// -----------------------------------------------------------------
typedef struct _SPY_CALL
{
BOOL fInUse; // set if used entry
HANDLE hThread; // id of calling thread
PSPY_HOOK_ENTRY pshe; // associated hook entry
PVOID pCaller; // caller''''s return address
DWORD dParameters; // number of parameters
DWORD adParameters [1+256]; // result and parameters
}
SPY_CALL, *PSPY_CALL, **PPSPY_CALL;
#define SPY_CALL_ sizeof (SPY_CALL)
// -----------------------------------------------------------------
SPY_HOOK_ENTRY aSpyHooks [SDT_SYMBOLS_MAX];
// -----------------------------------------------------------------
// The SpyHook macro defines a hook entry point in inline assembly
// language. The common entry point SpyHook2 is entered by a call
// instruction, allowing the hook to be identified by its return
// address on the stack. The call is executed through a register to
// remove any degrees of freedom from the encoding of the call.
#define SpyHook \
__asm push eax \
__asm mov eax, offset SpyHook2 \
__asm call eax
// -----------------------------------------------------------------
// The SpyHookInitializeEx() function initializes the aSpyHooks[]
// array with the hook entry points and format strings. It also
// hosts the hook entry points and the hook dispatcher.
// -----------------------------------------------------------------
// The SpyHookInitializeEx() function initializes the aSpyHooks[]
// array with the hook entry points and format strings. It also
// hosts the hook entry points and the hook dispatcher.
void SpyHookInitializeEx (PPBYTE ppbSymbols,
PPBYTE ppbFormats)
{
DWORD dHooks1, dHooks2, i, j, n;
__asm
{
jmp SpyHook9
ALIGN 8
SpyHook1: ; start of hook entry point section
}
// the number of entry points defined in this section
// must be equal to SDT_SYMBOLS_MAX (i.e. 0xF8)
SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook //08
SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook //10
SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook //18
SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook //20
SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook //28
SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook //30
SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook //38
SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook //40
SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook //48
SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook //50
SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook //58
SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook //60
SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook //68
SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook //70
SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook //78
SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook //80
SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook //88
SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook //90
SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook //98
SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook //A0
SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook //A8
SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook //B0
SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook //B8
SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook //C0
SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook //C8
SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook //D0
SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook //D8
SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook //E0
SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook //E8
SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook //F0
SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook SpyHook //F8
__asm
{
SpyHook2: ; end of hook entry point section
pop eax ; get stub return address
pushfd
push ebx
push ecx
push edx
push ebp
push esi
push edi
sub eax, offset SpyHook1 ; compute entry point index
mov ecx, SDT_SYMBOLS_MAX
mul ecx
mov ecx, offset SpyHook2
sub ecx, offset SpyHook1
div ecx
dec eax
mov ecx, gfSpyHookPause ; test pause flag
add ecx, -1
sbb ecx, ecx
not ecx
lea edx, [aSpyHooks + eax * SIZE SPY_HOOK_ENTRY]
test ecx, [edx.pbFormat] ; format string == NULL?
jz SpyHook5
push eax
push edx
call PsGetCurrentThreadId ; get thread id
mov ebx, eax
pop edx
pop eax
cmp ebx, ghSpyHookThread ; ignore hook installer
jz SpyHook5
mov edi, gpDeviceContext
lea edi, [edi.SpyCalls] ; get call context array
mov esi, SPY_CALLS ; get number of entries
SpyHook3:
mov ecx, 1 ; set in-use flag
xchg ecx, [edi.fInUse]
jecxz SpyHook4 ; unused entry found
add edi, SIZE SPY_CALL ; try next entry
dec esi
jnz SpyHook3
mov edi, gpDeviceContext
inc [edi.dMisses] ; count misses
jmp SpyHook5 ; array overflow
SpyHook4:
mov esi, gpDeviceContext
inc [esi.dLevel] ; set nesting level
mov [edi.hThread], ebx ; save thread id
mov [edi.pshe], edx ; save PSPY_HOOK_ENTRY
mov ecx, offset SpyHook6 ; set new return address
xchg ecx, [esp+20h]
mov [edi.pCaller], ecx ; save old return address
mov ecx, KeServiceDescriptorTable
mov ecx, [ecx].ntoskrnl.ArgumentTable
movzx ecx, byte ptr [ecx+eax] ; get argument stack size
shr ecx, 2
inc ecx ; add 1 for result slot
mov [edi.dParameters], ecx ; save number of parameters
lea edi, [edi.adParameters]
xor eax, eax ; initialize result slot
&nbs