《Undocumented Windows 2000 Secrets》翻译 --- 第五章(2)
第五章 监控 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 的 srcw2k_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 的 srcw2k_spyw2k_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 位字符构成的字符串写入日志
%%
百分号转义符
将一个“ % ”号写入日志