《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 被設為每個進入點的大?。ㄓ蛇M入點數組的大小除以進入點的個數而得出)。當然,這個值將是 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 位字符構成的字符串寫入日志
%%
百分號轉義符
將一個“ % ”號寫入日志
