SSDT详解及Win32 API到系统服务描述符表调用的完整过程

一、什么是SSDT?

SSDT 的全称是 System Services Descriptor Table,系统服务描述符表。这个表就是一个把 Ring3 的 Win32 API 和 Ring0 的内核 API 联系起来。SSDT 并不仅仅只包含一个庞大的地址索引表,它还包含着一些其它有用的信息,诸如地址索引的基地址、服务函数个数等。通过修改此表的函数地址可以对常用Windows 函数及 API 进行 Hook,从而实现对一些关心的系统动作进行过滤、监控的目的。一些 HIPS、防毒软件、系统监控、注册表监控软件往往会采用此接口来实现自己的监控模块。

正如我在从用户模式切换到内核模式的完整过程分析这篇博文里面所说的,Windows API OpenProcess是从Kernel32导出的,所以调用首先转到了Kernel32的OpenProcess函数。在OpenProcess中又调用了ntdll!NtOpenProcess函数。然后通过快速系统调用进入内核,根据传进来的索引在SSDT中得到函数的地址,然后调用函数。

在 NT 4.0 以上的 Windows 操作系统中,默认就存在两个系统服务描述表,这两个调度表对应了两类不同的系统服务,这两个调度表为:KeServiceDescriptorTable 和 KeServiceDescriptorTableShadow,其中 KeServiceDescriptorTable 主要是处理来自 Ring3 层得 Kernel32.dll中的系统调用,而 KeServiceDescriptorTableShadow 则主要处理来自 User32.dll 和 GDI32.dll 中的系统调用,并且 KeServiceDescriptorTable 在ntoskrnl.exe(Windows 操作系统内核文件,包括内核和执行体层)是导出的,而 KeServiceDescriptorTableShadow 则是没有被 Windows 操作系统所导出,而关于 SSDT 的全部内容则都是通过 KeServiceDescriptorTable 来完成的 ~

image

ntoskrnl.exe中的一个导出项KeServiceDescriptorTable即是SSDT的真身,亦即它在内核中的数据实体。SSDT的数 据结构定义如下:

typedef struct _tagSSDT {  
    PVOID pvSSDTBase;  
    PVOID pvServiceCounterTable;  
    ULONG ulNumberOfServices;  
    PVOID pvParamTableBase;  
} SSDT, *PSSDT;

其中,pvSSDTBase就是上面所说的“系统服务描述符表”的基地址。pvServiceCounterTable则指向另一个索引表,该表包含了每 个服务表项被调用的次数;不过这个值只在Checkd Build的内核中有效,在Free Build的内核中,这个值总为NULL(注:Check/Free是DDK的Build模式,如果你只使用SDK,可以简单地把它们理解为Debug /Release)。ulNumberOfServices表示当前系统所支持的服务个数。pvParamTableBase指向SSPT(System Service Parameter Table,即系统服务参数表),该表格包含了每个服务所需的参数字节数。

我们用windbg看看里面到底是什么?

image

根据基地址与服务总数来查看整个服务表的各项:

image

有了上面的介绍后,我们可以简单的将 KeServiceDescriptor 看做是一个数组了(其实质也就是个数组),在应用层 ntdll.dll 中的 API 在这个系统服务描述表(SSDT)中都存在一个与之相对应的服务,当我们的应用程序调用 ntdll.dll 中的 API 时,最终会调用内核中与之相对应的系统服务,由于有了 SSDT,所以我们只需要告诉内核需要调用的服务所在 SSDT 中的索引就 OK 了,然后内核根据这个索引值就可以在 SSDT 中找到相对应的服务了,然后再由内核调用服务完成应用程序 API 的调用请求即可。

二、应用层调用Win32 API 的完整执行流程

有了上面的 SSDT 基础后,我们再来看一下在应用层调用 Win32 API(这里主要指的是 ntdll.dll 中的 API)的完整流程,这里我们主要是分析 ntdll.dll 中的 NtQuerySystemInformation 这个 API 的调用流程,因为后面的应用就是通过对这个函数地址的修改。(PS:Windows 任务管理器即是通过这个 API 来获取到系统的进程等等信息的)。

Ntdll.dll中的NtQuerySystemInformation或ZwQuerySystemInformation。会调用Ntoskrnl.exe中的ZwQuerySystemInformation然后ZwQuerySystemInformation再去调用Ntoskrnl.exe中的NtQuerySystemInformation。

我们可以看看Ntdll.dll的导出函数:

 

image

除了 NtQuerySystemInformation 外,ZwQuerySystemInformation也导出了。

image

而实质上,在 Windows 操作系统中,Ntdll.dll 中的ZwQuerySystemInformation 和 NtQuerySystemInformation 是同一函数,我们来反汇编看一下:

image

众所周知 Ntdll.dll 中的 API 都只不过是一个简单的包装函数而已,当 Kernel32.dll 中的 API 通过 Ntdll.dll 时,会完成参数的检查,再调用一个中断(int 2Eh 或者 SysEnter 指令),从而实现从 Ring3 进入 Ring0 层,并且将所要调用的服务号(也就是在 SSDT 数组中的索引值)存放到寄存器 EAX 中,并且将参数地址放到指定的寄存器(EDX)中,再将参数复制到内核地址空间中,再根据存放在 EAX 中的索引值来在 SSDT 数组中调用指定的服务 ~

这些我在从用户模式切换到内核模式的完整过程分析分析已经很详细了,如果忘了请参考这篇博文。

我们同样可以在ntoskrnl.exe导出函数中找到ZwQuerySystemInformation 和 NtQuerySystemInformation ,这里我就不再截图了。

先来反汇编ntoskrnl.exe 中的 ZwQuerySystemInformation:

image

 

在 Ring0 下的 ZwQuerySystemInformation 将 105h 放入了寄存器 eax 中,然后调用了系统服务分发函数 KiSystemService,而这个KiSystemService 函数则是根据 eax 寄存器中的索引值,然后再 SSDT 数组中找到索引值为 eax 寄存器中存放的值得那个 SSDT 项,

最后就是根据这个 SSDT 项中所存放的系统服务的地址来调用这个系统服务了。比如在这里就是调用 KeServiceDescriptorTable[105h ] 处所保存的地址所对应的系统服务了 ~也就是调用 Ring0 下的 NtQuerySystemInformation 了。

至此,在应用层中调用 NtQuerySystemInformation 的全部流程也就结束了。

image

下面我们来验证一下,通过上面所说的索引号在SSDT数组中查找的结构是不是和我们预期的结构一样。

上面我们得到SSDT的基地址为:828b66f0。我们通过基地址和索引号105h求得

Address = 828b66f0 + 4 * 105h = 828B6B04

我们来看看828B6B04这个地址保存的值,然后再反汇编:

image

是不是和我们上面直接反汇编的结构一样!!!

其实 SSDT 就是一个用来保存 Windows 系统服务地址的数组而已 !!!

本文链接:http://www.alonemonkey.com/ssdt-explain-in-detail.html