本章对于 Windows 2000 Native API 的讨论,主要集中在这些 API 和系统模块之间的关系,将重点介绍 Windows 2000 采用的中断机制
// Import SDT pointer
extern PSERVICE_DESCRIPTOR_TABLE KeServiceDescriptorTable;
// Create SDT reference
PSERVICE_DESCRIPTOR_TABLE psdt = KeServiceDescriptorTable;
列表 2-2 访问系统服务描述符表
SDT 中的每个 SST 的 ServiceTable 成员都是一个指针,指向一个由函数指针构成的数组,此函数指针的类型为: NTPROC ,这为 Native API 提供了占
例如,由 kernel32.dll 导出的 Win32 API 函数 DeviceIoControl() 最终会调用由 ntdll.dll 导出的 NtDeviceIoControlFile()
服务描述符表( The Service Descriptor Tables )
从 示例 2-1 给出的反编译代码可看出, INT 2eh 随同传入 CPU 寄存器 EAX 和 EDX 的两个参数一起被调用
NT*() 和 Zw*() 函数集
有关 Windows 2000 架构的一个有趣的事实是:它模拟了多个操作系统
系统服务分配器( System Service Dispatcher )
Win32 子系统和 Native API 之间的关系可以由 Win32 核心模块与 Windows 2000 内核模块之间的依赖关系很好的解释
表 B-1 (位于附录 B 中)中有两件事需要特别注意
NtDeviceIoControlFile:
mov eax, 38h
lea edx, [esp+4]
int 2Eh
ret 28h
示列 2-1. ntdll.NtDeviceIoControlFile() 的实现方式
Windows 2000 Native API 由 248 个函数组成,这些函数都采用上述方式进入内核
www.orchn.com
Ntdll.dll 是一个操作系统组件,它为 Native API 准确地提供服务, ntdll.dll 是 Native API 在用户模式下的前端
l 半文档化的( Semidocumented ) :尽管不是正式文档,但这些信息还是可以从微软正式发布的文件中挖掘出来的
l 完全未文档化的 :微软很好的隐藏了某些信息,要获得它们只能通过逆向工程和推理
服务器
l 未文档化,但并没有隐藏 :这些信息不能在任何官方文档和开发文档中找到,但其中的一部分对调试工具是可用的
typedef NTSTATUS (NTAPI*NTPROC)();
typedef NTPROC* PNTPROC;
#define NTPROC_ sizeof(NTPROC)
typedef struct _SYSTEM_SERVICE_TABLE
{
PNTPROC ServiceTable; // array of entry points
PDOWRD CounterTable; // array of usage counters
DWORD ServiceLimit; // number of table entries
PBYTE ArgumentTable; // array of byte counts
}
SYSTEM_SERVICE_TABLE,
*PSYSTEM_SERVICE_TABLE,
**PPSYSTEM_SERVICE_TABLE;
//-----------------------------------------------------------------------------------------------------------
typedef struct _SERVICE_DESCRIPTOR_TABLE
{
SYSTEM_SERVICE_TABLE ntoskrnl; // ntoskrnl.exe ( native api )
SYSTEM_SERVICE_TABLE win32k; // win32k.sys (gdi/user support)
SYSTEM_SERVICE_TABLE Table3; // not used
SYSTEM_SERVICE_TABLE Table4; // not used
}
SYSTEM_DESCRIPTOR_TABLE,
*PSYSTEM_DESCRIPTOR_TABLE,
**PPSYSTEM_DESCRIPTOR_TABLE;
列表 2-1 系统服务描述符表的结构定义
现在,回到 SDT ( Service Descriptor Table )的秘密上来
你可能会奇怪为什么 表 B-1 (位于附录 B 中)分别为 ntdll.dll 和 ntoskrnl.exe 提供了两列,其名称分别为: ntdll.Nt* 、 ntdll.Zw* 和 ntoskrnl.Nt* 、 ntoskrnl.Zw*
www.orchn.com
尽管上述的两个表是系统基本的数据类型,但他们在 Windows 2000 DDK 中 并没有相应的文档记载,本书中出现的许多代码片断都包含未文档化的数据类型和函数
服务器
有关 Windows 2000 架构的详细讨论已经很多
网络
本书讨论的 Windows 2000 的内部细节覆盖了上述系统分类的后三个
列表 2-2 中第二个 C 指令就是这样的一个引用
图 2-1 展示了模块间的依赖关系,方框表示系统模块,箭头表示模块间的依赖关系
图 2-1 表现出的一个特性非常有趣,所有的 Win32 API 调用最后都转移到了 ntdll.dll ,而 ntdll.dll 又将其转移到了 ntoskrnl.exe
www.orchn.com 附录 B 中的 表 B-1 列出了所有可用的 Native API
Native API 真正的接口在 ntoskrnl.exe 中实现
Ntdll.dll 总共导出了 249 个这样的符号
Ntdll.dll 的主要任务就是为运行于用户模式的程序提供一个确定的内核函数的子集,这其中就包括 Win32 子系统 DLLs
Ntolkrnl.exe 导出了一个指针(符号为 KeServiceDescriptorTable )指向其主服务描述符表( Main SDT )
Win32 子系统是最流行的一个,因此它更多的被开发人员和操作系统所关照
Window 2000 采用此种方式导出了很多变量,稍后我将给出一个示例代码来使用其中的几个
Windows 2000 内置三个子系统来支持 Win32 、 POSIX 和 OS/2 应用程序
Windows 2000 利用此机制将对内核服务的请求从用户模式向内核模式传递
《 Inside Windows NT 》( Custer 1993, Solomon 1998 )的第一、二版都是有关此方面的好书,同样的还有《 Inside Windows 2000 》( Solomon and Russinovich 2000 )
与 Windows NT 4.0 相比多出了 37 个
事实上,内核模式驱动程序对系统服务的请求多数时候都会进入该模块
从 列表 2-1 给出的该结构的定义可看出该结构的头两个数组保留给了 ntoskrnl.exe 和 Win32 子系统(位于 win32k.sys )中的内核模式( kernel-mode )部分
从其文件名可以猜出它就是 NT 操作系统内核
从处于内核模式的模块中访问主服务描述符表( SDT )非常容易,你只需要两个 C 指令,如 列表 2-2 所示
服务器 令人奇怪的是,在处于内核模式的模块中,只能调用 Native API 的一个子集
以 Windows 2000 为例,很多重要的半文档化信息都源自头文件 ntddk.h 和 ntdef.h ,这两个文件都是 DDK 的一部分
但是, Windows 2000 的设计却有很大不同
位于 ntoskrnl.exe 中的中断处理例程将 EAX 中的数值作为一个索引来查询一个特定的表
www.orchn.com 你很容易在 ntdll.dll 的导出列表中通过 Nt 前缀来认出它们
www.orchn.com 例如, Windows 2000 的很多函数和结构体并没有在 SDK 或 DDK 文档中提到,但出现在一些头文件或示列程序中
其中的一些仅以 Nt* 或 Zw* 的形式出现
其次, ntoskrnl.exe 不再一贯的成对的导出 Nt/Zw 函数
内核还维护了一个替代的 SDT ,其名称为: KeServiceDescriptorTableShadow ,但这个 SDT 并没有被导出
原因是,这两个模块导出了两组相互关联的 Native API 符号
反编译 ndll.dll 可看出每对符号都指向相同的代码
另一个集合包含相似的名字,不过由 Zw 前缀代替了 Nt
另一方面, ntoskrnl.exe 导出了两个 ntdll.dll 没有提供的 Nt* 符号(指以 Nt 开头的符号): NtBuildNumber 和 NtGlobalFlag
另外, Win32K 接口和一些与 Native API 相关的主要运行时库也会被提及,同时还将介绍一些经常使用的数据类型
只有在原始的代码中包含所有的信息,但我无法得到它们
可执行文件或符号文件中的所有符号化信息都属于这一部分
因此,不能保证这些信息是完全真实可信的
因此,必须让这些代码远离内核函数
www.orchn.com 因此,有关此话题的信息并不是很多, neither SDK nor the DDK make the Native API available to Win32 Application.
未文档化的级别
本书中的多数东西都来自被称为未文档化的信息
在 图 2-1 中,从 ntdll.dll 指向 ntoskrnl.exe 的箭头旁标注的 INT 2eh 表示 Windows 2000 使用此中断将 CPU 特权级从用户模式切换到内核模式
在 图 2-1 中,模块: user32.dll 、 advapi32.dll 、 gdi32.dll 、 rpcrt4.dll 以及 kernel32.dll 实现了基本的 Win32 API
在 表 B-1 (位于附录 B 中)的最左列给出了所有名字中包含 Nt 前缀的符号
在 Alpha 平台上,有不同的方式来实现此种功能
在 Windows 9x 中, Win32 接口实际上是作为整个系统的基础结构来实现的
在创建这些符号时,我试图使用适当的名称,这些名称基于从已知符号的一个很小的子集(包括从符号文件中得到的那些)中得出的 命名方案
在很多编程书籍中, Windows NT/2000 的软件开发被简化为与 Win32 API 打交道的工作, NT 平台暴露出的一个隐藏的事实是存在另一个更为基础的调用接口: Native API
在该列表中还包含 SERVICE_DESCRIPTOR_TABLE 结构的定义,该结构共有四个 SST 类型的数组,其中的前两个用于特定目的
多出的那个函数是 NtCurrentTeb() ,该函数是一个纯粹的用户模式函数,它无需进入内核
大多数重要信息来自 SDK 、 DDK 和 MSDN
如果一个箭头从模块 A 指向模块 B ,这表示 A 依赖于 B ,即,模块 A 调用 B 中的函数
它仅仅是 Win32 子系统的一个基本组件
实际上,我并不打算阅读这些源代码,因为这需 要和微软签订一个 NDA ( Non-Disclosure Agreement, ,不可泄漏协议),由于该 NDA 的限制,将很难写出一本有关非文档化信息的书
将类型为 PSERVER_DESCRIPTOR_TABLE 的变量赋值为 KeServiceDescriptorTable 时,就会和 ntoskrnl.exe 建立一个动态连接
服务器 尽管 Win32 子系统包含一个名为 kernel32.dll 的系统模块,但这并不是实际的操作系统内核
开发内核( kernel-mode )模式程序的人员认为用户模式的代码是具有攻击性的、充满错误的和危险的
通信 当然,还有其他的 DLL (如 version.dll 、 shell32.dll 和 comctl32.dll )也为 Win32 API 提供支持,为了更清晰些,我省略了它们
当该模块被加载到进程的地址空间后,针对该符号的引用才会动态连接到相应的模块中
我不知道为什么会这样,我猜测 ntoskrnl.exe 仅导出了在 Windows 2000 DDK 中有文档记录的函数以及其它系统模块必须的那些函数
我个人的系统分类如下:
l 正式文档 :这些信息来自微软出版的书、文件或者开发工具
我已经提到过 EAX 中的“魔术”数字是一个分派 ID
所有符号化的信息,如结构名 称、结构成员和参数都是如此
挖掘系统内部的信息是非常困难的,但同样是非常有趣的
接下来的指令是一个简单的 INT 2eh ,该指令将跳转到中断描述符表( Interrupt Descriptor Table,IDT )的 0x2e 位置上存放的中断处理例程( interrupt handler )中
接下来,寄存器 EDX 被设置指向堆栈中的某处,其地址为堆栈指针 ESP 加上 4 ,因此, EDX 将指向堆栈中返回地址的后面,该返回地址在进入 NtDeviceIoControlFile() 时将被立即保存下来
www.orchn.com 显而易见, EDX 指向的位置是用来临时存放传递进来的参数的
通信 最好的例子是内核调试器的 !processfields 和 !threadfields 命令,这两个命令会给出两个未文档化的结构: EPROCESS 和 ETHREAD 的成员名称及其偏移量
www.orchn.com 来自 gdi32.dll 和 user32.dll 的调用都通过 Win32k 的系统服务表( SST )进行分派
www.orchn.com 模块由双向箭头连接,表示二者之间相互依赖
www.orchn.com 此类信息包含很多实现细节的信息,没有人认为 Windows 2000 开发人员需要关注它们,但是这些信息对于系统开发人员和开发调试软件的人来说却非常宝贵
注意,保留的 Native API 函数仍然实现于 ntoskrnl.exe 的内部
然而, Windows 2000 的 INT 2eh 接口要远比一个简单的 API 调用有用,分配器( dispatcher )利用它从用户模式进入内核模式
然而,你并不需要到驱动程序一层才能访问此接口 ---- 即使一个普通的 Win32 应用程序也可在任何时候调用 Native API
然而,在很多场合这种启发式方法并不成功
然而,如果你反编译 ntoskrnl.exe ,你就会发现 Nt* 符号指向实际的代码而 Zw* 指向 INT 2eh stubs (如 示列 2-1 列出的)
然而,未文档化也存在几个级别,这是因为可能被公布的有关庞大的操作系统(如 Windows 2000 )的信息非常的多
相信编写 kernel-mode driver 或 file system driver 的开发人员已经对 Native API 非常熟悉了,因为 kernel-mode 模块位于更低的系统层,在那里子系统是不可见的
而通过在调用 API 的过程中将特权级别从用户模式切换到内核模式是一种可控制这些问题的方式
许多有关 Windows NT 的讨论同样适用于 Windows 2000
服务器 该表同时还指出那个函数是由 ntoskrnl.exe 导出的
www.orchn.com 请注意,这种模式切换方式是 x86 处理器特有的
调用程序从来不可能触及内核,它只能察看它们
这不是什么大问题,因为 ntdll.dll 以相似的方式导出 Nt* 和 Zw* 函数
这两个符号都没有指向函数的入口地址,而是指向 ntoskrnl.exe 中的变量
这个表被称作系统服务表( System Service Table, SST )该表对应的 C 结构体 ---SYSTEM_SERVICE_TABLE 的定义在 列表 2-1 中给出
服务器 这些函数并没有公开的进入点,但可通过 INT 2eh 到达他们
通信 这就是提供分派 ID 的原因
www.orchn.com 这很像调用一个 DLL 中的 API 函数
这意味着 Zw* 函数集合将从用户模式转入内核模式,而 Nt* 符号直接指向的代码会在模式切换后被执行
www.orchn.com 这没什么技术上的限制 ---- 仅仅是微软不支持此种应用程序开发模式而已
这看上去是不是很熟悉?事实上,这有些像 DOS 下的 INT 21h 调用
这看起来似乎是浪费内存
www.orchn.com 这通常意味着微软没有公开发布这些信息
通过反编译该函数会发现此函数令人惊讶的实现方式 — 它是如此的简单! 示列 2-1 展示了这些
服务器 除 NtCurrentTeb() 之外的所有 Native API 都采用此种方式,处理 INT 2eh 的代码必须确定每个调用将被分配到那个函数
首先是由 extern 关键字修饰的变量说明,这告诉链接器该变量并不包含在此模块中,而且不需要在链接时解析相应的符号名称
首先, CPU 寄存器 EAX 被装入了一个“魔术”数字 0x38 ,这是一个分派 ID
首先, NtCurrentTeb() 函数没有对应的 Zw* 函数
驱动模块( driver module )可以使用 C 编译器的 extern 关键字来导入这些变量
0
顶一下0
踩一下