Drunkmars's Blog

ring0下的进程保护

字数统计: 2k阅读时长: 8 min
2022/02/15

SSDT 的全称是 System Services Descriptor Table,系统服务描述符表。这个表就是一个把 Ring3 的 Win32 API 和 Ring0 的内核 API 联系起来。SSDT 并不仅仅只包含一个庞大的地址索引表,它还包含着一些其它有用的信息,诸如地址索引的基地址、服务函数个数等。通过修改此表的函数地址可以对常用 Windows 函数及 API 进行 Hook,从而实现对一些关心的系统动作进行过滤、监控的目的。一些 HIPS、防毒软件、系统监控、注册表监控软件往往会采用此接口来实现自己的监控模块。

结构

ssdt是一张表,即系统服务描述符表

1
kd> dd  KeServiceDescriptorTable

image-20220215114152634

第一个参数指向的地址存储的是全部的内核函数

image-20220215114231762

这个参数代表ssdt表里面有多少个内核函数

image-20220215114335917

这个参数是一个指针指向一个地址,这里表示的是与上面的内核函数相对应的参数个数,例如第一个为18,参数个数就为18/4 = 6

image-20220215114438649

这里找一下OpenProcess在SSDT表的索引,首先bp OpenProcess

image-20220215154538580

断在了kerner32.OpenProcess,这里OpenProcess会调用ntdll里面的ZwOpenProcess进入ring0,在ring0ZwOpenProcess又会调用NtOpenProcess

image-20220215154557683

赶紧去可以发现mov eax,0x7A,那么这里ZwOpenProcess的索引就为0x7A

image-20220215154617719

然后通过KeServiceDescriptorTable 找到所有的内核函数,通过内核函数+偏移找到OpenProcess函数

image-20220215155330682

在 NT 4.0 以上的 Windows 操作系统中,默认就存在两个系统服务描述表,这两个调度表对应了两类不同的系统服务,这两个调度表为:KeServiceDescriptorTable 和 KeServiceDescriptorTableShadow,其中 KeServiceDescriptorTable 主要是处理来自 Ring3 层 Kernel32.dll 中的系统调用,而 KeServiceDescriptorTableShadow 则主要处理来自 User32.dll 和 GDI32.dll 中的系统调用,并且KeServiceDescriptorTable 在ntoskrnl.exe(Windows 操作系统内核文件,包括内核和执行体层)是导出的,而 KeServiceDescriptorTableShadow 则是没有被 Windows 操作系统所导出。

image-20220215160122039

关于 SSDT 的全部内容则都是通过KeServiceDescriptorTable 来完成的。

image-20220215160042717

SSDT表的结构通过结构体表示为如下:

1
2
3
4
5
6
7
8
typedef struct _KSERVICE_TABLE_DESCRIPTOR
{
  KSYSTEM_SERVICE_TABLE  ntoskrnl;  // ntoskrnl.exe 的服务函数
  KSYSTEM_SERVICE_TABLE  win32k;   // win32k.sys 的服务函数
(GDI32.dll/User32.dll 的内核支持)
  KSYSTEM_SERVICE_TABLE  notUsed1;
  KSYSTEM_SERVICE_TABLE  notUsed2;
} KSERVICE_TABLE_DESCRIPTOR, * PKSERVICE_TABLE_DESCRIPTOR;

其中每一项又是一个结构体: KSYSTEM_SERVICE_TABLE 。通过结构体表示为如下:

1
2
3
4
5
6
7
typedef struct _KSYSTEM_SERVICE_TABLE
{
  PULONG  ServiceTableBase;      // SSDT (System Service Dispatch Table)的基地址
  PULONG  ServiceCounterTableBase;  // 用于 checked builds, 包含 SSDT 中每个服务被调用的次数
  ULONG  NumberOfService;      // 服务函数的个数, NumberOfService * 4 就是整个地址表的大小
  ULONG  ParamTableBase;       // SSPT(System Service Parameter Table)的基地址
} KSYSTEM_SERVICE_TABLE, * PKSYSTEM_SERVICE_TABLE;

调用号

进入0环时调用号是eax传递的,但这个调用号并不只是一个普通的数字作为索引序号,系统会把他用32位数据表示,拆分成19:1:12的格式,如下:

