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

第五章  监控Native API调用

翻译:Kendiv( [email protected] )

更新: Thursday, February 24, 2005

 

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

 

汇编语言的救援行动

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

 

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

l         C函数调用者假定CPU寄存器EBPEBXESIEDI已经被保存了。

 

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

 

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

 

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

 

l         32位的函数结果(INTLONGDWORD等)由寄存器EAX返回。

 

l         64位的函数结果(__int64LONGLONGDWORDLONG等)由寄存器对EDXEAX返回。寄存器EAX包含031位,EDX保存3263位。

 

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

 

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

 

l         __fastcall修饰的函数声明,则希望前两个参数位于CPU寄存器ECXEDX中。如果还需要更多的参数,它们将按照相反的顺序传入堆栈,最后由被调用者清理堆栈,这和__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给出了此代码,这通常被称为函数的“序言”和“尾声”。有些编译器采用更简洁的i386ENTERLEAVE操作符,在“序言被执行后,堆栈将如5-3所示。EBP寄存器作为一分割点将函数的参数堆栈划分为两部分:(1)局部存储区域,该区域中包含所有定义于函数范围内的局部变量(2)调用者堆栈,其中保存有EBP的备份和返回地址。注意,微软的Visual C/C++的最新版中默认不使用堆栈帧。替代的是,代码通过ESP寄存器访问堆栈中的值,不过这需要指定变量相对于当前栈顶的偏移量。这种类型的代码非常难以阅读,因为每个PUSHPOP指令都会影响ESP的值和所有参数的偏移量。在此种情况下不再需要EBP,它将作为一个附加的通用寄存器。

 

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

 

《Undocumented Windows 2000 Secrets》翻译 --- 第五章(2)_第1张图片

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

 

 

Hook分派程序(Hook Dispatcher)

这部分的代码将较难理解。编写它们花费了我很多时间,而且在这一过程中我还欣赏了无数的蓝屏。我最初的方法是提供一个完全用汇编语言编写的模块。不过,这个方法在链接阶时带来了很大的麻烦,因此,我改为在C模块中使用嵌入式汇编。为了避免创建另一个内核模式的驱动程序,我决定将hook代码整合到Spy设备驱动程序中。还记得在4-2底部列出的形如SPY_IO_HOOK_*IOCTL函数吗?现在我们将和它们来一次亲密接触。后面的示列代码来自w2k_spy.cw2k_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()函数中,这个宏被重复了多大2480xF8)次,这正好是Windows 2000 Native API函数的数目。在列表5-3的顶部,这个数目被定义为SDT_SYMBOLS_MAX常量,该宏可以使SDT_SYMBOLS_NT4SDT_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指令的返回地址是其下一个语句的地址。通常,第NCALL EAX指令的返回地址是第N+1个语句的地址,但最后一个除外,最后这个将返回SpyHook2。因此,从0开始的所有进入点的索引可以由5-4中的通用公式计算出来。这三条规则中的潜在规则是:SDT_SYMBOLS_MAX进入点符合内存块SpyHook2---SpyHook1。那么有多少个进入点符合ReturnAddress---SpyHook1呢?因为计算结果是位于0SDT_SYMBOLS_MAX中的某一个数值,所以,肯定要使用该数值来获取一个从0开始的索引。

 

5-4.  通过Hook进入点的返回地址确定一个Hook进入点

 

5-4所示公式的实现方式可以在列表5-3中找到,在汇编标签SpyHook2的右边。在5-5的左下角也给出了该公式的实现代码,它展示了Hook Dispatcher机制的基本原理。注意,i386mul指令会在EDX:EAX寄存器中产生一个64位的结果值,这正是其后的div指令所期望的,因此,这里没有整数溢出的危险。在5-5的左上角,是对KiServiceTable的描述,该表将被SpyHook宏生成的进入点地址修改。在图的中部展示了展开后的宏代码(来自列表5-4中)。进入点的线性地址位于图的右手边。为了完全一致,每个进入点的大小都是8字节,因此,通过将KiServiceTable中每个函数的索引值乘以8,然后再将乘积加上SpyHook1的地址就可得出进入点的地址。

 

事实上,每个进入点并不都是纯粹的8字节长。我花费了大量的时间来寻找最佳的hook函数的实现方式。尽管按照32位边界对齐代码并不是必须的,但这从来都不是个坏主意,因为这会提高性能。当然,能提升的性能十分有限。你或许会奇怪:为什么我要通过EAX寄存器间接的调用SpyHook2,而不是直接使用CALL SpyHook2指令,这不是更高效吗?是的!不过,问题是i386CALL(还有jmp)指令可以有多种实现方式,而且都具有相同的效果,但是产生的指令大小却不相同。请参考:Intel’s Instruction Set Reference of the Pentium CPU familyIntel 199c)。因为最终的实现方式要由编译器/汇编器来确定,这不能保证所有的进入点都会有相同的编码。换句话说,MOV  EAX和一个32位常量操作数总是以相同的方式编码,同样的,这也适用于CALL EAX指令。

 

5-5.   Hook Dispatcher的功能原理

 

列表5-3中还有一点需要澄清。让我们从SpyHook9标签后的最后一快C代码段开始。紧随SpyHook9之后的汇编代码将SpyHook1SpyHook2的线性地址保存在dHook1dHook2变量中。接下来,变量n被设为每个进入点的大小(由进入点数组的大小除以进入点的个数而得出)。当然,这个值将是8列表5-3的剩余部分是一个循环语句,用来初始化全局数组aSpyHooks[]中的所有项。这个数组所包含的SPY_HOOK_ENTRY结构定义于列5-3的顶部,该数组中的每一项都对应一个Native API函数。要理解该结构中的HandlerpbFormat成员是如何被设置的,就必须进一步了解传递给SpyHookInitializeEx()ppbSymbolsppbFormats参数,列表5-5给出了外包函数SpyHookInitialize(),该函数会选择适合当前OS版本的参数来调用SpyHookInitializeEx()。前面已经提示过,我使用的代码不直接测试OS版本或Build Number,而是用常量SPY_SYMBOLS_NT4SPY_SYMBOLS_NT5SDT中与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.0Windows 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字符串

将一个由8ANSI字符构成的字符串写入日志

%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位字符构成的字符串写入日志

%%

百分号转义符

将一个“%”号写入日志

 

……………..待续…………….

你可能感兴趣的:(《Undocumented Windows 2000 Secrets》翻译 --- 第五章(2))