《Undocumented Windows 2000 Secrets》翻譯 --- 第二章(2)
第二章 The Windows 2000 Native API
翻譯: Kendiv
更新: Friday, February 04, 2005
Windows 2000 運行時庫
Nt*() 和 Zw*() 函數構成了 Native API 的基本部分,但并不是主要部分,還有一部分代碼位于 ntdll.dll 中。該 DLL 至少導出了 1179 個符號。其中的 249 和 248 個分別屬于 Nt*() 和 Zw*() 函數集,剩余的 682 個函數并不通過 INT 2eh 中斷進行調用。顯然,這一大組函數并不依賴 Windows 2000 內核。那提供它們的目的何在呢?讓我們繼續往下看。
C 運行時庫
如果你研究過位于 ntdll.dll 導出節( export section )的符號,你會發現很多在 C 程序員看來很熟悉的小寫的函數名稱。這些都是眾所周知的名子,如 memcpy() 、 sprintf() 和 qsort() ,這些 C 運行時庫中的函數都合并到了 ntdll.dll 中。對于 ntoskrnl.exe 也是如此,它同樣提供了一組與 C 運行時函數十分相像的函數,雖然這兩組函數并不相同。 附錄 B 的 表 B-3 列出了這兩組函數,并指出了每個函數分別屬于哪個模塊。
你可以簡單的將 ntdll.lib (來自 Windows 2000 DDK )添加到導入庫列表(鏈接器在解析符號期間將掃描該列表)中,就可以鏈接到這些函數。如果你更喜歡對話框,你可以選擇 Visual C/C++ 的工程菜單中的 Settings 子菜單,然后單擊 Linke 頁,選擇 Category General ,然后將 ntdll.dll 添加到 Object/Library 模塊列表中。還有一種方法:在源文件中,添加如下的內容:
#pragma comment(linker,”/defaultlib:ntdll.lib”)
這同樣有效,好處是,其他開發人員可以使用 Visual C/C++ 的默認設置來 rebuild 你的工程。
反編譯這些與 C 運行時函數類似的函數(來自 ntdll.dll 和 ntoskrnl.exe ),會發現 ntdll.dll 并不依賴于 ntoskrnl.exe ,這和 ndll.dll 中的 Native API 不一樣。事實上,這兩個模塊分別實現了這些函數。本節出現的其他函數也是如此。注意,表 B-3 中的一些函數并不使用其導出的名稱。例如,如果在內核模式的驅動程序中針對一個 64 位的 LARGE_INTEGER 使用移位操作符 << 和 >> ,編譯器和鏈接器會自動導入 ntoskrnl.exe 的 _allshr() 和 _allshl() 。
擴展的運行時函數
隨同標準的 C 運行時函數, Windows 2000 還提供了一組擴展的運行時函數。在次強調, ntdll.dll 和 ntoskrnl.exe 分別實現了它們。并且其中有些函數是重疊的。這些擴展函數的名字都有一個共同的前綴 Rtl ( for Runtime Library )。 附錄 B 的 表 B-4 列出了所有這些擴展函數。 Windows 2000 提供的這些運行時函數還包含用于普通任務的助手函數( helper function ),這些任務都超過了 C 運行時函數的能力范圍。例如,其中的某些用于管理安全性,另一些用于操作 Windows 2000 特有的數據結構,還有一些對內存管理提供支持。很難理解為什么微軟僅在 Windows 2000 DDK 中提供了其中 115 個函數的文檔,而扔掉了其余 406 個非常有用的函數。
浮點模擬器( The Floating-Point Emulator )
讓我用 ntdll.dll 提供的另一組函數集合來結束這次 API 函數匯展。 表 2-1 列出了這些函數的名稱,這些名稱可能對于匯編程序員有些眼熟。去了名稱前的 __e 前綴,你就會得到 i386 系列 CPU 中的 FPU ( Floating-Point Unit )匯編助記符。事實上,從 表 2-1 中列出的函數來看, ntdll.dll 包含了一個完整的浮點模擬器。這再次證明了這個 DLL 是一個龐大的代碼倉庫,這吸引了眾多的 System Spelunker 去反編譯它。
表 2-1. ntdll.dll 的浮點模擬器接口
函數名稱
_eCommonExceptions
_eFIST32
_eFLD64
_eFSTP32
_eEnulatorInit
_eFISTP16
_eFLD80
_eFSTp64
_eF2XM1
_eFISTP32
_eFLDCW
_eFSTP80
_eFABS
_eFISTP64
_eFLDENV
_eFSTSW
_eFADD32
_eFISUB16
_eFLDL2E
_eFSUB32
_eFADD64
_eFISUB32
_eFLDLN2
_eFSUB64
_eFADDPreg
_eFISUBR16
_eFLDPI
_eFSUBPreg
_eFADDreg
_eFISUBR32
_eFLDZ
_eFSUBR32
_eFADDtop
_eFLDI
_eFMUL32
_eFSUBR64
_eFCHS
_eFIDIVR16
_eFMUL64
_eFSUBreg
_eFCOM
_eFIDIVR32
_eFMULPreg
_eFSUBRPreg
_eFCOM32
_eFILD16
_eFMULreg
_eFSUBRreg
_eCOM64
_eFILD32
_eFMULtop
_eFSUBRtop
_eFCOMP
_eFILD64
_eFPATAN
_eFSUBtop
_eFCOMP32
_eFIMUL16
_eFPREm
_eFTST
_eFCOMP64
_eFIMUL32
_eFPREM1
_eFUCOM
_eFCOMPP
_eFINCSTP
_eFPTAN
_eFUCOMP
_eFCOS
_eFINIT
_eFRNDINT
_eFUCOMPP
_eFDECSTP
_eFIST16
_eFRSTOR
_eFXAM
_eFIDIVR16
_eFIST32
_eFSAVE
_eFXCH
_eFIDIVR32
_eFISTP16
_eFSCALE
_eFXTRACT
_eFILD16
_eFISTP32
_eFSIN
_eFYL2X
_eFILD32
_eFISTP64
_eFSQRT
_eFYL2XP1
_eFILD64
_eFISUB16
_eFST
_eGetStatusWord
_eFIMUL16
_eFISUB32
_eFST32
NPXEMULATORTABLE
_eFIMUL32
_eFISUBR16
_eFST64
RestoreEm87Context
_eFINCSTP
_eFISUBR32
_eFSTCW
SaveEm87Context
_eFINIT
_eFLD16
_eFSTENV
_eFIST16
_eFLD32
_eFSTP
有關浮點指令集的更多信息,請參考 Intel 80386 CPU 的原始文檔。可以從 Intel 官方網站: http://developer.intel.com/design/pentium/manuals/ 來下載 PDF 格式的 Pentium 手冊。講解這些機器碼指令集的手冊是: Intel Architecture SoftWare Developer's Manual . Volume 2 : Instruction Set Reference ( Intel 1999b )。
其它的 API 函數
除 附錄 B 和 表 2-1 列出的函數外, ntdll.dll 和 ntoskrnl.exe 還為多個內核組件導出了為數眾多的函數。為了避免更長的表格,我這里僅列出可用函數的名稱前綴及其所屬類別( 表 2-2 )。
表 2-2 函數名前綴及其所屬分類
前綴
ntdll.dll
ntoskrnl.exe
分類
_e
N/A
浮點模擬器
Cc
N/A
Cache 管理器
Csr
ClIEnt-Server 運行時庫
Dbg
N/A
調試支持
Ex
N/A
執行支持( Executive Support )
FsRtl
N/A
文件系統運行時庫
Hal
N/A
硬件抽象層調度器
Inbv
N/A
系統初始化 /VGA 啟動驅動( bootvid.dll )
Init
N/A
系統初始化
Interlocked
N/A
處理線程安全的變量
Io
N/A
I/O 管理器
Kd
N/A
內核調試支持
Ke
N/A
內核例程
Ki
內核中斷例程
Ldr
映像加載器
Lpc
N/A
本地過程調用( LPC )設備
Lsa
N/A
本地安全授權
Mm
N/A
內存管理器
Nls
National Language Support (NLS)
Nt
NT Native API
Ob
N/A
對象管理器
Pfx
前綴處理
Po
N/A
電源管理器
Ps
N/A
進程支持
READ_REGISTER_
N/A
從寄存器地址中讀取
Rtl
Windows 2000 運行時庫
Se
N/A
安全處理
WRITE_REGISTER_
N/A
向寄存器地址中寫入
Zw
另一組 Native API
<other>
幫助函數和 C 運行時庫
很多內核函數都使用統一的命名規則 ----PrefixOperationObject() 。例如, NtQueryInformationFile() 函數屬于 Native API ,這是因為其 Nt 前綴,而且該函數顯然針對一個文件對象執行了 QueryInformation 操作。但并不是所有函數都遵循這一規則,不過絕大多數都是如此。因此,可以很容易的通過函數的名稱猜測其功能。
經常使用的數據類型
當編寫與 Windows 2000 內核有關的軟件時 --- 不管是和用戶模式的 ntdll.dll 還是和內核模式的 ntoskrnl.exe ,你都必須處理幾個基本的數據類型,而這些數據類型在 Win32 世界里非常少見。它們中的多數都會在本書中反復出現。下面的章節將介紹使用頻率最高的數據類型。
整型
一般說來,整數類型有多個不同的變體。 Win32 SDK 的頭文件和 SDK 文檔使用了其專有的術語,這些術語很容易和 C/C++ 的基本類型以及一些派生類型相混淆。 表 2-3 列出了這些整數類型,以及它們之間的等價關系。在“ MASM ”列中,給出了微軟宏匯編語言( MASM )使用的類型名稱。 Win32 SDK 為 C/C++ 的基本數據類型定義了對應的 BYTE 、 WORD 、 DWORD 別名。“別名 1 ”和“別名 2 ”兩列包含其經常使用的別名。例如, WCHAR 代表基礎的 Unicode 字符類型。最后一列“有符號的”,列出了對應的有符號類型的常見別名。一定要記住 ANSI 字符類型 CHAR 是有符號的,而 Unicode 類型 WCHAR 是無符號的。當編譯器將表達式或計算中的這些類型轉換為整數類型時,這種不一致性將導致意外的錯誤。
表 2-3 最后一行的 MASM 的 TBYTE 類型(讀做“ 10-byte ”)是一個 80 位的浮點數,用于高精度的浮點運算操作。 Microsoft Visual C/C++ 沒有為 Win32 程序員提供對應的數據類型。需要注意的是, MASM 的 TBYTE 和 Win32 的 TBYTE (讀做“ text byte ”)沒有任何關系,后者只是一個用于轉換的宏,根據源文件中是否有 #define UNICODE 而分別對應 CHAR 或 WCHAR 。
表 2-3. 等價的整數類型
位數
MASM
基本類型
別名 1
別名 2
有符號的
8
BTYE
unsigned char
UCHAR
CHAR
16
WORD
unsigned short
USHORT
WCHAR
SHORT
32
DWORD
unsigned long
ULONG
LONG
32
DWORD
unsigned int
UINT
INT
64
QWORD
unsigned __int64
ULONGLONG
DWORDLONG
LONGLONG
80
TBYTE
N/A
由于在 32 位編程環境中較難處理 64 位整數, Windows 2000 通常不提供 64 位的基本類型,如 __int64 或其派生類型。替代的, DDK 頭文件 ntdef.h 中定義了一個精巧的 union 結構,可以將一個 64 位數解釋為一對 32 位數或一個完整的 64 位數,參見 列表 2-3 給出了 LARGE_INTEGER 和 ULARGE_INTEGER 類型定義。該類型可分別表示有符號和無符號的整數。通過使用 LONGLONG/ULONGLONG (針對 64 位的 QuadPart 成員)或者 LONG/ULONG (針對 32 位的 HighPart 成員)來控制有無符號。
typedef union _LARGE_INTEGER
{
struct
{
ULONG LowPart;
LONG HighPart;
}
LONGLONG QuadPart;
} LARGE_INTEGER,*PLARGE_INTEGER;
typedef union _ULARGE_INTEGER
{
struct
{
ULONG LowPart;
ULONG HighPat;
}
ULONGLONG QuadPat;
} ULARGE_INTEGER,*PULARGE_INTEGER;
列表 2-3. LARGE_INTEGER 和 ULARGE_INTEGER
字符串
在 Win32 程序設計中,常使用 PSTR 和 PWSTR 來分別代替 ANSI 和 Unicode 字符串。 PSTR 被定義為 CHAR* , PWSTR 則定義為 WCHAR* (參見表 2-3 )。通過源文件中是否出現 #define UNICODE 指示符,附加的 PTSTR 類型分別對應 PSTR 或 PWSTR ,這樣就可通過單一的源文件來維護應用程序的 ANSI 和 Unicode 版本。基本上,這些字符串都是簡單的指向以零結尾的 CHAR 或 WCHAR 類型的數組。如果你常和 Windows 2000 內核打交道,你將必須處理一種很不同的字符串表示法。最常見的類型是 UNICODE_STRING ,這是一個第三方類型, 列表 2-4 給出了它的定義。
typedef struct _UNICODE_STRING
{
USHORT Length;
USHORT MaximumLength;
PWSTR Buffer;
} UNICODE_STRING,*PUNICODE_STRING;
typedef struct _STRING
{
USHORT Length;
USHORT MaximumLength;
PCHAR Buffer;
} STRING, *PSTRING;
typedef STRING ANSI_STRING, *PANSI_STRING;
typedef STRING OEM_STRING, *POEM_STRING;
列表 2-4. 字符串類型
Length 成員給出了當前字符串的字節數(注意,不是字符個數), MaximumLength 成員指出 Buffer 所指向內存塊的大小,實際的字符串數據將保存在該內存塊中。注意, MaximumLength 也是字節數。由于 Unicode 字符寬度為 16 位,所有其長度總是字符個數的兩倍。通常, Buffer 指向的字符串都是以零結尾的。然而,有些內核模塊可能僅依賴字符串的長度值,而不考慮結尾的 0 字符,這種情況下要小心處理。
Windows 2000 的 ANSI 字符串叫做 STRING ,如 列表 2-4 中所示。為了方便, nedef.h 分別定義了 ANSI_STRING 和 OEM_STRING 來代表使用不同代碼頁的 8 位字符串( ANSI 默認代碼頁為 1252 ; OEM 默認代碼頁為 437 )。不過, Windows 2000 內核使用的主要字符串類型還是 UNICODE_STRING 。你可能偶爾會碰到 8 位字符串。
在 圖 2-3 中,我給出了兩個典型的 UNICODE_STRING 示例。左面的那個包含兩個獨立的內存塊:一個 UNICODE_STRING 結構和一個 16 位 PWCHAR 類型的 Unicode 字符數組。這或許是在 Windows 2000 數據類型中最常見的字符串類型。右邊的是一種頻繁出現的特殊類型,在此種類型中, UNICODE_STRING 和 PWCHAR 數組位于同一個內存塊中。有些內核函數,包括 Native API 內部使用的一些函數,都在連續的內存塊中保存其返回的結構化的系統信息。如果數據中包含字符串,它們通常都存儲在嵌入式的 UNICODE_STRING 中,如 圖 2-3 右面所示。例如, NtQuerySystemInformation() 函數就頻繁使用了這種特殊的字符串類型。
這些字符串結構不許要手工維護, ntdll.dll 和 ntoskrnl.exe 導出了一組豐富的運行時 API 函數,如 RtlCreateUnicodeString() 、 RtlInitUnicodeString() 、 RtlCopyUnicodeString() 等。通常, STRING 和 ANSI_STRING 也有對應的等價函數。這些函數中的大多數在 DDK 中都有文檔記錄,但其中有些沒有。不過,很容易猜出這些未文檔化的字符串函數的功能及其需要的參數。使用 UNICODE_STRING 、 STRING 的好處是,可以隱示的指定 Buffer 可容納的字符串的大小。如果你給一個函數傳遞了一個 UNICODE_STRING 類型的字符串,而該函數需要適當改變該字符串的值,而這可能會增加該字符串的長度,那這個函數只需要簡單的檢查 MaximumLength 成員就可確定是否有足夠的空間來存放結果。
結構體
個別的幾個內核 API 函數期望其處理的對象有一個合適的 OBJECT_ATTRIBUTES 結構, 列表 2-5 給出了該結構的定義。例如, NtOpenFile() 函數沒有 PWSTR 或 PUNICODE_STRING 參數用來指定要打開的文件的路徑。替代的, OBJECT_ATTRIBUTES 結構中的 ObjectName 成員給出了該路徑。通常,設置該結構很容易。除 ObjectName 外,還需要設置 Length 和 Attributes 成員。 Length 必須設置為: sizeof(OBJECT_ATTRIBUTES) , Attributes 是一組來自 ntdef.h 的 OBJ_* 常量。例如,如果你對象名稱不區分大小寫的話, Attributes 應設置為 OBJ_CASE_INSENSITIVE 。當然, ObjectName 成員是一個 UNICODE_STRING 指針,并不是通常的 PWSTR 。剩余的成員只要不使用,都可設置為 NULL 。
typedef struct _OBJECT_ATTRIBUTES
{
ULONG Length;
HANDLE RootDirectory;
PUNICODE_STRING ObjectName;
ULONG Attributes;
PVOID SecurityDescriptor;
PVOID SecurityQualityOfService;
} OBJECT_ATTRIBUTES, *POBJECT_ATTRIBUTES;
列表 2-5. OBJECT_ATTRIBUTES 結構
OBJECT_ATTRIBUTES 結構僅描述函數使用的數據的細節, 列表 2-6 給出的 IO_STATUS_BLOCK 結構則用于記錄對用戶所提交的操作的處理結果。該結構很簡單 ---Staus 成員存放一個 NTSTATUS 類型的代碼,其值可能是 STATUS_SUCCESS 或定義于 ntstatus.h 中的所有可能的錯誤代碼。 Information 成員在操作成功的情況下,提供與操作相關的附加數據。比如,如果函數返回一個數據塊,該成員將被設置為該數據塊的大小。
typedef struct _IO_STRATUS_BLOCK
{
NTSTATUS Status;
ULONG Information;
} IO_STATUS_BLOCK, *PIO_STATUS_BLOCK;
列表 2-6. IO_STATUS_BLOCK 結構
另一個常見的 Windows 2000 數據類型是 LIST_ENTRY 結構,列表 2-7 給出了該結構的定義。內核使用該結構將所有對象維護在一個雙向鏈表中。一個對象分屬多個鏈表是很常見的, Flink 成員是一個向前鏈接,指向下一個 LIST_ENTRY 結構, Blink 成員則是一個向后鏈接,指向前一個 LIST_ENTRY 結構。通常情況下,這些鏈表都成環形,也就是說,最后一個 Flink 指向鏈表中的第一個 LIST_ENTRY 結構,而第一個 Blink 指向最后一個。這樣就很容易雙向遍歷該鏈表。如果一個程序要遍歷整個鏈表,它需要保存第一個 LIST_ENTRY 結構的地址,以判斷是否已遍歷了整個鏈表。如果鏈表僅包含一個 LIST_ENTRY 結構,那么該 LIST_ENTRY 結構必須引用其自身,也就是說, Flink 和 Blink 都指向其自己。
typedef struct _LIST_ENTRY
{
struct _LIST_ENTRY *Flink;
struct _LIST_ENTRY *Blink;
} LIST_ENTRY, *PLIST_ENTRY;
列表 2-7. LIST_ENTRY 結構
圖 2-4 展示了對象鏈表各成員間的關系。對象 A1 、 A2 、 A3 屬于同一鏈表。注意, A3 的 Flink 指向 A1 , A1 的 Blink 指向 A3 。最右邊的對象 B1 僅有一個成員,因此,其 Flink 和 Blink 都指向相同的地址 --- 即對象 B1 的地址。典型的雙向鏈表的例子是進程和線程鏈表。內部變量 PsActiveProcessHead 就是一個 LIST_ENTRY 結構,位于 ntoskrnl.exe 的 .data 節中。該變量指向系統進程列表的首部(通過其 Blink 指針)。你可以在內核調試器中使用 dd PsActiveProcessHead 來獲取該鏈表的首部,然后通過其 Flink 和 Blink 指針遍歷整個鏈表(仍使用 dd 命令)。當然,這種探測 Windows 進程的方法非常繁瑣,但這可使你深入的觀察基本的系統結構。 Windows 2000 Native API 提供了更便利的方法來枚舉進程,如 NtQuerySystemInformation() 函數。
typedef struct _CLIENT_ID
{
HANDLE UniqueProcess;
HANDLE UniqueThread;
} CLIENT_ID, *PCLIENT_ID;
列表 2-8. CLIENT_ID 結構
處理進程和線程的 API 函數,如: NtOpenProcess() 和 NtOpenThread() ,使用 列表 2-8 給出的 CLIENT_ID 結構來和特定的進程、線程相關聯。盡管其類型為 HANDLE ,實際上,從嚴格的意義上來講 UniqueProcess 和 UniqueThread 成員并不是句柄( Handle ),它們都是整數型的進程 ID 和線程 ID 。即標準 Win32 函數 GetCurrentProcessId() 和 GetCurrentThreadId() 返回的 DWORD 類型的數值。
Windows 2000 執行體( Executive )還使用 CLIENT_ID 結構在全局范圍內標識唯一的線程。例如,如果你使用內核調試器的 !thread 命令來顯示當前線程參數,就會在輸出的第一行看到類似“ Cid ppp.ttt ”的顯示,其中“ ppp ”就是 CLIENT_ID 的 UniqueProcess 成員,而“ ttt ”則代表 UniqueThread ,如下所示。注意,我用黑體標出的地方。
kd> !thread
THREAD 83a51ba8 Cid 0a5c.0e64 Teb: 7ffdd000 Win32Thread: e14f4eb0 RUNNING on processor 0
Not impersonating
DeviceMap e20fb208
Owning Process 83a14708
Wait Start TickCount 906512 Elapsed Ticks: 68570
Context Switch Count 266 LargeStack
UserTime 00:00:00.0312
KernelTime 00:00:00.0015
。。。。。。。。。。。。。。。。。。。
Native API 的接口
對于內核模式的驅動程序,使用 Native API 的接口非常平常,就像在用戶模式下的程序中調用 Win32 API 一樣。 Windows 2000 DDK 提供的頭文件和庫包含了所有在調用 ntoskrnl.exe 導出的 Native API 時所需的信息。而另一方面, Win32 SDK 幾乎不支持在程序中調用 ntdll.dll 導出的 Native API 。我說“幾乎不”是因為 Win32 SDK 實際上提供了一個重要的東西:導入庫 ntdll.lib ,該文件位于 Program FilesMicrosoft Platfrom SDKLib 目錄中。如果沒有這個庫,將很難調用 ntdll.dll 導出的函數。
譯注:
你需要安裝 Windows 2000 DDK 才能獲得 ntdll.lib
可以到 http://www.microsoft.com/msdownload/platformsdk/sdkupdate/ 下載最新的 SDK
將 NTDLL.DLL 導入庫添加到工程中
在你能成功的編譯和鏈接在用戶模式下使用 ntdll.dll 導出函數的代碼之前,你必須考慮如下的四個重點:
1. SDK 的頭文件中,沒有包含這些函數的原型。
2. SDK 文件中缺少這些函數使用的幾個基本的數據類型。
3. SDK 和 DDK 頭文件并不兼容,你不能將 #include <ntddk.h> 加入你的 Win32 C 源代碼文件中。
4. ntdll.lib 并沒有加入 Visual C/C++ 默認的導入庫列表中
最后一個問題很容易解決,只需要編輯工程的設置屬性,或者將如下內容加入你的源代碼中, #pragma comment(linker,”defaultlib:ntdll.lib”) ,像在前面的 Windows 2000 運行時庫一節解釋的那樣,這會在編譯時,將 ntdll.dll 加入鏈接器的 /defaultlib 設置中。解決缺失的定義比較困難。因為不可能將 SDK 和 DDK 頭文件整合到 C 程序中,最簡易的解決方法是寫一格自定義的頭文件,在該頭文件中包含所有調用 ntdll.dll 導出函數必須的定義。幸運的是,你不需要開始這項工作了,在本書光盤的 srccommoninclude 目錄下的 w2k_def.h 文件包含了你所需要的所有基本信息。該頭文件將在第六、七兩章中扮演重要角色。因為它被設計為可同時兼容用戶模式和內核模式的工程,在用戶模式代碼中,你必須在 #include <w2k_def.h> 之前插入 #define _USER_MODE_ ,以加入僅出現在 DDK 中的一些定義。
有關 Native API 編程的很多詳細信息都已經出版,目前看來,針對 Windows 2000 平臺的好書是 Gary Nebbett's 的《 Windows NT/2000 Native API Reference 》。該書提供的示例程序較少,但它覆蓋了 Windows NT/2000 平臺上的所有 Native API ,還包括這些函數需要的數據結構定義以及其他必須的一些結構定義。
將在第六章介紹的 w2k_call.dll 示例庫,演示了 w2k_def.h 的典型用法。第六章還將討論另一種在用戶模式進入 Windows 2000 內核的方法,此種方法不受限于 Native API 。事實上,這種技巧也可用于 ntoskrnl.exe ,對于所有加載到內核空間的模塊,只要它們導出了函數或者可以和 .dbg 或 .pdb 符號文件相匹配都可以使用此方法。如你所見,在本書剩余章節中還有很多有趣的信息。但是,在我們到達那兒之前,我們會繼續討論一些基本的概念和技術。
