Loading... ##### 背景 SSDT全称为System Service Descriptor Table,即系统服务描述符表。SSDT表的作用就是把ring3的win32API函数和ring0的内核API函数联系起来。对于ring3下的一些API,最终会对应于ntdll.dll里一个Ntxxx函数,例如CreateFile,最终调用到ntdll.dll里NtCreateFile这个函数。 NtCreateFile最终将系统服务号放入EAX,然后CALL系统的服务分发函数KiSystemService,进入到内核中。从ring3到ring0,最终在ring0当中通过传入的EAX得到对应的同名系统服务的内核地址,这样就完成了以此系统服务的调用。SSDT并不仅仅只包含一个邦达的地址索引表,它还包含着一些其它有用的信息,注入地址索引的基地址、服务函数个数等。 SSDT通过修改此表的函数u地址可以对常用Windows函数进行HOOK,从而实现对一些核心的系统动作进行过滤、监控的目的。一些HIPS、防毒软件、系统监控、注册表监控软件往往会采用词接口来实现自己的监控模块。 本质上,其实SSDT就是一个用来保存windows系统服务地址的数组而已。 32位系统和64位系统上,获取SSDT表的方式并不相同,获取SSDT表中的函数地址也不相同。 我们说SSDT表就是一个把ring3的win32API和ring0的内核API联系起来的角色,下面以OpenProcess为例说明这个联系的过程。 用IDA打开kernelbase.dll,找到openProcess,里边有类似这样的汇编代码`call cs:NtOpenProcess`: ![image.png](http://47.117.131.13/usr/uploads/2021/10/3795647321.png) 这就是说,openProcess调用了ntdll.dll的NtOpenProcess函数。继续查看ntdll.dll的NtOpenProcess: ![image.png](http://47.117.131.13/usr/uploads/2021/10/4212518374.png) 查看ntdll.dll中的KiFastSystemCall函数: ![image.png](http://47.117.131.13/usr/uploads/2021/10/2720269030.png) 概括以上调用过程:把一个数放入eax,这个数值称作系统的服务号;sysenter或int 2Eh进入内核。 在ring3能看到的东西就到此未知了。事实上,在ntdll.dll中的这些函数可以称作真正的NT系统服务的存根(stub)函数。接下来SSDT就要出场了。 当程序的处理流程进入ring0之后,系统会根据服务号(eax)在SSDT这个系统服务描述表中查找对应的表项,这个找到的表项就是系统服务函数NtOpenProcess的真正地址。之后,系统会根据这个地址调用相应的系统服务函数,并把结果返回给ntdll.dll中的NtOpenProcess。 ##### 实现原理 ###### 获取SSDT表的地址 在32位系统中,SSDT表是内核Ntoskrnl.exe导出的一张表,导出符号位KeServiceDescriptorTable,该表含有一个指针指向SSDT中包含Ntoskrnl.exe实现的核心服务。所以,我们想在32位系统上获取SSDT表地址,直接获取Ntoskrnl.exe导出符号KeServiceDescriptorTable即可。 SSDT表结构为: ``` #pragma pack(1) typedef struct _SERVICE_DESCIPTOR_TABLE { PULONG ServiceTableBase;//SSDT基址 PULONG ServiceCounterTableBase;//SSDT中服务被调用次数计数器 ULONG NumberOfService;//SSDT服务个数 PUCHAR ParamTableBase;//系统服务参数表基址 }SSDTEntry,*PSSDTEntry; #pragma pack() ``` 所以,从Ntoskrnl.exe获取导出符号KeServiceDescriptorTable的代码如下: `extern SSDTEntry __declspec(dllimport) KeServiceDescriptorTable;` ##### 获取SSDT表函数地址 在32位系统中,SSDT包含了所有内核导出函数的地址。每个地址长度为4字节。所以要获得SSDT中某个函数的地址,如下代码所示: ``` KeServiceDescriptorTable.ServiceTableBase + SSDT函数索引号*4 //或者 KeServiceDescriptorTable.ServiceTableBase[SSDT函数索引号] ``` SSDT函数索引号可以从ntdll.dll文件中获取,当ring3级API函数最终进入ring0级的时候,它会先将SSDT函数索引号mov给eax寄存器。所以,我们获取ntdll.dll导出函数的地址,从中获取SSDT函数索引号 ##### 编程实现 **实现过程:** 1. 获取SSDT函数索引号; 2. 获取SSDT表地址; 3. 获取SSDT函数地址. ###### 获取SSDT函数索引号 使用**内存映射文件技术**,将磁盘上的ntdll.dll文件映射到内核内存空间中,并从导出表中获取导出函数地址,然后获取SSDT函数索引号。 * InitiallizeObjectAttributes:初始化文件对象; * ZwOpenFile * ZwCreateSection:生成一个内存映射节; * ZwMapViewOfSection:把文件的某个区域或者整个区域按照已设置的访问权限和映射方式映射到内存中; * ZwMapViewOfSection:解除文件映射. ``` // 从 ntdll.dll 中获取 SSDT 函数索引号 ULONG GetSSDTFunctionIndex(UNICODE_STRING ustrDllFileName, PCHAR pszFunctionName) { ULONG ulFunctionIndex = 0; NTSTATUS status = STATUS_SUCCESS; HANDLE hFile = NULL; HANDLE hSection = NULL; PVOID pBaseAddress = NULL; // 内存映射文件 status = DllFileMap(ustrDllFileName, &hFile, &hSection, &pBaseAddress); if (!NT_SUCCESS(status)) { KdPrint(("DllFileMap Error!\n")); return ulFunctionIndex; } // 根据导出表获取导出函数地址, 从而获取 SSDT 函数索引号 ulFunctionIndex = GetIndexFromExportTable(pBaseAddress, pszFunctionName); // 释放 ZwUnmapViewOfSection(NtCurrentProcess(), pBaseAddress); ZwClose(hSection); ZwClose(hFile); return ulFunctionIndex; } // 内存映射文件 NTSTATUS DllFileMap(UNICODE_STRING ustrDllFileName, HANDLE *phFile, HANDLE *phSection, PVOID *ppBaseAddress) { NTSTATUS status = STATUS_SUCCESS; HANDLE hFile = NULL; HANDLE hSection = NULL; OBJECT_ATTRIBUTES objectAttributes = { 0 }; IO_STATUS_BLOCK iosb = { 0 }; PVOID pBaseAddress = NULL; SIZE_T viewSize = 0; // 打开 DLL 文件, 并获取文件句柄 InitializeObjectAttributes(&objectAttributes, &ustrDllFileName, OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE, NULL, NULL); status = ZwOpenFile(&hFile, GENERIC_READ, &objectAttributes, &iosb, FILE_SHARE_READ, FILE_SYNCHRONOUS_IO_NONALERT); if (!NT_SUCCESS(status)) { KdPrint(("ZwOpenFile Error! [error code: 0x%X]", status)); return status; } // 创建一个节对象, 以 PE 结构中的 SectionALignment 大小对齐映射文件 status = ZwCreateSection(&hSection, SECTION_MAP_READ | SECTION_MAP_WRITE, NULL, 0, PAGE_READWRITE, 0x1000000, hFile); if (!NT_SUCCESS(status)) { ZwClose(hFile); KdPrint(("ZwCreateSection Error! [error code: 0x%X]", status)); return status; } // 映射到内存 status = ZwMapViewOfSection(hSection, NtCurrentProcess(), &pBaseAddress, 0, 1024, 0, &viewSize, ViewShare, MEM_TOP_DOWN, PAGE_READWRITE); if (!NT_SUCCESS(status)) { ZwClose(hSection); ZwClose(hFile); KdPrint(("ZwMapViewOfSection Error! [error code: 0x%X]", status)); return status; } // 返回数据 *phFile = hFile; *phSection = hSection; *ppBaseAddress = pBaseAddress; return status; } // 根据导出表获取导出函数地址, 从而获取 SSDT 函数索引号 ULONG GetIndexFromExportTable(PVOID pBaseAddress, PCHAR pszFunctionName) { ULONG ulFunctionIndex = 0; // Dos Header PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)pBaseAddress; // NT Header PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((PUCHAR)pDosHeader + pDosHeader->e_lfanew); // Export Table PIMAGE_EXPORT_DIRECTORY pExportTable = (PIMAGE_EXPORT_DIRECTORY)((PUCHAR)pDosHeader + pNtHeaders->OptionalHeader.DataDirectory[0].VirtualAddress); // 有名称的导出函数个数 ULONG ulNumberOfNames = pExportTable->NumberOfNames; // 导出函数名称地址表 PULONG lpNameArray = (PULONG)((PUCHAR)pDosHeader + pExportTable->AddressOfNames); PCHAR lpName = NULL; // 开始遍历导出表 for (ULONG i = 0; i < ulNumberOfNames; i++) { lpName = (PCHAR)((PUCHAR)pDosHeader + lpNameArray[i]); // 判断是否查找的函数 if (0 == _strnicmp(pszFunctionName, lpName, strlen(pszFunctionName))) { // 获取导出函数地址 USHORT uHint = *(USHORT *)((PUCHAR)pDosHeader + pExportTable->AddressOfNameOrdinals + 2 * i); ULONG ulFuncAddr = *(PULONG)((PUCHAR)pDosHeader + pExportTable->AddressOfFunctions + 4 * uHint); PVOID lpFuncAddr = (PVOID)((PUCHAR)pDosHeader + ulFuncAddr); // 获取 SSDT 函数 Index # ifdef _WIN64 ulFunctionIndex = *(ULONG *)((PUCHAR)lpFuncAddr + 4); # else ulFunctionIndex = *(ULONG *)((PUCHAR)lpFuncAddr + 1); # endif break; } } return ulFunctionIndex; } ``` ###### 获取SSDT表地址 ``` # pragma pack(1) typedef struct _SERVICE_DESCIPTOR_TABLE { PULONG ServiceTableBase; // SSDT基址 PULONG ServiceCounterTableBase; // SSDT中服务被调用次数计数器 ULONG NumberOfService; // SSDT服务个数 PUCHAR ParamTableBase; // 系统服务参数表基址 }SSDTEntry, *PSSDTEntry; # pragma pack() // 直接获取 SSDT extern SSDTEntry __declspec(dllimport) KeServiceDescriptorTable; ``` ###### 获取SSDT函数地址 ``` // 获取 SSDT 函数地址 PVOID GetSSDTFunction(PCHAR pszFunctionName) { UNICODE_STRING ustrDllFileName; ULONG ulSSDTFunctionIndex = 0; PVOID pFunctionAddress = NULL; RtlInitUnicodeString(&ustrDllFileName, L"\\??\\C:\\Windows\\System32\\ntdll.dll"); // 从 ntdll.dll 中获取 SSDT 函数索引号 ulSSDTFunctionIndex = GetSSDTFunctionIndex(ustrDllFileName, pszFunctionName); // 根据索引号, 从SSDT表中获取对应函数地址 pFunctionAddress = (PVOID)KeServiceDescriptorTable.ServiceTableBase[ulSSDTFunctionIndex]; // 显示 DbgPrint("[%s][Index:%d][Address:0x%p]\n", pszFunctionName, ulSSDTFunctionIndex, pFunctionAddress); return pFunctionAddress; } ``` ##### 总结 1. **32位下的SSDT表已经由Ntoskrnl.exe导出**,直接获取导出符号KeServiceDriptorTable就能获取SSDT表; 2. SSDT表的作用就是把ring3的win32API函数和ring0的内核API函数联系起来。本质上,**SSDT就是一个用来保存windows系统服务地址的数组**; 3. **SSDT函数索引号可以从ntdll.dll文件中获取**,当ring3级API函数最终进入ring0级的时候,它会先将SSDT函数索引号mov给eax(使用**内核映射文件技术**,将磁盘上的ntdll.dll文件映射到内核内存空间中,并从导出表中获取导出函数地址,然后获取SSDT函数索引号); 4. **根据函数索引号就可以从SSDT表中获取对应函数地址**。 参考:《windows黑客编程技术详解》 最后修改:2021 年 10 月 28 日 10 : 28 AM © 允许规范转载