第五章 监控Native API调用
翻译: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中。
图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
stosd
dec ecx
jz SpyHook5 ; no arguments
lea esi, [esp+24h] ; save argument stack
rep movsd
SpyHook5:
mov eax, [edx.Handler] ; get original handler
pop edi
pop esi
pop ebp
pop edx
pop ecx
pop ebx
popfd
xchg eax, [esp] ; restore eax and...
ret ; ...jump to handler
SpyHook6:
push eax
pushfd
push ebx
push ecx
push edx
push ebp
push esi
push edi
push eax
call PsGetCurrentThreadId ; get thread id
mov ebx, eax
pop eax
mov edi, gpDeviceContext
lea edi, [edi.SpyCalls] ; get call context array
mov esi, SPY_CALLS ; get number of entries
SpyHook7:
cmp ebx, [edi.hThread] ; find matching thread id
jz SpyHook8
add edi, SIZE SPY_CALL ; try next entry
dec esi
jnz SpyHook7
push ebx ; entry not found ?!?
call KeBugCheck
SpyHook8:
push edi ; save SPY_CALL pointer
mov [edi.adParameters], eax ; store NTSTATUS
push edi
call SpyHookProtocol
pop edi ; restore SPY_CALL pointer
mov eax, [edi.pCaller]
mov [edi.hThread], 0 ; clear thread id
mov esi, gpDeviceContext
dec [esi.dLevel] ; reset nesting level
dec [edi.fInUse] ; clear in-use flag
pop edi
pop esi
pop ebp
pop edx
pop ecx
pop ebx
popfd
xchg eax, [esp] ; restore eax and...
ret ; ...return to caller
SpyHook9:
mov dHooks1, offset SpyHook1
mov dHooks2, offset SpyHook2
}
n = (dHooks2 - dHooks1) / SDT_SYMBOLS_MAX;
for (i = j = 0; i < SDT_SYMBOLS_MAX; i++, dHooks1 += n)
{
if ((ppbSymbols != NULL) && (ppbFormats != NULL) &&
(ppbSymbols [j] != NULL))
{
aSpyHooks [i].Handler = (NTPROC) dHooks1;
aSpyHooks [i].pbFormat =
SpySearchFormat (ppbSymbols [j++], ppbFormats);
}
else
{
aSpyHooks [i].Handler = NULL;
aSpyHooks [i].pbFormat = NULL;
}
}
return;
}
列表5-3. Hook Dispatcher的实现方式
SpyHook宏实际是什么呢?在SpyHookInitializeEx()函数中,这个宏被重复了多大248(0xF8)次,这正好是Windows 2000 Native API函数的数目。在列表5-3的顶部,这个数目被定义为SDT_SYMBOLS_MAX常量,该宏可以使SDT_SYMBOLS_NT4或SDT_SYMBOLS_NT5。因为我打算支持Windows NT 4.0。回到SpyHook宏上来:该宏调用的汇编语句在列表5-4中给出了。每个SpyHook都产生同样的三行代码:
1. 第一行,将当前EAX寄存器的内容保存到堆栈中。
2. 第二行,将SpyHook2的线性地址保存到EAX中。
3. 第三行,调用EAX中的地址(即:call eax)。
你可能会惊讶:当这个CALL返回时会发生什么。接下来的一组SpyHook代码会被调用吗?不----这个CALL并不支持返回,因为在到达SpyHook2之后,这个CALL的返回地址就会被立即从堆栈中移出,列表5-4最后的POP EAX指令可以证明这一点。这种看上去毫无疑义的代码在古老的汇编程序设计时代曾被广泛的讨论的一种技巧,就像今天我们讨论面向对象的程序设计一样。当ASM老大级人物需要构建一个数组,而此数组的每一项都有类似的进入点,但却需要被分派到独立的函数时,就会采用这种技巧。对所有进入点使用几乎相同的代码可以保证它们之间有相等的间隔,因此客户端就可以很容易的通过CALL指令的返回地址计算出进入点的在数组中的索引值,数组的基地址和大小以及数组中共有多少项
SpyHook1:
push eax
mov eax, offset SpyHook2
call eax
push eax
mov eax, offset SpyHook2
call eax
;244 boring repetitions cimitted
push eax
mov eax, offset SpyHook2
call eax
push eax
mov eax, offset SpyHook2
call eax
SpyHook2:
pop eax
列表5-4. 扩充SpyHook宏调用
例如,列表5-4中第一个CALL EAX指令的返回地址是其下一个语句的地址。通常,第N个CALL EAX指令的返回地址是第N+1个语句的地址,但最后一个除外,最后这个将返回SpyHook2。因此,从0开始的所有进入点的索引可以由图5-4中的通用公式计算出来。这三条规则中的潜在规则是:SDT_SYMBOLS_MAX进入点符合内存块SpyHook2---SpyHook1。那么有多少个进入点符合ReturnAddress---SpyHook1呢?因为计算结果是位于0到SDT_SYMBOLS_MAX中的某一个数值,所以,肯定要使用该数值来获取一个从0开始的索引。
图5-4. 通过Hook进入点的返回地址确定一个Hook进入点
图5-4所示公式的实现方式可以在列表5-3中找到,在汇编标签SpyHook2的右边。在图5-5的左下角也给出了该公式的实现代码,它展示了Hook Dispatcher机制的基本原理。注意,i386的mul指令会在EDX:EAX寄存器中产生一个64位的结果值,这正是其后的div指令所期望的,因此,这里没有整数溢出的危险。在图5-5的左上角,是对KiServiceTable的描述,该表将被SpyHook宏生成的进入点地址修改。在图的中部展示了展开后的宏代码(来自列表5-4中)。进入点的线性地址位于图的右手边。为了完全一致,每个进入点的大小都是8字节,因此,通过将KiServiceTable中每个函数的索引值乘以8,然后再将乘积加上SpyHook1的地址就可得出进入点的地址。
事实上,每个进入点并不都是纯粹的8字节长。我花费了大量的时间来寻找最佳的hook函数的实现方式。尽管按照32位边界对齐代码并不是必须的,但这从来都不是个坏主意,因为这会提高性能。当然,能提升的性能十分有限。你或许会奇怪:为什么我要通过EAX寄存器间接的调用SpyHook2,而不是直接使用CALL SpyHook2指令,这不是更高效吗?是的!不过,问题是i386的CALL(还有jmp)指令可以有多种实现方式,而且都具有相同的效果,但是产生的指令大小却不相同。请参考:Intel’s Instruction Set Reference of the Pentium CPU family(Intel 199c)。因为最终的实现方式要由编译器/汇编器来确定,这不能保证所有的进入点都会有相同的编码。换句话说,MOV EAX和一个32位常量操作数总是以相同的方式编码,同样的,这也适用于CALL EAX指令。
图5-5. Hook Dispatcher的功能原理
列表5-3中还有一点需要澄清。让我们从SpyHook9标签后的最后一快C代码段开始。紧随SpyHook9之后的汇编代码将SpyHook1和SpyHook2的线性地址保存在dHook1和dHook2变量中。接下来,变量n被设为每个进入点的大小(由进入点数组的大小除以进入点的个数而得出)。当然,这个值将是8。列表5-3的剩余部分是一个循环语句,用来初始化全局数组aSpyHooks[]中的所有项。这个数组所包含的SPY_HOOK_ENTRY结构定义于列表5-3的顶部,该数组中的每一项都对应一个Native API函数。要理解该结构中的Handler和pbFormat成员是如何被设置的,就必须进一步了解传递给SpyHookInitializeEx()的ppbSymbols和ppbFormats参数,列表5-5给出了外包函数SpyHookInitialize(),该函数会选择适合当前OS版本的参数来调用SpyHookInitializeEx()。前面已经提示过,我使用的代码不直接测试OS版本或Build Number,而是用常量SPY_SYMBOLS_NT4、SPY_SYMBOLS_NT5和SDT中与ntoskrnl.exe相关的ServiceLimit成员的值进行比较。如果没有一个匹配,Spy设备将把aSpyHooks[]数组内容全部初始化为NULL,从而有效的禁止Native API Hook机制。
BOOL SpyHookInitialize (void)
{
BOOL fOk = TRUE;
switch (KeServiceDescriptorTable->ntoskrnl.ServiceLimit)
{
case SDT_SYMBOLS_NT4:
{
SpyHookInitializeEx (apbSdtSymbolsNT4, apbSdtFormats);
break;
}
case SDT_SYMBOLS_NT5:
{
SpyHookInitializeEx (apbSdtSymbolsNT5, apbSdtFormats);
break;
}
default:
{
SpyHookInitializeEx (NULL, NULL);
fOk = FALSE;
break;
}
}
return fOk;
}
列表5-5. SpyHookInitialize()选择匹配当前OS版本的符号表
将全局数组:apbSdtSymbolsNT4[]和apbSdtSymbolsNT5[]传递给SpyHookInitializeEx()函数作为其第一个参数ppbSymbols,这两个数组只是简单的字符串数组,包含Windows NT 4.0和Windows 2000的所有Native API函数的名称,按照它们在KiServiceTable中的索引顺序来存储,最后以NULL结束。列表5-6给出了apbStdFormats[]字符串数组。这个格式字符串列表也是hook机制中很重要的一部分,因为它确定了记录了那个Native API调用,以及每个记录项的格式。显然,这些字符串的结构借鉴了C运行时库中的printf()函数,但针对Native API经常使用的数据类型进行了修改。表5-2列出了所有可被API Logger识别的格式化ID。
PBYTE apbSdtFormats [] =
{
"%s=NtCancelIoFile(%!,%i)",
"%s=NtClose(%-)",
"%s=NtCreateFile(%+,%n,%o,%i,%l,%n,%n,%n,%n,%p,%n)",
"%s=NtCreateKey(%+,%n,%o,%n,%u,%n,%d)",
"%s=NtDeleteFile(%o)",
"%s=NtDeleteKey(%-)",
"%s=NtDeleteValueKey(%!,%u)",
"%s=NtDeviceIoControlFile(%!,%p,%p,%p,%i,%n,%p,%n,%p,%n)",
"%s=NtEnumerateKey(%!,%n,%n,%p,%n,%d)",
"%s=NtEnumerateValueKey(%!,%n,%n,%p,%n,%d)",
"%s=NtFlushBuffersFile(%!,%i)",
"%s=NtFlushKey(%!)",
"%s=NtFsControlFile(%!,%p,%p,%p,%i,%n,%p,%n,%p,%n)",
"%s=NtLoadKey(%o,%o)",
"%s=NtLoadKey2(%o,%o,%n)",
"%s=NtNotifyChangeKey(%!,%p,%p,%p,%i,%n,%b,%p,%n,%b)",
"%s=NtNotifyChangeMultipleKeys(%!,%n,%o,%p,%p,%p,%i,%n,%b,%p,%n,%b)",
"%s=NtOpenFile(%+,%n,%o,%i,%n,%n)",
"%s=NtOpenKey(%+,%n,%o)",
"%s=NtOpenProcess(%+,%n,%o,%c)",
"%s=NtOpenThread(%+,%n,%o,%c)",
"%s=NtQueryDirectoryFile(%!,%p,%p,%p,%i,%p,%n,%n,%b,%u,%b)",
"%s=NtQueryInformationFile(%!,%i,%p,%n,%n)",
"%s=NtQueryInformationProcess(%!,%n,%p,%n,%d)",
"%s=NtQueryInformationThread(%!,%n,%p,%n,%d)",
"%s=NtQueryKey(%!,%n,%p,%n,%d)",
"%s=NtQueryMultipleValueKey(%!,%p,%n,%p,%d,%d)",
"%s=NtQueryOpenSubKeys(%o,%d)",
"%s=NtQuerySystemInformation(%n,%p,%n,%d)",
"%s=NtQuerySystemTime(%l)",
"%s=NtQueryValueKey(%!,%u,%n,%p,%n,%d)",
"%s=NtQueryVolumeInformationFile(%!,%i,%p,%n,%n)",
"%s=NtReadFile(%!,%p,%p,%p,%i,%p,%n,%l,%d)",
"%s=NtReplaceKey(%o,%!,%o)",
"%s=NtSetInformationKey(%!,%n,%p,%n)",
"%s=NtSetInformationFile(%!,%i,%p,%n,%n)",
"%s=NtSetInformationProcess(%!,%n,%p,%n)",
"%s=NtSetInformationThread(%!,%n,%p,%n)",
"%s=NtSetSystemInformation(%n,%p,%n)",
"%s=NtSetSystemTime(%l,%l)",
"%s=NtSetValueKey(%!,%u,%n,%n,%p,%n)",
"%s=NtSetVolumeInformationFile(%!,%i,%p,%n,%n)",
"%s=NtUnloadKey(%o)",
"%s=NtWriteFile(%!,%p,%p,%p,%i,%p,%n,%l,%d)",
NULL
};
列表5-6. Native API Logger使用的格式化字符串
这里要特别提出的是:每个格式字符串要求必须提供函数名的正确拼写。SpyHookInitializeEx()遍历它接受到的Native API符号列表(通过ppbSymbols参数),并试图从ppbFormats列表中找出与函数名匹配的格式字符串。由帮助函数SpySearchFormat()来进行比较工作,列表5-3底部的if语句中调用了该函数。因为要执行大量的字符串查找操作,我使用了一个高度优化的查找引擎,该引擎基于“Shift/And”搜索算法。如果你想更多的学习它的实现方式,请察看随书CD的\src\w2k_spy\w2k_spy.c源文件中的SpySearch*()函数。当SpyHookInitializeEx()推出循环后,aSpyHooks[]中的所有Handler成员都将指向适当的Hook进入点,pbFormat成员提供与之匹配的格式字符串。对于Windows NT 4.0,所有索引值在0xD3---0xF8的数组成员都将被设为NULL,因为在NT4中,它们并没有被定义。
表5-2. 可识别的格式控制ID
ID
名 称
描 述
%+
句柄(登记)
将句柄和对象名写入日志,并将其加入句柄表。
%!
句柄(检索)
将句柄写入日志,并从句柄表中检索其对应的对象名。
%-
句柄(撤销登记)
将句柄和对象名写入日志,并将其从句柄表移除
%a
ANSI字符串
将一个由8位ANSI字符构成的字符串写入日志
%b
BOOLEAN
将一个8位的逻辑值写入日志
%c
CLIENT_ID*
将CLIENT_ID结构的成员写入日志
%d
DWORD *
将该DWORD所指变量的值写入日志
%i
IO_STATUS_BLOCK *
将IO_STATUS_BLOCK结构的成员写入日志
%l
LARGE_INTEGER *
将一个LARGE_INTEGER的值写入日志
%n
数值(DWORD)
将一个32位无符号数写入日志
%o
OBJECT_ATTRIBUTES *
将对象的ObjectName写入日志
%p
指针
将指针的目标地址写入日志
%s
状态(NTSTATUS)
将NT状态代码写入日志
%u
UNICODE_STRING *
将UNICOD_STRING结构的Buffer成员写入日志
%w
宽字符串
将一个由16位字符构成的字符串写入日志
%%
百分号转义符
将一个“%”号写入日志
……………..待续…………….