SSDT 的全称是 System Services Descriptor Table系统服务描写叙述符表

这个表就是一个把 Ring3 的 Win32 API 和 Ring0 的内核 API 联系起来。Ring3下调用的全部函数终于都会先进入到ntdll里面的,比方ReadFile。就会进入ntdllZwReadFile


SSDT 并不只只包括一个庞大的地址索引表,它还包括着一些其他实用的信息,诸如地址索引的基地址、服务函数个数等。

1. //系统服务描写叙述符表-ntoskrnl.exe中导出KeServiceDescriptorTable这个表  

2. #pragma pack(1)  

3. typedef struct _ServiceDescriptorTable  

4. {  

5.     //System Service Dispatch Table的基地址  

6.     PVOID ServiceTableBase;        

7.     //SSDT中每一个服务被调用次数的计数器。

这个计数器一般由sysenter 更新。  

8.     PVOID ServiceCounterTable;   

9.     // ServiceTableBase 描写叙述的服务数目。  

10.     unsigned int NumberOfServices;    

11.     //每一个系统服务參数字节数表的基地址-系统服务參数表SSPT   

12.     PVOID ParamTableBase;   

13. }*PServiceDescriptorTable;    

14. #pragma pack()  


通过改动此表的函数地址能够对经常使用 Windows 函数及 API 进行 Hook,从而实现对一些关心的系统动作进行过滤、监控的目的。

ZwOpenProcessZwLoadDriver。一些 HIPS、防毒软件、系统监控、注冊表监控软件往往会採用此接口来实现自己的监控模块。


在 NT 4.0 以上的 Windows 操作系统中(windows2000),默认就存在两个系统服务描写叙述表,这两个调度表相应了两类不同的系统服务,这两个调度表为:

SSDTKeServiceDescriptorTable

 ShadowSSDT:KeServiceDescriptorTableShadow


KeServiceDescriptorTable 主要是处理来自 Ring3 的 Kernel32.dll 中的系统调用

比方函数 OpenProcessReadFile 等函数。

kernel32.dll--->ntdll.dll--->进入内核 ntoskrnl.exe(有些机器可能不是这个名字)


KeServiceDescriptorTableShadow 则主要处理来自 User32.dll 和 GDI32.dll 中的系统调用

比方常见的PostMessageSendMessageFindWindowWin32k.sys等。


非常多人一定非常奇怪。为什么系统中有非常多内核文件 ntoskrnl.exe ntkrnlpa.exe。简单来说就是他们都是同一套源码依据编译选项的不同而编译出四个可运行文件,分别用于: 

ntoskrnl - 单处理器,不支持PAE(物理地址扩展)

ntkrnlpa - 单处理器。支持PAE 

ntkrnlmp - 多处理器。不支持PAE 

ntkrpamp - 多处理器。支持PAE 


Vista之前,安装程序会在安装时依据系统的配置选择两个多处理器或者两个单处理器的版本号拷贝到目标系统 system32

Vista開始以后,会统一使用多处理器版本号。由于多处理器版本号执行在单处理器上仅仅是效率略微低一些。


SSDT表已经导出了。通过ntoskrnl.exe的导出表能够查看到。既然KeServiceDescriptorTable是一个导出的全局变量(数组)。那么我们来看wrk。大家都知道在编写代码的时候,要导出一个函数,通常使用def文件。所以ntoskrnl在编写的时候,相同也用到了def来导出导出文件是ntosx86.def,我们翻看wrk


*********** ntosx86.def-->导出了 KeServiceDescriptorTable CONSTANT ***********


有了上面的介绍后,我们能够简单的将 KeServiceDescriptor 看做是一个数组了(实质也就是个数组)。在应用层 ntdll.dll 中的 API 在这个系统服务描写叙述表(SSDT)中都存在一个与之相相应的服务.


Ntdll ZwReadFile  111h  

Ntos mov  eax,       111h


当我们的应用程序调用 ntdll.dll 中的 API 时,终于会调用内核中与之相相应的系统服务。因为有了 SSDT,所以我们仅仅须要告诉内核须要调用的服务所在 SSDT 中的索引就 OK 了。然后内核依据这个索引值就能够在 SSDT 中找到相相应的服务了,然后再由内核调用服务完毕应用程序 API 的调用请求就可以。