image-20220215160737509

分析一下0-11这低12位组成一个真正的索引号,第12位表示服务表号,13-31位没有使用。而进入内核后调用哪一张表,就由调用号中的第12位决定,为0则调用SSDT表,为1则调用ShadowSSDT表。

CR4寄存器

这里函数准备好以后,就要将该函数的指针覆盖原来NtOpenProcess的指针。但是需要注意的是:我们自己改自己的代码是不用管权限的,改别人的代码很有可能这块内存是只读的,并不可写。

那么本质上就是SSDT对应的物理页是只读的,这里有两种办法,我们都知道物理页的内存R/W位的属性是由PDE和PTE相与而来的,那么我们就可以改变SSDT对应的PDE和PTE的R/W属性,将物理页设置为可读可写的。通过CR4寄存器判断是2-9-9-12分页还是10-10-12分页。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if(RCR4 & 0x00000020)
{//说明是2-9-9-12分页
KdPrint(("2-9-9-12分页 %p\n",RCR4));
KdPrint(("PTE1 %p\n",*(DWORD*)(0xC0000000 + ((HookFunAddr >> 9) &0x007FFFF8))));
*(DWORD64*)(0xC0000000 + ((HookFunAddr >> 9) & 0x007FFFF8)) |= 0x02;
KdPrint(("PTE1 %p\n",*(DWORD*)(0xC0000000 + ((HookFunAddr >> 9) &0x007FFFF8))));
}
else
{//说明是10-10-12分页
KdPrint(("10-10-12分页\n"));
KdPrint(("PTE1 %p\n",*(DWORD*)(0xC0000000 + ((HookFunAddr >> 10) & 0x003FFFFC))));
*(DWORD*)(0xC0000000 + ((HookFunAddr >> 10) & 0x003FFFFC)) |= 0x02;
KdPrint(("PTE2 %p\n",*(DWORD*)(0xC0000000 + ((HookFunAddr >> 10) &0x003FFFFC))));
}

CR0寄存器

使用PsGetCurrentThread()函数可获取当前KTHREAD的首地址。

但是需要注意的是SSDT表所在的内存页属性是只读,没有写入的权限,所以需要把该地址设置为可写入,这样才能写入自己的函数,使用的是CR0寄存器关闭只读属性。

简单介绍下CR0寄存器:

image-20220215160834842

可以看到这里使用32位寄存器,而在CR0寄存器中,我们重点关注的是3个标志位:

PE ­ 是否启用保护模式,置1则启用。
PG ­ 是否使用分页模式, 置1则开启分页模式, 此标志置1时, PE 标志也必须置1,否则CPU报异常。
WP WP为1时, 不能修改只读的内存页 , WP为0时, 可以修改只读的内存页。

所以在进行HOOK时,只要把CR0寄存器中的WP位置为0,就能对内存进行写入操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//关闭页只读保护
__asm
{
push eax;
mov eax, cr0;
and eax, ~0x10000; // 与0x10000相与后取反得到0
mov cr0, eax;
pop eax;
ret;
}

//开启页只读保护
__asm
{
push eax;
mov eax, cr0;
or eax, 0x10000;
mov cr0, eax;
pop eax;
ret;
}

实现代码

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
#include <ntddk.h>
#include <ntstatus.h>

// 记录原函数的地址
ULONG uOldNtOpenProcess;

//内核之SSDT-HOOK
//系统服务表
typedef struct _KSYSTEM_SERVICE_TABLE
{
PULONG ServiceTableBase; //函数地址表的首地址
PULONG ServiceCounterTableBase;//函数表中每个函数被调用的次数
ULONG NumberOfService; //服务函数的个数
ULONG ParamTableBase; //参数个数表首地址
}KSYSTEM_SERVICE_TABLE, *PKSYSTEM_SERVICE_TABLE;

//服务描述符
typedef struct _KSERVICE_TABLE_DESCRIPTOR
{
KSYSTEM_SERVICE_TABLE ntoskrnl;//ntoskrnl.exe的服务函数,SSDT
KSYSTEM_SERVICE_TABLE win32k; //win32k.sys的服务函数,ShadowSSDT
KSYSTEM_SERVICE_TABLE notUsed1;//暂时没用1
KSYSTEM_SERVICE_TABLE notUsed2;//暂时没用2
}KSERVICE_TABLE_DESCRIPTOR, *PKSERVICE_TABLE_DESCRIPTOR;

