Windows内核 探究SSDT SSDT简介 在Windows x86系统中,系统服务描述表(System Service Descriptor Table,简称SSDT)是一个核心数据结构,主要用于管理和调度系统调用(syscall)。SSDT在操作系统的内核模式下提供了一种机制,使得用户模式的应用程序能够请求内核模式服务,比如文件操作、进程管理、内存管理等。以下是SSDT的主要作用:
系统调用接口 SSDT充当了用户模式应用程序与操作系统内核之间的桥梁。应用程序通过调用标准的Windows API(如ReadFile
、WriteFile
等),这些API最终会通过系统调用的方式进入内核模式。SSDT中包含了一组函数指针,这些指针指向具体的内核服务例程。每个系统调用在SSDT中都有对应的入口,通过这些入口可以找到实现该调用的内核函数。
系统调用号到函数指针的映射 每个系统调用在SSDT中都有一个唯一的索引号(系统调用号)。当应用程序发出系统调用时,会使用该调用的系统调用号在SSDT中查找对应的内核服务例程的地址。然后,系统将控制权转移给这个地址处的服务例程来执行实际的操作。例如,通过syscall
指令,CPU根据系统调用号查找SSDT中的相应地址并跳转执行。
安全和稳定性 SSDT通过将系统调用的实现限定在内核模式中,可以防止用户模式代码直接访问和修改内核数据结构,从而提供了一层安全保护。此外,SSDT的结构化管理使得操作系统能够更有效地控制和调度系统资源,从而提高系统的稳定性。
系统扩展和模块化 SSDT的设计使得操作系统可以灵活地增加或修改系统调用。例如,驱动程序可以通过修改SSDT来添加新的系统调用或者替换现有的系统调用,这在某些特殊的内核模块和扩展中非常有用。然而,这种操作也可能带来安全问题,因为恶意软件可能利用这种机制来进行钩子注入(hooking),以隐藏自身活动或拦截系统调用。
逆向工程和安全分析 在逆向工程和安全分析领域,研究SSDT非常重要。安全专家经常分析SSDT来检查系统调用是否被恶意软件钩取。通过检测SSDT中函数指针的异常修改,安全工具可以发现潜在的恶意行为,如rootkit的存在。
新版本通过Sysenter,也差不多:
SSDT结构 SSDT 表条目结构
每个条目通常包含一个指向系统调用处理函数的指针,以及一个参数个数。结构体可以这样定义:
1 2 3 4 struct SSDTEntry { PVOID ServiceFunction; ULONG NumberOfArguments; };
SSDT 表结构
SSDT 表是一个包含多个 SSDTEntry 的表,通常会有一个基地址和总条目数:
1 2 3 4 5 6 7 struct SSDT { SSDTEntry* ServiceTable; PVOID ArgumentTable; ULONG NumberOfServices; PVOID ServiceLimit; PVOID TableBase; };
通过SSDT进入内核 跟踪一波API,例如 ExitProcess这个API
1 2 3 4 5 6 7 8 9 10 11 12 #include <iostream> #include <windows.h> using namespace std;int main () { __asm { int 3 } ExitProcess (0 ); return 0 ; }
在虚拟机运行这个程序,触发int 3中断后,被windbg捕获,开始调试
但是断下来之后,我们发现是没有符号的,因为Windbg还没加载符号,所以我们要手动加载符号
windbg指令:
这样我们就可以进行有符号调试了
通过查阅资料发现
老版本系统的调用ExitProcess的过程如下:ExitProcess => ntdll.NtTerminateProcess => KiIntSystemCall=> int 0x2E => nt!KiSystemService =>查表 =>nt.NtTerminateProcess =>iretd
可以看到老版本系统Syscall通过的是中断实现
具体来说:
通过nt!KiIntSystemCall调用 int 0x2E
我们查看下int 0x2e对应的中断门是啥
通过查看对应的IDT表项发现是一个系统服务
让我们来模拟一下老版本下的ExitProcess(0)的调用:
1 2 3 4 5 6 7 8 9 10 11 __asm { push 0 push -1 ;代表当前进程 mov eax,0x173 ;系统调用号 mov edx,esp int 0x2e } ;mov edx, esp 的作用 ;在这个模拟的系统调用代码中,mov edx, esp 的作用是将栈指针(ESP)的值赋给 EDX,这样 EDX 指向了当前的栈顶。这个操作将栈中的参数传递给系统调用。
这样写的代码的优势是1.让不懂内核的人逆不动。2.可以通过中断去调用API,而不需要写驱动程序
劣势就是系统不通用,可能系统调用号会改变
用Windbg可以直接查到SSDT表:
1 dd KeServiceDescriptorTable
然后我们用WRK文档查询下这个结构体:
1 2 3 4 5 6 typedef struct _KSERVICE_TABLE_DESCRIPTOR { PULONG_PTR Base; PULONG Count; ULONG Limit; PUCHAR Number; } KSERVICE_TABLE_DESCRIPTOR, *PKSERVICE_TABLE_DESCRIPTOR;
可以看出来这个表大小是16个字节: 在这个表的下面0x10个字节,就是KeServiceDescriptorTableShadow
KeServiceDescriptorTableShadow
是 KeServiceDescriptorTable
的扩展,它用于支持用户模式和内核模式下的服务调用。
可以看到,在控制台程序中,KeServiceDescriptorTableShadow是空的。但是如果在Win32窗口程序,这一项就不会为空
在 WinDbg 中,dds
指令用于以符号形式显示内存中的双字 (DWORD) 值。它的语法和功能如下:
这样,就会以4个字节为一个地址,以符号形式显示0x10个地址
防御的思路: 总有一些恶意软件可能会调用一些敏感API,这时候我们就可以进行主动防御
写一个报警软件:
思路:
Hook掉SSDT表,当调用这个API的时候,就在内核创建一个事件对象(CreateEvent)
此时三环的报警软件采用WaitEvent进行接收内核的信号(WaitEvent),一旦收到信号,就进行弹窗,询问用户是否同意这个敏感API的调用
(以上用队列实现)
还有一些软件加了强壳,例如VMP,这时候如果我们Hook了SSDT表,就可以对这个软件调用的所有API监控,这样就可以大概摸清这个软件大致干啥
遇到被Hook的API如何还原 最快的方法就是重载内核,然后再去对比
这样既能找到哪里被Hook,也能修改回原来的API
如何拿SSDT和ShadowSSDT表 直接暴力切换进程
然后会有一个问题,就是每个系统版本不一样,那么就会导致SSDT可能在EPROCESS结构体的位置不一样,那这个咋解决呢?
我们可以通过 nt!KiSystemServic 这个函数去找到对应偏移
这样找特征可以保证在各个系统下通用
1 2 3 4 PETHREAD pNowThread = PsGetCurrentThread (); g_pServiceTable = (KSERVICE_TABLE_DESCRIPTOR*)(*(ULONG*)((ULONG)pNowThread + 0xbc ));
当然windbg还有更简单的命令:
1 dd nt!KeServiceDescriptorTable Lxxx (xxx代表长度)
实战Hook SSDT表 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 #include "ntddk.h" #pragma pack(1) typedef struct ServiceDescriptorEntry { unsigned int *ServiceTableBase; unsigned int *ServiceCounterTableBase; unsigned int NumberOfServices; unsigned char *ParamTableBase; } ServiceDescriptorTableEntry_t, *PServiceDescriptorTableEntry_t; #pragma pack() NTSTATUS PsLookupProcessByProcessId ( IN HANDLE ProcessId, OUT PEPROCESS *Process ) ;__declspec(dllimport) ServiceDescriptorTableEntry_t KeServiceDescriptorTable; typedef NTSTATUS (*MYNTOPENPROCESS) ( OUT PHANDLE ProcessHandle, IN ACCESS_MASK AccessMask, IN POBJECT_ATTRIBUTES ObjectAttributes, IN PCLIENT_ID ClientId ) ;ULONG O_NtOpenProcess; BOOLEAN ProtectProcess (HANDLE ProcessId,char *str_ProtectObjName) { NTSTATUS status; PEPROCESS process_obj; if (!MmIsAddressValid (str_ProtectObjName)) { return FALSE; } if (ProcessId==0 ) { return FALSE; } status=PsLookupProcessByProcessId (ProcessId,&process_obj); if (!NT_SUCCESS (status)) { KdPrint (("我错了,这个是错误号:%X---这个是进程ID:%d" ,status,ProcessId)); return FALSE; } if (!strcmp ((char *)process_obj+0x174 ,str_ProtectObjName)) { ObDereferenceObject (process_obj); return TRUE; } ObDereferenceObject (process_obj); return FALSE; } NTSTATUS MyNtOpenProcess ( __out PHANDLE ProcessHandle, __in ACCESS_MASK DesiredAccess, __in POBJECT_ATTRIBUTES ObjectAttributes, __in_opt PCLIENT_ID ClientId ) { if (ProtectProcess (ClientId->UniqueProcess,"calc.exe" )) { KdPrint (("%s想打开我吗?不可能。哈哈。。" ,(char *)PsGetCurrentProcess ()+0x174 )); return STATUS_UNSUCCESSFUL; } return ((MYNTOPENPROCESS)O_NtOpenProcess)(ProcessHandle, DesiredAccess, ObjectAttributes, ClientId); } void PageProtectOff () { __asm{ cli mov eax,cr0 and eax,not 10000 h mov cr0,eax } } void PageProtectOn () { __asm{ mov eax,cr0 or eax,10000 h mov cr0,eax sti } } void UnHookSsdt () { PageProtectOff (); KeServiceDescriptorTable.ServiceTableBase[122 ]=O_NtOpenProcess; PageProtectOn (); } NTSTATUS ssdt_hook () { O_NtOpenProcess=KeServiceDescriptorTable.ServiceTableBase[122 ]; PageProtectOff (); KeServiceDescriptorTable.ServiceTableBase[122 ]=(unsigned int )MyNtOpenProcess; PageProtectOn (); return STATUS_SUCCESS; } void DriverUnload (PDRIVER_OBJECT pDriverObject) { UnHookSsdt (); KdPrint (("Driver Unload Success !" )); } NTSTATUS DriverEntry (PDRIVER_OBJECT pDriverObject,PUNICODE_STRING pRegsiterPath) { DbgPrint ("This is My First Driver!" ); ssdt_hook (); pDriverObject->DriverUnload = DriverUnload; return STATUS_SUCCESS; }