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>之后使用语句
1 | extern SSDTEntry __declspec ( dllimport ) KeServiceDescriptorTable; |
来获得KeServiceDescriptorTable的地址。
32位系统中KeServiceDescriptorTable结构如下图所示
1 2 3 4 5 6 7 8 9 | #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结构如下:
1 2 3 4 5 6 7 8 9 | #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函数的地址。
1 | pKiSystemCall64=( PVOID )__readmsr(0xc0000082); |
获得地址后遍历该地址后的数据会经过KiSystemServiceRepeat函数,就可以通过特征码4c 8d 15找到目标指令。
具体计算公式是:
特征码起始地址是pKiSystemCall64+i则(i表示相对KiSystemCall64函数开始处偏移i个字节)
1 2 3 | 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语言驱动程序示例代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 | 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; } |
作者:J坚持C
欢迎任何形式的转载,但请务必注明出处。
限于本人水平,如果文章和代码有表述不当之处,还请不吝赐教。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步