关于SSDT的详解(1)
引子
2006年,中国互联网上的斗争硝烟弥漫。这时的战场上,先前颇为流行的窗口挂钩、API挂钩、进程注入等技术已然成为昨日黄花,大有逐渐淡出之势;取而代之的,则是更狠毒、更为赤裸裸的词汇:驱动、隐藏进程、Rootkit……
前不久,我不经意翻出自己2005年9月写下的一篇文章《DLL的远程注入技术》,在下面看到了一位名叫L4bm0s的网友说这种技术已经过时了。虽然我也曾想过拟出若干辩解之词聊作应对,不过最终还是作罢了——毕竟,拿出些新的、有技术含量的东西才是王道。于是这一次,李马首度从ring3(应用层)的围城跨出,一跃而投身于ring0(内核层)这一更广阔的天地,便有了这篇《城里城外看SSDT》。——顾名思义,城里和城外的这一墙之隔,就是ring3与ring0的分界。
在这篇文章里,我会用到太多杂七杂八的东西,比如汇编,比如内核调试器,比如DDK。这诚然是一件令我瞻前顾后畏首畏尾的事情——一方面在ring0我不得不依靠这些东西,另一方面我实在担心它们会导致我这篇文章的阅读门槛过高。所以,我决定尽可能少地涉及驱动、内核与DDK,也不会对诸如如何使用内核调试器等问题作任何讲解——你只需要知道我大概在做些什么,这就足够了。
什么是SSDT?
什么是SSDT?自然,这个是我必须回答的问题。不过在此之前,请你打开命令行(cmd.exe)窗口,并输入“dir”并回车——好了,列出了当前目录下的所有文件和子目录。
那么,以程序员的视角来看,整个过程应该是这样的:
- 由用户输入dir命令。
- cmd.exe获取用户输入的dir命令,在内部调用对应的Win32 API函数FindFirstFile、FindNextFile和FindClose,获取当前目录下的文件和子目录。
- cmd.exe将文件名和子目录输出至控制台窗口,也就是返回给用户。
到此为止我们可以看到,cmd.exe扮演了一个非常至关重要的角色,也就是用户与Win32 API的交互。——你大概已经可以猜到,我下面要说到的SSDT亦必将扮演这个角色,这实在是一点新意都没有。
没错,你猜对了。SSDT的全称是System Services Descriptor Table,系统服务描述符表。这个表就是一个把ring3的Win32 API和ring0的内核API联系起来的角色,下面我将以API函数OpenProcess为例说明这个联系的过程。
你可以用任何反汇编工具来打开你的kernel32.dll,然后你会发现在OpenProcess中有类似这样的汇编代码:
汇编代码
- call ds:NtOpenProcess
这就是说,OpenProcess调用了ntdll.dll的NtOpenProcess函数。那么继续反汇编之,你会发现ntdll.dll中的这个函数很短:
汇编代码
- mov eax, 7Ah
- mov edx, 7FFE0300h
- call dword ptr [edx]
- retn 10h
另外,call的一句实质是调用了KiFastSystemCall:
C++代码
- mov edx, esp
- sysenter
上面是我的XP Professional sp2中ntdll.dll的反汇编结果,如果你用的是2000系统,那么可能是这个样子:
C++代码
- mov eax, 6Ah
- lea edx, [esp+4]
- int 2Eh
- retn 10h
虽然它们存在着些许不同,但都可以这么来概括:
- 把一个数放入eax(XP是0x7A,2000是0x6A),这个数值称作系统的服务号。
- 把参数堆栈指针(esp+4)放入edx。
- sysenter或int 2Eh。
好了,你在ring3能看到的东西就到此为止了。事实上,在ntdll.dll中的这些函数可以称作真正的NT系统服务的存根(Stub)函数。分隔ring3与ring0城里城外的这一道叹息之墙,也正是由它们打通的。接下来SSDT就要出场了,come some music。
站在城墙看城外
插一句先,貌似到现在为止我仍然没有讲出来SSDT是个什么东西,真正可以算是“犹抱琵琶半遮面”了。——书接上文,在你调用sysenter或int 2Eh之后,Windows系统将会捕获你的这个调用,然后进入ring0层,并调用内核服务函数NtOpenProcess,这个过程如下图所示。
SSDT在这个过程中所扮演的角色是至关重要的。让我们先看一看它的结构,如下图。
当程序的处理流程进入ring0之后,系统会根据服务号(eax)在SSDT这个系统服务描述符表中查找对应的表项,这个找到的表项就是系统服务函数NtOpenProcess的真正地址。之后,系统会根据这个地址调用相应的系统服务函数,并把结果返回给ntdll.dll中的NtOpenProcess。图中的“SSDT”所示即为系统服务描述符表的各个表项;右侧的“ntoskrnl.exe”则为Windows系统内核服务进程(ntoskrnl即为NT OS KerneL的缩写),它提供了相对应的各个系统服务函数。ntoskrnl.exe这个文件位于Windows的system32目录下,有兴趣的朋友可以反汇编一下。
附带说两点。根据你处理器的不同,系统内核服务进程可能也是不一样的。真正运行于系统上的内核服务进程可能还有ntkrnlmp.exe、ntkrnlpa.exe这样的情况——不过为了统一起见,下文仍统称这个进程为ntoskrnl.exe。另外,SSDT中的各个表项也未必会全部指向ntoskrnl.exe中的服务函数,因为你机器上的杀毒监控或其它驱动程序可能会改写SSDT中的某些表项——这也就是所谓的“挂钩SSDT”——以达到它们的“主动防御”式杀毒方式或其它的特定目的。
KeServiceDescriptorTable
事实上,SSDT并不仅仅只包含一个庞大的地址索引表,它还包含着一些其它有用的信息,诸如地址索引的基地址、服务函数个数等等。ntoskrnl.exe中的一个导出项KeServiceDescriptorTable即是SSDT的真身,亦即它在内核中的数据实体。SSDT的数据结构定义如下:
C++代码
- 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输出
下面,让我们开看看这个结构里边到底有什么。打开内核调试器(以kd为例),输入命令显示KeServiceDescriptorTable,如下。- lkd> dd KeServiceDescriptorTable l4
- 8055ab80 804e3d20 00000000 0000011c 804d9f48
接下来,亦可根据基地址与服务总数来查看整个服务表的各项:
WinDbg输出- lkd> dd 804e3d20 l11c
- 804e3d20 80587691 f84317aa f84317b4 f84317be
- 804e3d30 f84317c8 f84317d2 f84317dc f84317e6
- 804e3d40 8057741c f84317fa f8431804 f843180e
- 804e3d50 f8431818 f8431822 f843182c f8431836
- ...
你获得的结果可能和我会有不同——我指的是那堆以十六进制f开头的地址项,因为我的SSDT被System Safety Monitor接管了,没留下几个原生的ntoskrnl.exe表项。
C++代码
现在是写些代码的时候了。KeServiceDescriptorTable及SSDT各个表项的读取只能在ring0层完成,于是这里我使用了内核驱动并借助DeviceIoControl来完成。其中DeviceIoControl的分发代码实现如下面的代码所示,没有什么技术含量,所以不再解释。- switch ( IoControlCode )
- {
- case IOCTL_GETSSDT:
- {
- __try
- {
- ProbeForWrite( OutputBuffer, sizeof( SSDT ), sizeof( ULONG ) );
- RtlCopyMemory( OutputBuffer, KeServiceDescriptorTable, sizeof( SSDT ) );
- }
- __except ( EXCEPTION_EXECUTE_HANDLER )
- {
- IoStatus->Status = GetExceptionCode();
- }
- }
- break;
- case IOCTL_GETPROC:
- {
- ULONG uIndex = 0;
- PULONG pBase = NULL;
- __try
- {
- ProbeForRead( InputBuffer, sizeof( ULONG ), sizeof( ULONG ) );
- ProbeForWrite( OutputBuffer, sizeof( ULONG ), sizeof( ULONG ) );
- }
- __except( EXCEPTION_EXECUTE_HANDLER )
- {
- IoStatus->Status = GetExceptionCode();
- break;
- }
- uIndex = *(PULONG)InputBuffer;
- if ( KeServiceDescriptorTable->ulNumberOfServices <= uIndex )
- {
- IoStatus->Status = STATUS_INVALID_PARAMETER;
- break;
- }
- pBase = KeServiceDescriptorTable->pvSSDTBase;
- *((PULONG)OutputBuffer) = *( pBase + uIndex );
- }
- break;
- // ...
- }
补充一下,再。DDK的头文件中有一件很遗憾的事情,那就是其中并未声明KeServiceDescriptorTable,不过我们可以自己手动添加之:
C++代码- extern PSSDT KeServiceDescriptorTable;
——当然,如果你对DDK开发实在不感兴趣的话,亦可以直接使用配套代码压缩包中的SSDTDump.sys,并使用DeviceIoControl发送IOCTL_GETSSDT和IOCTL_GETPROC控制码即可;或者,直接调用我为你准备好的两个函数:
C++代码- BOOL GetSSDT( IN HANDLE hDriver, OUT PSSDT buf );
- BOOL GetProc( IN HANDLE hDriver, IN ULONG ulIndex, OUT PULONG buf );