第2章:本机API
gushaow@mails.gscas.ac.cn
除了Win32 API,NT平台开放了另一个基本接口就是本机API。内核模式驱动和文件系统驱动的开发人员可能已经熟悉了本机API,因为内核模式模块位于更低的系统级别,在那个级别上环境子系统是不可见的。尽管如此,并不需要驱动级别去访问这个接口,普通的Win32程序可以在任何时候向下调用本机API。并没有任何技术上的限制,只不过微软不支持这种应用开发方法。
User32.dll,advapi32.dll,gdi32.dll,rpcrt4.dll,kernel32.dll代表了Win32 API的基本提供者,当然,还有其它的dll,比如version.dll,shell32.dll,comctl32.dll。所有的Win32 API调用最终都转向ntdll.dll,然后由它转发到ntoskrnl.exe。ntdll.dll是本机 API的用户模式终端。真正的接口在ntoskrnl.exe中实现。事实上,内核模式驱动大部分时间调用这个模块,如果它们请求系统服务。Ntdll.dll的主要作用是使得内核函数的特定子集能够被用户模式下运行的程序所调用。Ntdll.dll通过软件中断int 2Eh进入ntoskrnl.exe,即通过中断门切换CPU特权级。比如kernel32.dll导出的函数DeviceIoControl()实际上调用ntdll.dll中导出的NtDeviceIoControlFile(),反汇编一下这个函数可以看到,EAX载入magic数0x38,实际上是系统调用号,然后EDX指向堆栈。目标地址是当前堆栈指针ESP+4,所以EDX指向返回地址后面一个,也就是指向在进入NtDeviceIoControlFile()之前存入堆栈的东西。事实上就是函数的参数。下一个指令是int 2Eh,转到中断描述符表IDT位置0x2E处的中断处理程序。
NtDeviceloControlFile :
mov eax, 38h
lea edx, [esp+4]
int 2Eh
ret 28h
当然int 2E接口不仅仅是简单的API调用调度员,它作为从用户模式进入内核模式的main gate。
Windows 2000 Native API由248个这么处理的函数组成,比NT 4.0多了37个。可以从ntdll.dll的导出列表中很容易认出来:前缀Nt。Ntdll.dll中导出了249个,原因在于NtCurrentTeb()是一个纯用户模式函数,因此不需要传给内核。令人惊奇的是,仅仅Native API的一个子集能够从内核模式调用。另一方面,ntoskrnl.exe导出了两个Nt*符号,它们不存在于ntdll.dll中: NtBuildNumber, NtGlobalFlag。它们不指向函数,事实上,是指向ntoskrnl.exe的变量,能够被使用C编译器extern关键字的驱动模块导入。Ntdll.dll和ntoskrnl.exe中都有两种前缀Nt*,Zw*。事实上ntdll.dll中反汇编结果两者是一样的。而在ntoskrnl.exe中,nt前缀指向真正的代码,而zw还是一个int 2Eh的stub。也就是说zw*函数集通过用户模式到内核模式门传递,而Nt*符号直接指向模式切换之后的代码。Ntdll.dll中的NtCurrentTeb()没有对应的zw函数。Ntoskrnl并不导出配对的Nt/zw函数。有些函数只以一种方式出现。
2Eh中断处理程序把EAX中的值作为查找表中的索引,去找到把最终的目标函数。这个表也就是系统服务表SST,C语言的结构SYSTEM_SERVICE_TABLE如下定义:清单也包含了结构SERVICE_DESCRIPTOR_TABLE的定义,是SST数组的第四个成员,前两个有特别的用途。
typedef NTSTATUS (NTAPI *NTPROC) ( ) ;
typedef NTPROC *PNTPROC;
#define NTPROC_ sizeof (NTPROC)
typedef struct _SYSTEM_SERVICE_TABLE
{ PNTPROC ServiceTable; // 入口指针数组
PDWORD CounterTable; // 调用次数计数数组
DWORD ServiceLimit ; // 服务入口个数
PBYTE ArgumentTable; // 服务参数字节数数组
) SYSTEM_SERVICE_TABLE ,
* PSYSTEM_SERVICE_TABLE ,
* * PPSYSTEM_SERVICE_TABLE ;
/ / _ _ _ _ _ _ _ _ _ _ _ _
typedef struct _SERVICE_DESCRIPTOR_TABLE
{ SYSTEM_SERVICE_TABLE ntoskrnl ; // ntoskrnl实现的系统服务,本机API}
SYSTEM_SERVICE_TABLE win32k; // win32k实现的系统服务,gdi/user支持
SYSTEM_SERVICE_TABLE Table3; // 未使用
SYSTEM_SERVICE_TABLE Table4; // 未使用
} SERVICE_DESCRIPTOR_TABLE ,
* PSERVICE_DESCRIPTOR_TABLE,
* PPSERVICE_DESCRIPTOR_TABLE ;
ntoskrnl通过一个符号:KeServiceDescriptorTable,导出了主要SDT的一个指针。内核维护另外一个SDT,也就是KeServiceDescriptorTableShadow。但是这个符号没有导出。要在内核模式组件中存取主要SDT很简单,只需要两行C语言代码:
extern PSERVICE_DESCRIPTOR_TABLE KeServiceDescriptorTable;
PSERVICE_DESCRIPTOR_TABLE psdt= KeServiceDescriptorTable;
NTPROC是本机 API的方便的占位符,类似于Win32编程中的PROC。Native API通常返回一个NTSTATUS代码,使用NTAPI调用约定,它和_stdcall同义。ServiceLimit成员有在ServiceTable数组中找到的入口的数目。在2K下,默认值是248。ArgumentTable是BYTEs的数组,每一个对应于ServiceTable的位置并且显示了在调用者堆栈中的参数比特数。这个信息和EDX结合,是内核从调用者堆栈copy参数到自己的堆栈所需要的。CounterTable成员在free buid的2k中并没有使用,在debug build中,这个成员指向代表每个函数使用计数的DWORDS数组,这个信息可以用于性能分析。
可以使用命令:dd KeServiceDescriptorTable来显示,调试器把这个符号解析为0x8046e0c0。只有前四行是重要的,对应于那四个SDT成员。
运行命令:ln 8046e100,显示符号为KeServiceDescriptorTableShadow,说明第五个开始确实是内核维护的第二个SDT。主要的区别在于后一个包含win32k.sys的入口,而前一个没有。在两个表中,Table3和Table4都是空的。Ntoskrnl.exe提供了一个方便的API函数名字为:
KeAddSystemServiceTable去填充这些位置。
2Eh的中断处理标记为KisystemService()。这也是ntoskrnl.exe没有导出的内部符号,但是包含在2k符号文件中。KisystemService的操作如下:
1 从当前线程控制块检索SDT指针
2 决定使用SDT中4个SST的哪一个。通过测试EAX中的递送ID的第12和13位来决定。ID在0x0000-0x0fff的映射到ntoskrnl表格,ID在
0x1000和0x1ffff的分配给win32k表格。剩下的0x2000-0x2ffff和
0x3000-0x3ffff为Table3和Table4保留。
3 通过选定SST的ServiceLimit成员检查EAX的0-11位。如果ID超过范围,返回错误代码STATUS_INVALID_SYSTEM_SERVICE。
4 检查EAX中的参数堆栈指针和MmUserProbeAddress。这是一个ntoskrnl导出的全局变量。通常等于0x7FFF0000,如果参数指针不在这个地址之下,返回STATUS_ACCESS_VIOLATION。
5 查找ArgumentTable中的参数堆栈字节数,从调用者堆栈copy所有的参数到当前内核模式堆栈。
6 查找serviceTable中的服务函数指针,并且调用这个函数。
7 控制转到内部函数KiserviceExit,在这次服务调用返回之后。
从对SDT的讨论可以看到和本机API一起还有第二个内核模式接口。这个接口把Win32子系统的图形设备接口以及窗口管理器和内核模式组件Win32k连接起来。Win32k接口也是基于int 2eh。本机API的服务号从0x0000到0x0fff,win32k的服务号从0x1000到0x1fff。(dd W32pServiceTable确定win32k.sys的符号可用。)win32k总共包含639个系统服务。
2Eh处理过程没有使用全局SDT KeServiceDescriptorTable
而是一个线程相关的指针。显然,线程可以有不同的SDT相关到自身。线程初试化时KeInitializeThread()把KeServiceDescriptorTable写到线程控制块。尽管如此,这个默认设置之后可能被改变成其它值,比如KeServiceDescriptorTableShadow。
Windows 2k运行时库
Ntdll.dll导出了不少于1179个符号。其中的249/248属于Nt*/zw*集合。因此还有682个函数不通过int 2eh门中转。显然,这么多函数不依靠2k的内核。
其中一些是和C运行时库几乎相同的函数。其实ntoskrnl也实现了一些类似C运行时库的函数。可以通过ddk中的ntdll.lib来链接并使用这些函数。反汇编ntdll.dll和ntoskrnl.exe的C运行时函数可以发现,ntdll.dll并不依赖ntoskrnl.exe。两个模块各自实现了这些函数。
除了C运行时库,2k还提供了一个扩展的运行时函数集合。再一次,ntdll.dll和ntoskrnl.exe各自实现了它们。同样,实现集合有重叠,但并不完全匹配。这个集合的函数都以Rtl开头。2k运行时库包括一些辅助函数用于C运行时无法完成的任务。比如有些处理安全事务,另外的操纵2k专用的数据结构,还有些支持内存管理。微软仅仅在DDK中记录了特别有用的406个函数中的115个。
Ntdll.dll还提供了另外一个函数集合,以__e前缀开头。实际上它们用于浮点数模拟器。
还有很多的函数集合,所有这些函数的前缀如下:
__e(浮点模拟),Cc(Cache管理),Csr(c/s运行时库),Dbg(调试支持),Ex(执行支持),FsRtl(文件系统运行时),Hal(硬件抽象层),Inbv(系统初试化/vga启动驱动程序bootvid.dll),Init(系统初试化),Interlocked(线程安全变量操作),Io(IO管理器),Kd(内核调试器支持),Ke(内核例程),Ki(内核中断处理),Ldr(映象装载器),Lpc(本地过程调用),Lsa(本地安全授权),Mm(内存管理),Nls(国际化语言支持),Nt(NT本机API),Ob(对象管理器),Pfx(前缀处理),Po(电源管理),Ps(进程支持),READ_REGISTER_(从寄存器地址读),Rtl(2k运行时库),Se(安全处理),WRITE_REGISTER_(写寄存器地址),Zw(本机API的替换叫法),<其它>(辅助函数和C运行时库)。
当编写从用户模式通过ntdll.dll或内核模式通过ntoskrnl.exe和2k内核交互的软件的时候,需要处理很多基本的数据结构,这些结构在Win32世界中很少见到。
常用数据结构
l 整数
ANSI字符是有符号的,而Unicode WCHAR是无符号的
MASM的TBYTE是80位的浮点数,用于高精度浮点运算单元操作,注意它与Win32的TBYTE(text byte)完全不同。
TABLE 2-3. Equivalent Integral Data Types
BITS MASM FUNDAMENTAL ALIAS #1 ALIAS #2 SIGNED
8 BYTE 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
typedef union _LARGE_INTEGER
{ struct{
ULONG LowPart;
LONG HighPart;};
LONGLONG QuadPart;
}
LARGE_INTEGER , * PULARGE_INTEGER ;
typedef union _ULARGE_INTEGER{
struct{
ULONG LowPart;
ULONG HighPart;}
ULONGLONG QuadPart;
}ULARGE_INTEGER, *PULARGE_INTEGER;
l 字符
Win32编程中PSTR用户CHAR*,PWSTR用于WCHAR*。取决于是否定义了UNICODE,PTSTR解释为PSTR或者PWSTR。在2k内核模式下,常用的数据类型是UNICODE_STRING,而STRING用来表示ANSI字符串:
typedef struct _UNICODE_STRING{
USHORT Length; //当前字节长度,不是字符!!!
USHORT MaximumLength; //Buffer的最大字节长度
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;
操纵函数:RtlCreatUnicodeString(),RtlInitUnicodeString(),
RtlCopyUnicodeString()等等
l 结构
许多内核API函数需要一个固定大小的OBJECT_ATTRIBUTES结构,比如NtOpenFile()。对象的属性是OBJ_*值的组合,可以从ntdef.h中查到。
IO_STATUS_BLOCK结构提供了所请求操作结果的信息,很简单,status成员包含一个NTSTATUS代码, 如果操作成功 information成员提供特定请求的信息。
还有一个结构是LIST_ENTRY,这是一个双向环链表。
typedef struct _OBJECT_ATTRIBUTES
{
ULONG Length;
HANDLE RootDirectory;
PUNICODE_STRING ObjectName;
ULONG Attributes;
PVOID SecurityDescriptor;
PVOID SecurityQualityOfService;
} OBJECT_ATTRIBDTES, *POBJECT_ ATTRIBUTES;
typedef struct _IO_STATUS_BLOCK
{
NTSTATDS Status;
ULONG Information;
}IO_STATUS_BLOCK , * PIO_STATUS_BLOCK ;
typedef struct _LIST_ENTRY
{
Struct _LIST_ENTRY *Flink;
Struct _LIST_ENTRY *Blink;
}LIST_ENTRY, *PLIST_ENTRY;
双向链表的典型例子就是进程和线程链。内部变量PsActiveProcessHead是一个LIST_ENTRY结构,在ntoskrnl.exe的数据段中,指定了系统进程列表的第一个成员。
CLIENT_ID结构由进程和线程ID组成。
typedef struct _CLIENT_ID
{ HANDLE UniqueProcess;
HANDLE UniqueThread;
)CLIENT_ID, *PCLIENT_ID;
想要从用户模式调用ntdll.dll中的API函数,必须考虑到以下四点:
1 SDK头文件没有包括这些函数的原型
2 这些函数使用的若干基本数据类型没有包括在SDK文件中
3 SDK和DDK头文件不兼容,不能在win32的c源文件包含ntddk.h中
4 ntdll.lib没有包括在VC的默认导入库列表中。
第4个很容易解决:#progma comment(linker,“/defaultlib:ntdll.lib”)
缺失的定义比较难解决,最简单的方法是写一个自定义的头文件,刚刚包含需要调用ntdll.dll中函数的定义。幸运的是,已经在光盘的w2k_def.h文件中做了这个工作。因为这个头文件将用于用户模式和内核模式程序,所以必须在用户模式代码中,#include<w2k_def.h>之前#define _USER_MODE_,使得DDK中出现而SDK中没有的定义可用。
还有另外三个好的本机API资料来源:
l Mark Russinovich的Inside the Native API
www.sysinternals.com/ntdll.htm
l 1999,11期DDJ包含我的文章:Inside Windows NT System Data
www.ddj.com/ftp/1999/1999_11/ntinfo.zip
l Gary Nebbett的Windows NT/2000 Native API Reference。
第6章中出现的w2k_call.dll例子库,展示了如何使用w2k_def.h。第6章还提供了另一种方法,从用户模式调用2k内核,并且这种方法不限制于本机API集合。事实上,这个方法不限制于ntoskrnl.exe,而可以用于任何加载到内核内存的模块,如果导出了API函数,或者提供了匹配的.dbg或.pdb符号文件。