SSDT表概念具体解释_数组


ntdllNtQuerySystemInformationZwQuerySystemInformation 的开头尽管是ntzw两套函数。事实上是一样的

我们看IDA。我们先看Nt*系列的函数地址:

.text:77F061F8 _NtQuerySystemInformation@16


在ntdll中,zwnt的两套函数事实上他们都是同一个主体

.text:77F061F8 mov   eax, 105h       ; NtQuerySystemInformation

.text:77F061F8         ; RtlGetNativeSystemInformation

.text:77F061FD mov   edx, 7FFE0300h

.text:77F06202 call    dword ptr [edx]

.text:77F06204 retn    10h


然后再对照图片:

SSDT表概念具体解释_寄存器_02


Mode检查 是 usermode 还是kernelmode

众所周知 Ntdll.dll 中的 API 都仅仅只是是一个简单的包装函数而已,当 Kernel32.dll 中的 API 通过 Ntdll.dll (比方:ReadFile --->ZwReadFile),会完毕參数的检查。再调用一个中断(int 2Eh 或者 SysEnter 指令)。从而实现从 Ring3 进入 Ring0 层,而且将所要调用的服务号(也就是在 SSDT 数组中的索引值)存放到寄存器 EAX  mov  eax, 105h(比方看IDA。然后对照xuetr是否一致:结果吻合)。而且将參数地址放到指定的寄存器 EDX 中( mov  edx, 7FFE0300h),再将參数拷贝到内核地址空间中,再依据存放在 EAX 中的索引值来在 SSDT 数组中调用指定的服务


我们来看内核下这个函数:

windbg的命令 nt!ZwQuerySystemInformation


 nt!ZwQuerySystemInformation:
804ffb1c b8ad000000      mov     eax,0ADh
804ffb21 8d542404          lea     edx,[esp+4]
804ffb25 9c                        pushfd
804ffb26 6a08                   push    8
804ffb28 e854e90300     call    nt!KeReleaseInStackQueuedSpinLockFromDpcLevel+0x95d (8053e481)
804ffb2d c21000               ret     10h
804ffb30 b8ae000000     mov     eax,0AEh
804ffb35 8d542404          lea     edx,[esp+4]


能够看到在 Ring0 下的 ZwQuerySystemInformation 将 105h 放入了寄存器 eax 中。

lkd> ZwQuerySystemInformation


nt!ZwQuerySystemInformation:

84456c38 b805010000      mov  eax,105h     //将 105h 放入了寄存器eax

84456c3d 8d542404          lea    edx,[esp+4]

84456c41 9c                         pushfd

84456c42 6a08                    push   8

84456c44 e835140000      call    nt!KiSystemService (8445807e)

84456c49 c21000                 ret     10h


然后调用了系统服务分发函数 KiSystemService。而这个 KiSystemService 函数则是依据 eax 寄存器中的索引值,然后再到SSDT 数组中找到索引值为eax 寄存器中存放的值的那个 SSDT 项。最后就是依据这个 SSDT 项中所存放的系统服务的地址来调用这个系统函数了。比方在这里就是调用 KeServiceDescriptorTable[105h] 处所保存的地址所相应的系统服务了也就是调用 Ring0 下的 NtQuerySystemInformation


说明一下内核中 ZwNt两套函数的差别

lkd> u ZwQuerySystemInformation


nt!ZwQuerySystemInformation:

84456c38 b805010000      mov    eax,105h //将 105h 放入了寄存器 eax 

84456c3d 8d542404        lea     edx,[esp+4]

84456c41 9c              pushfd

84456c42 6a08            push    8

84456c44 e835140000      call    nt!KiSystemService (8445807e)

84456c49 c21000          ret     10h


lkd> u NtQuerySystemInformation  l 10

nt!NtQuerySystemInformation:

8464ae3e 8bff             mov    edi,edi

8464ae40 55              push    ebp

8464ae41 8bec            mov     ebp,esp

8464ae43 8b5508          mov     edx,dword ptr [ebp+8]

8464ae46 83fa53          cmp     edx,53h

8464ae49 7f21            jg      nt!NtQuerySystemInformation+0x2e (8464ae6c)

8464ae4b 7440            je      nt!NtQuerySystemInformation+0x4f (8464ae8d)

