Windows内核(16)——探究SSDT
Windows内核探究SSDTSSDT简介在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 表条目结构
每个条目通常包含一个指向系统调用处理函数的指针,以及一个参数个数。结构体可以这样定义:
1234struct SSDTEntry { PVOID ServiceFunction; // 指向系统调用函数的指针 ULONG NumberOfArguments; // 系统调用的参数数量};
SSDT 表结构
SSDT 表是一个包含多个 SSDTEntry 的表,通常会有一个基地址和总条目数:
1234567struct SSDT { SSDTEntry* ServiceTable; // 指向 SSDTEntry 数组的指针 PVOID ArgumentTable; // 可选:指向系统调用参数表的指针 ULONG NumberOfServices; // 系统调用的总数 PVOID ServiceLimit; // 服务表的限制地址 PVOID TableBase; // 表的基地址};
通过SSDT进入内核跟踪一波API,例如 ExitProcess这个API
123456789101112#include
在虚拟机运行这个程序,触发int 3中断后,被windbg捕获,开始调试
但是断下来之后,我们发现是没有符号的,因为Windbg还没加载符号,所以我们要手动加载符号
windbg指令:
1.reload
这样我们就可以进行有符号调试了
通过查阅资料发现
老版本系统的调用ExitProcess的过程如下:ExitProcess => ntdll.NtTerminateProcess => KiIntSystemCall=> int 0x2E => nt!KiSystemService =>查表 =>nt.NtTerminateProcess =>iretd
可以看到老版本系统Syscall通过的是中断实现
具体来说:
通过nt!KiIntSystemCall调用 int 0x2E
我们查看下int 0x2e对应的中断门是啥
通过查看对应的IDT表项发现是一个系统服务
让我们来模拟一下老版本下的ExitProcess(0)的调用:
1234567891011__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表:
1dd KeServiceDescriptorTable
然后我们用WRK文档查询下这个结构体:
123456typedef 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) 值。它的语法和功能如下:
1dds 0x400000 L10
这样,就会以4个字节为一个地址,以符号形式显示0x10个地址
防御的思路:总有一些恶意软件可能会调用一些敏感API,这时候我们就可以进行主动防御
写一个报警软件:
思路:
Hook掉SSDT表,当调用这个API的时候,就在内核创建一个事件对象(CreateEvent)
此时三环的报警软件采用WaitEvent进行接收内核的信号(WaitEvent),一旦收到信号,就进行弹窗,询问用户是否同意这个敏感API的调用
(以上用队列实现)
还有一些软件加了强壳,例如VMP,这时候如果我们Hook了SSDT表,就可以对这个软件调用的所有API监控,这样就可以大概摸清这个软件大致干啥
遇到被Hook的API如何还原最快的方法就是重载内核,然后再去对比
这样既能找到哪里被Hook,也能修改回原来的API
如何拿SSDT和ShadowSSDT表直接暴力切换进程
1.Process xxx
然后会有一个问题,就是每个系统版本不一样,那么就会导致SSDT可能在EPROCESS结构体的位置不一样,那这个咋解决呢?
我们可以通过 nt!KiSystemServic 这个函数去找到对应偏移
这样找特征可以保证在各个系统下通用
1234//1.获取KTHREADPETHREAD pNowThread = PsGetCurrentThread();//2.获取ServiceTable表g_pServiceTable = (KSERVICE_TABLE_DESCRIPTOR*)(*(ULONG*)((ULONG)pNowThread + 0xbc));
当然windbg还有更简单的命令:
1dd nt!KeServiceDescriptorTable Lxxx (xxx代表长度)
实战Hook SSDT表123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144#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 );//定义一个指针函数,用于下面对O_NtOpenProcess进行强制转换ULONG O_NtOpenProcess;BOOLEAN ProtectProcess(HANDLE ProcessId,char *str_ProtectObjName){ NTSTATUS status; PEPROCESS process_obj; if(!MmIsAddressValid(str_ProtectObjName))//这个条件是用来判断目标进程名是否有效 { return FALSE; } if(ProcessId==0)//这个条件是用来排除System Idle Process进程的干扰 { return FALSE; } status=PsLookupProcessByProcessId(ProcessId,&process_obj);//这句用来获取目标进程的EPROCESS结构 if(!NT_SUCCESS(status)) { KdPrint(("我错了,这个是错误号:%X---这个是进程ID:%d",status,ProcessId)); return FALSE; } if(!strcmp((char *)process_obj+0x174,str_ProtectObjName))//进行比较 { ObDereferenceObject(process_obj);//对象计数器减1,为了恢复对象管理器计数,便于回收 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 ){ //KdPrint(("%s",(char *)PsGetCurrentProcess()+0x174)); if(ProtectProcess(ClientId->UniqueProcess,"calc.exe")) { KdPrint(("%s想打开我吗?不可能。哈哈。。",(char *)PsGetCurrentProcess()+0x174)); return STATUS_UNSUCCESSFUL; } //KdPrint(("Hook Success!")); return ((MYNTOPENPROCESS)O_NtOpenProcess)(ProcessHandle,//处理完自己的任务后,调用原来的函数,让其它进程正常工作 DesiredAccess, ObjectAttributes, ClientId);}void PageProtectOff()//关闭页面保护{ __asm{ cli mov eax,cr0 and eax,not 10000h mov cr0,eax }}void PageProtectOn()//打开页面保护{ __asm{ mov eax,cr0 or eax,10000h mov cr0,eax sti }}void UnHookSsdt(){ PageProtectOff(); KeServiceDescriptorTable.ServiceTableBase[122]=O_NtOpenProcess;//恢复ssdt中原来的函数地址 PageProtectOn();}NTSTATUS ssdt_hook(){ //int i; //for(i=0;i