//定义HOOK的函数的类型
typedef NTSTATUS(NTAPI* FuZwOpenProcess)(
_Out_ PHANDLE ProcessHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_ POBJECT_ATTRIBUTES ObjectAttributes,
_In_opt_ PCLIENT_ID ClientId
);

//自写的函数声明
NTSTATUS NTAPI MyZwOpenProcess(
_Out_ PHANDLE ProcessHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_ POBJECT_ATTRIBUTES ObjectAttributes,
_In_opt_ PCLIENT_ID ClientId
);

// KeServiceDescriptorTable 为 ntoskrnl.exe 所导出的全局变量
extern PKSERVICE_TABLE_DESCRIPTOR KeServiceDescriptorTable;

//记录系统的该函数
FuZwOpenProcess g_OldZwOpenProcess;
//服务描述符表指针
KSERVICE_TABLE_DESCRIPTOR* g_pServiceTable = NULL;
//要保护进程的ID
ULONG g_Pid = 1624;

//安装钩子
NTSTATUS HookNtOpenProcess();
//卸载钩子
NTSTATUS UnHookNtOpenProcess();
//关闭页写入保护
void ShutPageProtect();
//开启页写入保护
void OpenPageProtect();

//卸载驱动
void DriverUnload(DRIVER_OBJECT* obj);



/***驱动入口主函数***/
NTSTATUS DriverEntry(DRIVER_OBJECT* driver, UNICODE_STRING* path)
{
KdPrint(("驱动启动成功!\n"));

//安装钩子
HookNtOpenProcess();

driver->DriverUnload = DriverUnload;
return STATUS_SUCCESS;
}

//卸载驱动
void DriverUnload(DRIVER_OBJECT* obj)
{
//卸载钩子
UnHookNtOpenProcess();

KdPrint(("驱动卸载成功!\n"));
}


NTSTATUS HookNtOpenProcess()
{
NTSTATUS Status;

Status = STATUS_SUCCESS;

//1.关闭页只读保护
ShutPageProtect();
//2.修改SSDT表
uOldNtOpenProcess = KeServiceDescriptorTable->ntoskrnl.ServiceTableBase[0x7a];
KeServiceDescriptorTable->ntoskrnl.ServiceTableBase[0x7a] =(ULONG)MyZwOpenProcess;
//3.开启页只读保护
OpenPageProtect();
return Status;

}

//卸载钩子
NTSTATUS UnHookNtOpenProcess()
{
NTSTATUS status;
status = STATUS_SUCCESS;

//1.关闭页只读保护
ShutPageProtect();
//2.写入原来的函数到SSDT表内
KeServiceDescriptorTable->ntoskrnl.ServiceTableBase[0x7a] = uOldNtOpenProcess;
//3.开启页只读保护
OpenPageProtect();

return status;
}

//关闭页只读保护
void _declspec(naked) ShutPageProtect()
{
__asm
{
push eax;
mov eax, cr0;
and eax, ~0x10000;
mov cr0, eax;
pop eax;
ret;
}
}

//开启页只读保护
void _declspec(naked) OpenPageProtect()
{
__asm
{
push eax;
mov eax, cr0;
or eax, 0x10000;
mov cr0, eax;
pop eax;
ret;
}
}

//自写的函数
NTSTATUS NTAPI MyZwOpenProcess(
_Out_ PHANDLE ProcessHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_ POBJECT_ATTRIBUTES ObjectAttributes,
_In_opt_ PCLIENT_ID ClientId
)
{
//当此进程为要保护的进程时
if (ClientId->UniqueProcess == (HANDLE)g_Pid)
{
//设为拒绝访问
DesiredAccess = 0;
}


//调用原函数
return NtOpenProcess(ProcessHandle,DesiredAccess,ObjectAttributes,ClientId);
}

实现效果如下

SSDT

参考

参考自hxd的r0下的进程保护

CATALOG
  1. 1. 结构
  2. 2. 调用号
  3. 3. CR4寄存器
  4. 4. CR0寄存器
  5. 5. 参考