SSDT(系统服务描述符表 system services descriptor table)
SSDT表介绍
ntdll.dll模块中的函数有些以nt或zw开头的函数为了完成相应的功能需要进入内核,调用内核中以nt开头的函数来完成相应的功能。ntdll.dll里的函数在进入内核层之前首先将系统服务号传入eax寄存器中,然后调用KiSystemService函数进入内核层。进入内核后会根据eax值索引ssdt表里的函数进行执行相应地址的函数。
SSDT的每一项是一个系统服务函数的地址,可以通过HOOK这些函数完成特定的功能。
32位系统上SSDT是导出的,64位是不会导出的。
通过PCHunter查看win7 x64系统的SSDT表:
如何获得SSDT表的地址和每一个项对应的服务名称呢?
结论:每一个版本的windows操作系统的系统服务函数的编号都是固定的,例如所有32位的windows 7的系统服务函数的编号都是固定的,无论系统版本如何变化。这主要是因为一旦更新操作系统后,如果系统服务函数的编号发生变化会导致系统不稳定。基于以上事实,我们只需要针对win7和win10定义四份函数表即可。而函数表的获得只需要依靠PC hunter直接导出即可。
下面介绍SSDT表的基本知识
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个字节,还需要注意的是:windows 10的1809版本(10.0.17763.xxx)后的系统的SSDT表的获得又发生了新的变化。因为它通过jmp指令跳转到其它地址后才执行到上述特征指令。
方式三:解析本操作系统下的内核文件的符号文件。
使用VS2017和WDK编写64位C语言驱动程序示例代码
typedef struct _SYSTEM_SERVICE_DESCIPTOR_TABLE_SR //系统描述符表(SSDT)占用32字节 { PULONG ServiceTableBase; //SSDT数组的基址,8字节大小,数组中每个元素占4字节,保存的内容左移4位得到的是地址相对偏移量,是服务函数地址相对SSDT起始地址的偏移量 PVOID ServiceCounterTableBase; //SSDT中服务被调用次数计数器,8字节大小 ULONGLONG NumberOfService; //SSDT服务函数的个数,8字节大小 PVOID ParamTableBase; //系统服务参数表基址,8字节大小。实际指向的数组是以字节为单位的记录着对应服务函数的参数个数 }SYSTEM_SERVICE_DESCIPTOR_TABLE_SR, *PSYSTEM_SERVICE_DESCIPTOR_TABLE_SR; #ifdef DBG #define KDbgPrint DbgPrint //debug生成模式下将输出调试信息 #else #define KDbgPrint(format,...) //Release生成模式下不会输出调试信息 #endif PEPROCESS find_process(IN const char* process_image_file_name, OUT OPTIONAL PDWORD pprocess_pid) { ULONG pid; NTSTATUS status; PEPROCESS peprocess_find,peprocess_ret=NULL; for (pid = 0; pid <= 240000; pid += 4) { status = PsLookupProcessByProcessId((HANDLE)pid, &peprocess_find); if (NT_SUCCESS(status)) { if (strcmp((CHAR*)PsGetProcessImageFileName(peprocess_find), process_image_file_name) == 0) { peprocess_ret = peprocess_find; if(pprocess_pid) *pprocess_pid = pid; } ObDereferenceObject(peprocess_find); } if (peprocess_ret) return peprocess_ret; } return NULL; } PVOID get_system_service_descriptor_table_addr_x64() { PUCHAR start_search_addr = NULL; PUCHAR end_search_addr = NULL; PUCHAR i = NULL; UCHAR b1 = 0, b2 = 0, b3 = 0; LONG tmp_long = 0; ULONGLONG addr = 0; RTL_OSVERSIONINFOW ver = { 0 }; ver.dwOSVersionInfoSize = sizeof(ver); RtlGetVersion(&ver); start_search_addr = (PUCHAR)__readmsr(0XC0000082); if (ver.dwBuildNumber >= 17763)//1809版本(10.0.17763.xxx)以后jmp 过去了 { for (i = start_search_addr; i < start_search_addr + 0x500; i++) { if (MmIsAddressValid(i) && MmIsAddressValid(i + 5)) { b1 = *i; b2 = *(i + 5); if (b1 == 0xe9 && b2 == 0xc3) { memcpy(&tmp_long, i + 1, 4); start_search_addr = i + 5 + tmp_long; break; } } } } for (i = start_search_addr; i < start_search_addr + 0x500; i++) { if (MmIsAddressValid(i) && MmIsAddressValid(i + 2)) { b1 = *i; b2 = *(i + 1); b3 = *(i + 2); if (b1 == 0x4c && b2 == 0x8d && b3 == 0x15) { memcpy(&tmp_long, i + 3, 4); addr = (ULONGLONG)tmp_long + (ULONGLONG)i + 7; break; } } } KDbgPrint("%s SSDT addr=0x%p\n", __FUNCTION__, addr); return (PVOID)addr; } PVOID get_ssdt_fun_addr_by_index_x64(ULONG index) { static PSYSTEM_SERVICE_DESCIPTOR_TABLE_SR pssdt = NULL; if (pssdt == NULL)pssdt = (PSYSTEM_SERVICE_DESCIPTOR_TABLE_SR)get_system_service_descriptor_table_addr_x64(); if (pssdt == NULL) return NULL; PULONG fun_array = pssdt->ServiceTableBase; if (index >= pssdt->NumberOfService) return NULL; LONG tmp = fun_array[index]; tmp = tmp >> 4; PVOID ret = (PVOID)((UINT64)tmp + (UINT64)fun_array); KDbgPrint("%s line=%d fun_index=%d fun_addr=0x%p\n", __FUNCTION__, __LINE__, index, ret); return ret; } PVOID get_shadow_system_service_descriptor_table_addr_x64() { PUCHAR start_search_addr = NULL; PUCHAR end_search_addr = NULL; PUCHAR i = NULL; UCHAR b1 = 0, b2 = 0, b3 = 0; LONG tmp_long = 0; ULONGLONG addr = 0; RTL_OSVERSIONINFOW ver = { 0 }; ver.dwOSVersionInfoSize = sizeof(ver); RtlGetVersion(&ver); start_search_addr = (PUCHAR)__readmsr(0XC0000082); if (ver.dwBuildNumber >= 17763)//1809版本以后 { for (i = start_search_addr; i < start_search_addr + 0x500; i++) { if (MmIsAddressValid(i) && MmIsAddressValid(i + 5)) { b1 = *i; b2 = *(i + 5); if (b1 == 0xe9 && b2 == 0xc3) { memcpy(&tmp_long, i + 1, 4); start_search_addr = i + 5 + tmp_long; break; } } } } for (i = start_search_addr; i < start_search_addr + 0x500; i++) { if (MmIsAddressValid(i) && MmIsAddressValid(i + 2)) { b1 = *i; b2 = *(i + 1); b3 = *(i + 2); if (b1 == 0x4c && b2 == 0x8d && b3 == 0x1d) { memcpy(&tmp_long, i + 3, 4); addr = (ULONGLONG)tmp_long + (ULONGLONG)i + 7; break; } } } KDbgPrint("%s Shadow SSDT addr=0x%p\n", __FUNCTION__, addr); //当前获得了shadow ssdt的地址但是里面保存的是ssdt的描述符,不过没关系,增加一个结构体大小后就是shadow ssdt描述符的地址 return (PVOID)(addr+sizeof(SYSTEM_SERVICE_DESCIPTOR_TABLE_SR)); } PVOID get_shadow_ssdt_fun_addr_by_index_x64(ULONG index) { static PSYSTEM_SERVICE_DESCIPTOR_TABLE_SR pshadow_ssdt = NULL; //win10中不能正确的获得SSSDT表的地址里的函数原因: //win32k.sys的内存地址只有GUI线程才会被映射该内存 //windows中获得的shadow ssdt描述符地址后只有在GUI线程中才会映射真正的数据,否则该地址映射的实际上依然是ssdt描述符表的内容 //因此获得之前应当切换到一个GUI进程中,然后再读取地址内的数据 //切换到目标进程的方法是:首先调用KeStackAttachProcess切换到目标进程,然后再调用KeUnstackDetachProcess返回到当前进程 //当获得了地址后,还需要做函数的地址的转换(如果需要的话),因为对于来自用户层的函数调用实际上会通过jmp跳转到win32kfull.sys中进行后续任务 if (pshadow_ssdt == NULL)pshadow_ssdt = (PSYSTEM_SERVICE_DESCIPTOR_TABLE_SR)get_shadow_system_service_descriptor_table_addr_x64(); PEPROCESS peprocess_csrss=find_process("csrss.exe"); KAPC_STATE apc_old = { 0 ,}; KeStackAttachProcess(peprocess_csrss, &apc_old); if (pshadow_ssdt == NULL) return NULL; PULONG fun_array = pshadow_ssdt->ServiceTableBase; if (index >= pshadow_ssdt->NumberOfService) return NULL; LONG tmp = fun_array[index]; tmp = tmp >> 4; PVOID ret = (PVOID)((UINT64)tmp + (UINT64)fun_array); KeUnstackDetachProcess(&apc_old); KDbgPrint("%s line=%d fun_index=%d fun_addr=0x%p\n", __FUNCTION__,__LINE__, index, ret); return ret; }