您的位置:首页技术文章
文章详情页

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

【字号: 日期:2023-08-27 15:52:48浏览:34作者:猪猪

第二章 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 符号文件相匹配都可以使用此方法。如你所见,在本书剩余章节中还有很多有趣的信息。但是,在我们到达那儿之前,我们会继续讨论一些基本的概念和技术。

标签: Windows系统