Windows内核

探究SSDT

SSDT简介

在Windows x86系统中,系统服务描述表(System Service Descriptor Table,简称SSDT)是一个核心数据结构,主要用于管理和调度系统调用(syscall)。SSDT在操作系统的内核模式下提供了一种机制,使得用户模式的应用程序能够请求内核模式服务,比如文件操作、进程管理、内存管理等。以下是SSDT的主要作用:

  1. 系统调用接口
    SSDT充当了用户模式应用程序与操作系统内核之间的桥梁。应用程序通过调用标准的Windows API(如ReadFileWriteFile等),这些API最终会通过系统调用的方式进入内核模式。SSDT中包含了一组函数指针,这些指针指向具体的内核服务例程。每个系统调用在SSDT中都有对应的入口,通过这些入口可以找到实现该调用的内核函数。
  2. 系统调用号到函数指针的映射
    每个系统调用在SSDT中都有一个唯一的索引号(系统调用号)。当应用程序发出系统调用时,会使用该调用的系统调用号在SSDT中查找对应的内核服务例程的地址。然后,系统将控制权转移给这个地址处的服务例程来执行实际的操作。例如,通过syscall指令,CPU根据系统调用号查找SSDT中的相应地址并跳转执行。
  3. 安全和稳定性
    SSDT通过将系统调用的实现限定在内核模式中,可以防止用户模式代码直接访问和修改内核数据结构,从而提供了一层安全保护。此外,SSDT的结构化管理使得操作系统能够更有效地控制和调度系统资源,从而提高系统的稳定性。
  4. 系统扩展和模块化
    SSDT的设计使得操作系统可以灵活地增加或修改系统调用。例如,驱动程序可以通过修改SSDT来添加新的系统调用或者替换现有的系统调用,这在某些特殊的内核模块和扩展中非常有用。然而,这种操作也可能带来安全问题,因为恶意软件可能利用这种机制来进行钩子注入(hooking),以隐藏自身活动或拦截系统调用。
  5. 逆向工程和安全分析
    在逆向工程和安全分析领域,研究SSDT非常重要。安全专家经常分析SSDT来检查系统调用是否被恶意软件钩取。通过检测SSDT中函数指针的异常修改,安全工具可以发现潜在的恶意行为,如rootkit的存在。

新版本通过Sysenter,也差不多:
1724587877279

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; // 指向 SSDTEntry 数组的指针
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还没加载符号,所以我们要手动加载符号

1724571327556

windbg指令:

1
.reload

这样我们就可以进行有符号调试了

1724571401352

通过查阅资料发现

老版本系统的调用ExitProcess的过程如下:ExitProcess => ntdll.NtTerminateProcess => KiIntSystemCall=> int 0x2E => nt!KiSystemService =>查表 =>nt.NtTerminateProcess =>iretd

可以看到老版本系统Syscall通过的是中断实现

具体来说:

通过nt!KiIntSystemCall调用 int 0x2E

1724571805154

我们查看下int 0x2e对应的中断门是啥

通过查看对应的IDT表项发现是一个系统服务

1724573732714

让我们来模拟一下老版本下的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文档查询下这个结构体:

1724588489239

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

KeServiceDescriptorTableShadowKeServiceDescriptorTable 的扩展,它用于支持用户模式和内核模式下的服务调用。

可以看到,在控制台程序中,KeServiceDescriptorTableShadow是空的。但是如果在Win32窗口程序,这一项就不会为空

1724588951275

在 WinDbg 中,dds 指令用于以符号形式显示内存中的双字 (DWORD) 值。它的语法和功能如下:

1
dds 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 这个函数去找到对应偏移

1724637118596

这样找特征可以保证在各个系统下通用

1
2
3
4
//1.获取KTHREAD
PETHREAD pNowThread = PsGetCurrentThread();
//2.获取ServiceTable表
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 );//定义一个指针函数,用于下面对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<KeServiceDescriptorTable.NumberOfServices;i++)
//{
// KdPrint(("NumberOfService[%d]------- %x",i,KeServiceDescriptorTable.ServiceTableBase[i]));
//}
O_NtOpenProcess=KeServiceDescriptorTable.ServiceTableBase[122];//保存原来的函数地址
PageProtectOff();
//将原来ssdt中所要hook的函数地址换成我们自己的函数地址
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;
}