主体,就是nt系列函数。


所以结论就是:Zw系列函数仅仅是类似一个过渡而Nt系列函数才是真正的运行主体

至此,在应用层中调用 NtQuerySystemInformation 的所有流程也就结束了 ~



Ring3!ZwQuerySystemInformation 或者 NtQuerySystemInformation 

进入内核

Ntos 105h ntos!ZwQuerySystemInformation 

接着通过ssdt索引,找到

ntos!NtQuerySystemInformation 运行主体。


说了那么多理论知识,我们windbg来看下SSDT表的结构:


lkd> dd KeServiceDescriptorTable

84583b00  84498d5c 00000000 00000191 844993a4


84498d5c 就是SSDT表的起始地址

00000191 就是SSDT表的个数 unsigned int NumberOfServices //这个成员就是个数


lkd> dd 84498d5c

84498d5c  84693e78 844db3ad 84623c60 8443f8ba

84498d6c  8469574f 84518306 84705f53 84705f9c

84498d7c  846184af 8471f7c2 84720a17 8460ec87

84498d8c  8469fd8d 846f8ca9 8464bbc0 8461b7c4

84498d9c  845b19ae 846eab84 84602240 84644bcc

84498dac  84691041 845f22bc 8469044e 8460fcfe

84498dbc  846a1814 84612381 846a15f4 84699d4c

84498dcc  846241e8 846e5927 84697119 846a1a46

这些是nt函数的主体:


lkd> u  84693e78

nt!NtAcceptConnectPort:

84693e78 8bff            mov     edi,edi

84693e7a 55              push    ebp

84693e7b 8bec            mov     ebp,esp

84693e7d 64a124010000    mov     eax,dword ptr fs:[00000124h]

84693e83 66ff8884000000   dec     word ptr [eax+84h]

84693e8a 56              push    esi

84693e8b 57              push    edi

84693e8c 6a01            push    1


所谓主体,就是真正的汇编运行代码而不是直接的过渡代码。


++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

首地址 84498d5c ring0 服务号 105h


[Address] = SSDT首地址 +  4 * 索引號


ntos!NtQuerySystemInformation = 84498d5c + 4 * 105h = [84499170h]


[84499170h] = 8464ae3eh


lkd> u 8464ae3e

nt!NtQuerySystemInformation:

8464ae3e 8bff            mov     edi,edi

8464ae40 55             push    ebp

8464ae41 8bec            mov     ebp,esp

8464ae43 8b5508          mov     edx,dword ptr [ebp+8]

8464ae46 83fa53          cmp     edx,53h

8464ae49 7f21            jg      nt!NtQuerySystemInformation+0x2e (8464ae6c)

8464ae4b 7440            je      nt!NtQuerySystemInformation+0x4f (8464ae8d)

8464ae4d 83fa08          cmp     edx,8

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++



以下也提供一份简单的SSDT表的遍历代码:


#include <ntifs.h>

typedef struct _SERVICE_DESCRIPTOR_TABLE {
/*
* Table containing cServices elements of pointers to service handler
* functions, indexed by service ID.
*/
PULONG ServiceTable;
/*
* Table that counts how many times each service is used. This table
* is only updated in checked builds.
*/
PULONG CounterTable;
/*
* Number of services contained in this table.
*/
ULONG TableSize;
/*
* Table containing the number of bytes of parameters the handler
* function takes.
*/
PUCHAR ArgumentTable;
} SERVICE_DESCRIPTOR_TABLE, *PSERVICE_DESCRIPTOR_TABLE;

//ssdt表已经导出了。这里例行公事下
extern PSERVICE_DESCRIPTOR_TABLE KeServiceDescriptorTable;

//卸载函数
VOID DriverUnload(IN PDRIVER_OBJECT DriverObject)
{
DbgPrint("卸载完毕!\n");
}

//入口函数
NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject,PUNICODE_STRING RegistryPath)
{
int i = 0;

DriverObject->DriverUnload = DriverUnload;

for (i=0;i<KeServiceDescriptorTable->TableSize;i++)
{
DbgPrint("Number:%d Address:0x%08X\r\n\r\n",i, KeServiceDescriptorTable->ServiceTable[i]);
}

return STATUS_SUCCESS;
}