SSDT表介绍
ntdll.dll模块中的函数有些以nt或zw开头的函数为了完成相应的功能需要进入内核,调用内核中以nt开头的函数来完成相应的功能。ntdll.dll里的函数在进入内核层之前首先将系统服务号传入eax寄存器中,然后调用KiSystemService函数进入内核层。进入内核后会根据eax值索引ssdt表里的函数进行执行相应地址的函数。
SSDT的每一项是一个系统服务函数的地址,可以通过HOOK这些函数完成特定的功能。
32位系统上SSDT是导出的,64位是不会导出的。
通过PCHunter查看win7 x64系统的SSDT表:
如何获得SSDT表的地址和每一个项对应的服务名称呢?
注意:内核文件有多个,操作系统会根据当前cpu和分页方式选择不同的内核文件。
ntoskrnl.exe - 单处理器,不支持PAE分页模式;
ntkrnlpa.exe - 单处理器,支持PAE分页模式;
ntkrnlmp.exe - 多处理器,不支持PAE分页模式;
ntkrpamp.exe - 多处理器,支持PAE分页模式。
32位系统
32系统上ntdll.dll使用mov eax,xxx传入索引值,可以通过遍历ntdll.dll查看每一个函数对应的服务号,从而找到服务函数名和服务编号的关系。另外,内核中32位的SSDT的起始地址是直接在ntoskrnl.exe中通过KeServiceDescriptorTable符号导出,不需要使用工具来获得,可以直接在驱动程序中引用该符号的地址。注意:在代码实现上应当引入头文件#include <ntimage.h>之后使用语句
extern SSDTEntry __declspec(dllimport) KeServiceDescriptorTable;
来获得KeServiceDescriptorTable的地址。
32位系统中KeServiceDescriptorTable结构如下图所示
#pragma pack(1)
typedef struct _SERVICE_DESCRIPTOR_TABLE
{
PULONG ServiceTableBase;//SSDT的起始地址
PULONG ServiceCounterTableBase;//
ULONG NumberOfService;//SSDT表中服务函数的总数
PUCHAR ParamTableBase;//服务函数的参数个数数组的起始地址,数组的每一个成员占1字节,记录的值是对应函数的参数个数*4
} SSDTEntry, *PSSDTEntry;
#pragma pack()
ServiceTableBase的内容是SSDT表的起始地址,然后从ServiceTableBase开始是一个长度为NumberOfService的指针数组,每一项是4个字节,是SSDT表中每一个服务的函数地址。
在内核调试器windbg中使用dd KeServiceDescriptorTable命令查看KeServiceDescriptorTable数据,就可以看到SSDTEntry结构的每一项数据。
64位系统
64位系统上ntdll.dll使用 mov r10,rcx;mov eax,xxx传入索引值,可以通过遍历64位的ntdll.dll查看每一个函数对应的服务号。从而找到服务函数名和服务编号的关系。
如下图所示,使用IDA查看windows 7 x64的64位ntdll.dll的函数
通过SSDT表可以看出二者却是相互对应(这是因为,系统就是通过ntdll.dll来逆推出SSDT表的每一项对应的函数名的)
但是,由于64位的内核文件并未导出KeServiceDescriptorTable的信息,所以64位系统的SSDT表的起始地址无法直接获得。但是却同样可以在windbg中使用dd KeServiceDescriptorTable命令查看KeServiceDescriptorTable内容:
但是,我们却无法使用某种方式直接获得KeServiceDescriptorTable的地址,于是常采用间接方式。
使用windbg观察nt!KiSystemServiceRepeat函数的反汇编如下
可以发现,x64系统会在KiSystemServiceRepeat函数使用lea r10,[nt!KeServiceDescriptorTable]指令获得KeServiceDescriptorTable的地址,因此只要在KiSystemServiceRepeat函数内搜索4c 8d 15 得到这条指令的起始地址。
KeServiceDescriptorTable地址保存在这条指令之后的地址即fffff800`03e98779+指令操作数(是偏移量)得到的新地址中(指令反汇编的知识,用四个字节作为偏移量),即
pKeServiceDescriptorTable= fffff800`03e98779+2320c7= FFFFF800`040CA840
通过windbg验证KeServiceDescriptorTable的地址
而KeServiceDescriptorTable结构如下:
#pragma pack(1)
typedef struct _SERVICE_DESCIPTOR_TABLE
{
PULONG ServiceTableBase; // SSDT基址,8字节大小
PVOID ServiceCounterTableBase; // SSDT中服务被调用次数计数器,8字节大小
ULONGLONG NumberOfService; // SSDT服务函数的个数,8字节大小
PVOID ParamTableBase; // 系统服务参数表基址,8字节大小。实际指向的数组是以字节为单位的记录着对应服务函数的参数个数
}SSDTEntry, *PSSDTEntry;
#pragma pack()
通过以上信息,可以看出SSDT的首地址是fffff800`03e9a300,SSDT中以每4个字节为单位描述一个服务的地址项信息,不过其内容并非实际的地址,因为4个字节无法保存64位下的地址,实际上其内容左移4位得到的是地址相对偏移量,是真实服务地址相对SSDT起始地址的偏移量。下面的内容可以帮助理解真实服务地址的计算过程。
通过windbg查看fffff800`03e9a300的数据
通过以上信息,验证第一项和第二个项指向的地址,SSDT的首地址是fffff800`03e9a300:
fffff800`03e9a300+040d9a00>>4= fffff800`03e9a300+040d9a0= FFFFF800`042A7C0
fffff800`03e9a300+02f55c00>>4= fffff800`03e9a300+02f55c0= FFFFF800`0418F8C0
很明显这与PCHunter显示的一致
上面的操作需要直到KiSystemServiceRepeat地址,然而KiSystemServiceRepeat函数也没有导出。所以无法知道其地址。
如何获得64位Windows系统的SSDT表的基址
方式一:硬编码,对每一个系统的不同版本,分别使用windbg工具获得KiSystemServiceRepeat函数的地址。
方式二:读取msr寄存器(特别模块寄存器)的0xc0000082的值,即在微软的C语言中使用__readmsr(0xc0000082)获得KiSystemCall64函数的地址。
pKiSystemCall64=(PVOID)__readmsr(0xc0000082);
获得地址后遍历该地址后的数据会经过KiSystemServiceRepeat函数,就可以通过特征码4c 8d 15找到目标指令。
具体计算公式是:
特征码起始地址是pKiSystemCall64+i则(i表示相对KiSystemCall64函数开始处偏移i个字节)
pKeServiceDescriptorTable=(PVOID)(pKiSystemCall64+i+7+*(PLONG)( pKiSystemCall64+i+3));
pSSDT=*(PLONG)pKeServiceDescriptorTable
注意:上述代码中数字7表示lea r10,[xxx]指令的长度是7个字节
方式三:解析本操作系统下的内核文件的符号文件。