| 位符,这种方式和在 Win32 编程中使用的 PROC 类型很相似。 NTPROC 的定义在前面的 列表 2-1 中给出。 Native API 函数通常返回一个 NTSTATUS 类型的代码并且使用 NTAPI 调用方式, NTAPI 实际上就是 _stdcall 。 ServiceLimit 成员保存在 ServieTable 数组中发现的入口地址的个数。在 Windows 2000 中,其默认值为 248 。 ArgumentTable 成员是一个 BTYE 类型的数组,它和 ServiceTable 所指的数组一一对应,并给出其中每个函数指针所需的参数在调用者的堆栈中的字节数。此信息随 EDX 寄存器提供的指针一起使用。当内核从调用者的堆栈中复制参数到自己的堆栈时就需要这些信息。 CounterTable 成员在 Windows 2000 的 Free Build 版中不被使用。在 Debug Build 版中,该成员指向一个 DWORD 类型的数组,作为每个函数的使用计数器( usage counters )。 This information can be used for profiling purposes.
使用 Windows 2000 的内核调试器可方便的显示 SDT 中的内容。如果你还没有设置好这个有用的程序,那请参考第一章。在 示列 2-2 中,我首次使用了 dd KeServiceDescriptorTable 命令。调试器会将此公开符号解析为 0x8046AB80 ,同时显示该地址之后的 32 个 DWORD 的 16 进制转储。不过仅有前面的四行才是有意义的,它们分别对应 列表 2-1 中的四个 SDT 成员。为了更清晰些,它们都将以黑体显示。如果你仔细观察,你会发现第五行与第一行十分相像,这是另一个 SDT 吗?这是测试内核调试器的 ln 命令的好机会。在示列 2-2 中,在显示完 KeServiceDescriptorTable 的十六进制 dump 之后,我输入 ln 8046abc0 命令。显然,调试器知道地址 0x8046abc0 ,它将此地址转化为对应的符号 KeServiceDescriptorTableShadow 可以看出,这是内核维护的第二个 SDT 。二者之间的显著区别是:第二个 SDT 包含 Win32k.sys 的入口地址。这两个表的的第三和第四个成员都是空的。 Ntoskrnl.exe 提供了一个函数 KeAddSystemServiceTabel() 来填充这两个成员。
注意,我截断了 ln 命令的输出信息,仅保留了基本的信息。
从地址 0x8046ab88 开始,是 KeServiceDescriptorTable 的十六进制转储,在那儿可以找到 ServiceLimit 成员,可看到其值为 0xF8 (十进制 248 ),这和我们预期的一样。 ServiceTable 和 ArgumentTable 的值分别指向地址 0x804704d8 和 0x804708bc 。用 ln 命令察看着两个地址,可得到其符号: KiServiceTable 和 KiArgumentTable 。这两个符号都没有从 ntoskrnl.exe 中导出,但是调试器可通过察看 Windows 2000 的符号文件识别它们。 ln 命令还可应用到 Win32k SST 指针上,针对其 ServiceTable 和 ArgumentTable 成员,调试器分别给出了其对应的符号 w32pServiceTable 和 W32pArgumenTable 。这两个符号都来自 Win32k.sys 的符号文件。如果调试器无法解析这些地址,可使用 .reload 命令强制重新加载所有可用符号文件,然后再进行解析。
示例 2-2 的剩余部分是 KiServiceTable 和 KiArgumentTable 最前面的 128 个字节的十六进制转储。到目前为止,如果我说的有关 Native API 的东西都是正确的,那么 NtClose() 函数的地址应位于 KiServiceTable 数组的第 24 个位置上,其地址为 0x80470538 。在该地址处,可发现其值为 0x8044c422 ,在 dd KiServiceTable 的输出中,该地址以黑体标记。用 ln 察看 0x8044c422 ,会看到其对应的符号正是 NtClose() 。
kd> dd KeServiceDescriptorTable
8046ab80 804704d8 00000000 000000f8 804708bc
8046ab90 00000000 00000000 00000000 00000000
8046aba0 00000000 00000000 00000000 00000000
8046abb0 00000000 00000000 00000000 00000000
8046abc0 804704d8 00000000 000000f8 804708bc
8046abd0 a0186bc0 00000000 0000027f a0187840
8046abe0 00000000 00000000 00000000 00000000
8046abf0 00000000 00000000 00000000 00000000
kd> ln 8046abc0
(8046abc0) nt!KeServiceDescriptorTableShadow
kd> ln 804704d8
(804704d8) nt!KiServiceTable
kd> ln 804708bc
(804708bc) nt!KiArgumentTable
kd> ln a0186bc0
(a0186bc0) win32k!W32pServiceTable
kd> ln a0187840
(a0187840) win32k!W32pArgumentTable
kd> dd KiServiceTable
804704d8 804ab3bf 804ae86b 804bdef3 8050b034
804704e8 804c11f4 80459214 8050c2ff 8050c33f
804704f8 804b581c 80508874 8049860a 804fc7e2
80470508 804955f7 8049c8a6 80448472 804a8d50
80470518 804b6bfb 804f0cef 804fcb95 8040189a
80470528 804d06cb 80418f66 804f69d4 8049e0cc
80470538 8044c422 80496f58 804ab849 804aa9da
80470548 80465250 804f4bd5 8049bc80 804ca7a5
kd> db KiArgumentTable
804708bc 18 20 2c 2c 40 2c 40 44-0c 18 18 08 04 04 0c 10 . ,,@,@D........
804708cc 18 08 08 0c 08 08 04 04-04 0c 04 20 08 0c 14 0c ........... ....
804708dc 2c 10 0c 1c 20 10 38 10-14 20 24 1c 14 10 20 10 ,... .8.. $... .
804708ec 34 14 08 04 04 04 0c 08-28 04 1c 18 18 18 08 18 4.......(.......
804708fc 0c 08 0c 04 10 00 0c 10-28 08 08 10 00 1c 04 08 ........(.......
8047090c 0c 04 10 00 08 04 08 0c-28 10 04 0c 0c 28 24 28 ........(....($(
8047091c 30 0c 0c 0c 18 0c 0c 0c-0c 30 10 0c 0c 0c 0c 10 0........0......
8047092c 10 0c 0c 14 0c 14 18 14-08 14 08 08 04 2c 1c 24 .............,.$
kd> ln 8044c422
(8044c422) nt!NtClose
示例 2-2 检查服务描述符表
译注:
在 Windows XP 中, KeServiceDescriptorTable 和 KeServiceDescriptorTableShadow 和 Windows 2000 有所区别。在 XP 中,后者位于前者的前面,而在 W2K 中,后者位于前者的后面。
INT 2eh 系统服务处理例程( System Service Handler )
隐藏在内核模式中的 INT 2eh 中断处理例程为 KiSystemService() 。再强调一次,这是一个内部符号, ntoskrnl.exe 并没有导出该符号,不过,它却包含在 Windows 2000 的符号文件中。因此,内核调试器可以正确的解析该符号。从本质上来看, KiSystemService() 将执行如下操作:
1. 从当前线程的控制块( thread's control block )中检索 SDT 指针。
2. 通过测试 EAX 寄存器中的分派 ID 的第 12 、 14 位来确定使用 SDT 中的那个 SST ( SDT 中有四个 SST )。如果分派 ID 位于 0x0000-0x0FFF ,将选择 ntoskrnl 表;位于 0x1000-0x1FFF 则选择 Win32k 表。 0x2000-0x2FFF 和 0x3000-0x3FFF 由 SDT 的 Table3 和 Table4 保留。如果分配 ID 超过了 0x3FFF ,在分派前多余的位将被屏蔽掉。
3. 通过检查分派 ID 的 0 到 11 位来确定该 ID 在所选 SST 中对应的 ServiceLimit 成员。如果 ID 超出了范围,将返回错误代码: STATUS_INVALID_SYSTEM_SERVICE 。在一个未使用的 SST 中, ServiceLimit 成员始终是 0 ,从而为所有可能的分派 ID 产生一个错误代码。
4. 通过检查 EDX 中保存的参数堆栈指针,来取得 MmUserProbeAddress 的值。这是由 ntoskrnl.exe 导出的一个公开变量。参数指针通常会与 0x7FFF0000 进行比较。如果没有低于该地址,那么将返回 STATUS_ACCESS_VIOLATION 。
5. 根据在 SST 的 ArgumentTable 中查找到的参数堆栈的字节数,将所有函数参数从调用者堆栈中复制到当前的内核堆栈中。
6. 在从服务调用( Service Call )中返回后,将控制权传递给内部函数 KiServiceExit()
非常有趣的是 INT 2eh 中断处理例程并不使用全局 SDT (即 KeServiceDescriptorTable ),而是使用线程专属的指针替代之。显然,每个线程可以拥有不同的 SDT 。在线程初始化时, KeInitializeThread() 会将 KeServiceDescriptorTable 的指针写入线程控制块( Thread Control Block )中。不过,此默认值在稍后可能会改变,如改为指向 KeServiceDescriptorTableShadow 。
Win32 内核模式接口( Win32 Kernel-mode Interface )
从前面对 SDT 的讨论,可看出存在着与 Native API 相关的第二个主内核模式接口( main Kernel-mode Interface )。该接口将 Win32 子系统的图形设备接口( Graphics Device Interface, GDI )、窗口管理器(即 User 模块)连接至内核组件 ---Win32K (即 Win32k.sys ) . ,该组件随同 Windows NT 4.0 引入。引入该组件是为了克服 Win32 图形引擎固有的性能限制(由于 Windows NT 子系统的最初设计导致)。在 Windows NT 3.x 中, Win32 子系统采用的是客户 - 服务器模式( Client-Server model ),这样就必须从用户模式切换到内核模式才能进行内核调用( Kernel Involved )。通过将图形引擎的绝大部分移至内核组件 ---Win32k.sys ,从而避免了大部分因内核切换导致的性能损失。
Win32K 分派 ID ( Win32K Dispatch IDs )
现在该介绍 Win32k.sys 了,也是该更新 图 2-1 的时候了。 图 2-2 基于 图 2-1 ,但在 ntoskrnl.exe 左面加入了 Win32k.sys 。同时我还加入了从 GDI32.DLL 和 USER32.DLL 指向 Win32k.sys 的箭头。当然,这不是百分之百正确,因为这些模块中的 INT 2eh 调用实际上指向 ntoskrnl.exe ,在 ntoskrnl.exe 中才有该中断的处理例程。然而,调用最后还是由 Win32k.sys 管理,这也是箭头这样指的原因。

稍早提到过, Win32K 接口同样基于 INT 2eh 分派器( INT 2eh Dispatcher ),这与 Native API 非常相似。仅有的区别在于 Win32K 使用另一区段的分派 ID 。尽管与所有 Native API 调用相关的分派 ID 都位于 0x0000----0x0FFF ,而 Win32K 分派 ID 位于 0x1000---0x1FFF 之间。如 图 2-2 所示, Win32K 的主要客户端是 GDI32.DLL 和 USER32.DLL 。因此,通过反编译这些模块(指 gdi32.dll 和 user32.dll )可能会找到与 Win32K 分派 ID 相关的符号化名称。通过反编译可发现在这些模块( gdi32.dll 和 user32.dll )的导出节( export sections )中仅包含 INT 2eh 调用的一个很小的子集,看来是时候再次使用内核调试器了。如 示例 2-3 所示,我通过使用 dd W32pServiceTable 命令,来确定 Win32k.sys 的符号是可用的,在此之前请先使用 .reload 命令以加载所有可用符号文件。

在 示例 2-3 的最后三行中,我使用 ln 命令显示与 W32pServiceTable 的第一个入口地址相关的符号。显然,可看到分派 ID 为 0 的 Win32K 函数为 NtGdiAbortDoc() 。你可以针对所有 639 个 ID 来重复此过程,但是最好能自动进行符号的查找。现在,我已经为你完成了这项工作,所有分派 ID 对应的符号名称都收录在 附录 B 的 表 B-2 中。符号从 gdi32.dll 和 user32.dll 映射到 win32k.sys 十分简单: GDI 符号可通过在其前面添加 NtGdi 前缀就可转换为 Win32K 符号, USER 符号则添加 NtUser 前缀。然而,有一少部分例外。例如,如果一个 GDI 符号以 Gdi 开始,那么其前缀就减少为 Nt ,这可能是为了避免出现 NtGdiGdi 这样的字符序列。在其他的一些例子中,字符的大小写会有些不同(比如 EnableEUDC() 转化后则变成了 NtGdiEnableEudc() ),或者用符号名称尾部的 W 来表示没有对应的 Unicode 函数(如, CopyAcceleratorTableW() 转化后成为 NtUserCopyAcceleratorTable() )。
提供 Win32K API 的详细文档需要很大的努力。这些函数几乎是 Native API 的三倍。或许某天有人会为这些 API 编写一本不错的参考手册,就像 Gary Nebbett 编写的 Native API 手册。不过,在本书范围内,有关这些 API 的信息已经足够了。 |