基于SSDT HOOK技术的Ring0级进程保护组件设计与实现

来源:互联网 发布:宽带网络安装 编辑:程序博客网 时间:2024/05/22 01:21

一、背景介绍

进程保护这一思想很早就出现,现在大多数的杀毒软件等安全防护软件均带有对自身进程保护的功能,这是为了防止病毒等恶意程序破坏进程的运行。相对的,很多病毒为了防止被发现或终止,提高自己的运行权限,隐藏自己、保护自己不被终止。 

然而并不是只有病毒及杀毒软件有对自己的进程进行保护的需求,很多软件,如网吧、机房等的监控系统、计费系统等,以及一些必须确保自身运行过程中不被强行中断,否则会导致数据丢失、操作系统崩溃等严重后果的软件系统,都需要对自身进行必要的防护,防止恶意行为或者操作者的误操作引起的损失。  

高权限的进程保护关系到操作系统底层,并不是所有语言都容易开发高权限进程保护功能,尤其是基于代码托管的语言。目前,实现进程保护的软件均将进程保护功能嵌入在软件内,没有专门的组件提供进程保护功能。因此我们想利用C++实现一套专门提供进程隐藏、进程保护的组件,以 DLL(Dynamic Link Lib)的形式呈现给软件开发者,使其通过调用即可实现对进程的高权限保护,而不必在乎其软件本身的开发语言。 

进程保护的实现方式多种多样,当如何做实现高权限(如运行在 Ring0 特权级下) 、灵活性(方便的对单个/多个进程进行守护) 、可移植性等特性成为关键因素。在选择实现技术时,我们选择了 SSDT  Hook 技术作为组件的实现方式。

SSDT 为 Windows 操作系统下的系统服务描述符表,这个表是一个把 Ring3 的Win32 API和Ring0 的内核 API联系起来的角色。我们利用 SSDT的角色特点,设计钩子拦截进程操作相关API调用,实现进程隐藏及进程保护。其中由于SSDT的读取只能在 Ring0 层完成,于是我们使用了内核驱动并借助 DeviceIoControl来完成相应操作。


二、SSDT Hook技术 

2.1 SSDT简介 

SSDT,全称 System Services Descriptor Table,即系统服务描述符表。这个表一个把Ring3 的Win32 API和Ring0的内核 API联系起来的角色。 

例如,我们打开任务管理器(taskmgr.exe),选中一个进程,如 notepad.exe并选择结束进程,这时记事本程序就会关闭。这个过程看似简单,但是其中的流程如下: 

1、taskmgr 进程首先获取要结束的进程句柄,通过 OpenProcess()这个 Ring3下API,OpenProcess 调用在Kernel32.dll 中; 

2、系统捕获API调用,转到Ring0 级下,从 SSDT中取得对应的 Ring0 API:NtOpenProcess 的入口地址,执行相应操作,此过程在 ntdll.dll 中; 

3、完成NtOpenProcess 操作后转回Ring3 级下返回 OpenProcess 函数,继而返回taskmgr进程,因此获取了要结束的进程的句柄; 

4、taskmgr 执行 TerminateProcess()这个 Ring3 下的 API,传入进程句柄,尝试结束进程; 

5、系统捕获API调用,再次转到 Ring0 级下,从SSDT中取得对应的 Ring0 API:NtTerminateProcess 的入口地址,执行结束进程操作;

6、完成 NtTerminateProcess 操作后转回 Ring3 级下返回 TerminateProcess 函数,继而返回taskmgr 进程,完成结束进程过程; 

整个过程流程如下: 


可以看到这个简单的操作跨了 Ring0 和 Ring3 两个特权级。其中 SSDT表起了关键的作用,他给出了系统服务函数的入口地址,系统会根据这个地址调用相应的系统服务函数。SSDT表的结构是这样的: 


当程序的处理流程进入 Ring0 之后,系统会根据服务号(eax)在 SSDT 这个系统服务描述符表中查找对应的表项,这个找到的表项就是系统服务函数NtOpenProcess 的真正地址。之后,系统会根据这个地址调用相应的系统服务函数,并把结果返回给ntdll.dll 中的 NtOpenProcess。图中的“SSDT”所示即为系统服务描述符表的各个表项;右侧的“ntoskrnl.exe”则为 Windows 系统内核服务进程(ntoskrnl即为 NT OS KerneL的缩写),它提供了相对应的各个系统服务函数。ntoskrnl.exe这个文件位于 Windows 的system32 目录下。 

根据处理器的不同,系统内核服务进程可能也是不一样的。真正运行于系统上的内核服务进程可能还有 ntkrnlmp.exe、ntkrnlpa.exe等情况。 


2.2 进程保护功能分析 

了解了 SSDT表的作用后,就可以利用它进行进程的保护了。本组件实现了两项进程保护功能: 

1、 进程信息隐藏 

隐藏进程的信息,其他程序无法查看进程的应用程序路径、用户、特权级等等。 


2、 进程防结束 

使进程无法轻易被其他进程结束。 

要做到隐藏进程信息的功能,就需要防止其他程序打开此进程,我们可以对NtOpenProcess 函数做钩子实现。要实现进程防结束功能,则可以通过对NtTerminateProcess 函数进行 HOOK 实现。因此,本组件需要实现挂载 SSDT,对NtOpenProcess 及 NtTerminateProcess 进行 HOOK。 


2.3 SSDT HOOK实现 

要挂载 SSDT 表,需要知道 SSDT 表的位置等信息。这时就要用到KeServiceDescriptorTable。KeServiceDescriptorTable 是由内核导出的一个表,这个表是访问SSDT的关键,具体结构如下: 

typedef struct ServiceDescriptorTable{ PVOID pvSSDTBase; PVOID pvServiceCounterTable; ULONG ulNumberOfServices; PVOID pvParamTableBase; }SSDT, *PSSDT; 

其中,pvSSDTBase 就是 SSDT 表的基地址;pvServiceCounterTable 则指向另一个索引表,该表包含了每个服务表项被调用的次数;ulNumberOfServices 表示当前系统所支持的服务个数;pvParamTableBase 指向 SSPT(System  Service Parameter Table,即系统服务参数表),该表格包含了每个服务所需的参数字节数。  

DDK 的头文件中并未声明 KeServiceDescriptorTable,不过我们可以自己手动添加: 

Extern PSSDT KeServiceDescriptorTable; 

有了这些信息后,我们就可以进行 SSDT  HOOK 了。HOOK 的实现方式如下:首先访问 SSDT 表,保存表中记录的需要挂钩的函数(如 NtOpenProcess)地址。其次将 SSDT 表中此函数地址更改为钩子函数(如 MyNtOpenProcess)。这样就能当系统中其他程序通过 API调用NtOpenPorcess 时,执行的就不是原来系统的 NtOpenProcess 了,而是自己的程序。最后想要取消 HOOK 时只需要将自己写入的函数地址再替换为 HOOK时保存的原函数地址即可。 

下面以 HOOK NtOpenProcess 函数为例详细介绍 SSDT HOOK的实现过程。  

1、改变SSDT 内存的保护

系统对SSDT 是只读的,不能写。一般可以通过修改CR0 寄存器实现去除保护。Windows  对内存的分配,是采用的分页管理。其中CR0  寄存器其中第1位叫做保护属性位,控制着页的读或写属性。如果为1,则可以读、写、执行;如果为0,则只可以读、执行。

SSDT、IDT(Interrupt  Descriptor  Table,中断描述符表)的页属性在默认下都是只读,可执行的,但不能写。所以现在要把这一 位设置成1。 

//修改SSDT 表只读为可读写 __asm {     cli     mov eax, cr0     and eax, not 10000h     mov cr0, eax } 

2、在SSDT 中找到NtOpenProcess 项的地址 

SSDT 表中每项大小为4 个字节,每个项都有一个索引值或者叫做服务号,可以通过索引值找到该项的地址。在各版本的Windows操作系统中各函数的索引值不尽相同。

例如,Windows XP 中NtOpenProcess 项的索引值为7AH,而在Windows 2000中其索引值为6AH 。因此在执行前还需要确定操作系统版本,或者通过枚举ntdll.dll的导出函数来间接枚举SSDT所有表项所对应的函数。

下图既对SSDT 表的遍历,显示当前系统中SSDT 的基地址、服务个数,以及各个表项所对应的索引号、所在模块、地址和服务名。


ULONG Address; //Windows XP 下NtOpenProcess 项在SSDT表中的索引为 7AH //因此在SSDT 表中的地址为:基址+7AH*4 Address = (ULONG)KeServiceDescriptorTable->ServiceTableBase + 0x7A * 4; 

3、 保存原 NtOpenProcess 函数地址 

//保存原NtOpenProcess地址 RealOPServiceAddress = *(ULONG*)Address; RealNtOpenProcess = (NTOPENPROCESS)RealOPServiceAddress; 

4、 挂载钩子函数 

//挂载NtOpenProcess钩子函数 *((ULONG*)Address) = (ULONG)MyNtOpenProcess; 

5、 恢复 SSDT 内存的保护 

//修改SSDT 为只读 __asm {   mov eax, cr0   or eax, 10000h   mov cr0, eax   sti } 

至此,对 NtOpenProcess 函数的 HOOK 完毕,当其他进程调用时就会执行MyNtOpenProcess。取消 HOOK 的流程与 HOOK 的流程类似,只是将原来保存的真实服务地址写回 SSDT 表项中即可,在此就不再详细描述取消 HOOK 的流程及代码。 

进程防结束功能是通过对NtTerminateProcess系统服务函数进程HOOK实现的,其 HOOK 与取消 HOOK 方法和 NtOpenProcess 的基本完全相同,唯一不同的地方在于Windows XP 下NtTerminateProcess 在SSDT中的索引值为 101H,地址则为SSDT表基址+101H*4。 


2.4 钩子函数实现 

在执行系统调用 NtOpenProcess 时,需要传入参数,其中一项参数中给出了进程的PID号。因此我们可以通过 PID判断要打开的进程是否为要保护的进程,如果是则返回打开失败,如果不是则调用真实的 NtOpenProcess 函数(HOOK时已保存此函数地址)执行打开操作。具体代码如下: 

NTSTATUS __stdcall MyNtOpenProcess(OUT PHANDLE ProcessHandle,    IN ACCESS_MASK DesiredAccess,    IN POBJECT_ATTRIBUTES ObjectAttributes,    IN PCLIENT_ID ClientId){ //判断是否为正在保护的进程 if( (ClientId != NULL) ){ pid = (ULONG)ClientId->UniqueProcess; for(i = 0; i < MAX_ANTIOPEN_NUM; i++) {//如果是则返回打开失败 if( ANTIOPEN_PIDS[i] == pid ){ ProcessHandle = NULL; rc = STATUS_ACCESS_DENIED; return rc; } }} //非保护进程则调用原打开函数正常打开 rc = (NTSTATUS)(NTOPENPROCESS)RealNtOpenProcess(ProcessHandle, DesiredAccess, ObjectAttributes, ClientId); return rc; } 

其中 ANTIOPEN_PIDS[]为自定义的一个数组,里面存着要保护的进程的PID。 

这样对如果利用 SSDT HOOK技术实现对进程信息隐藏的功能就全部实现。下面介绍NtTerminateProcess的钩子函数——MyNtTerminateProcess的功能实现。NtTerminateProcess 有两项传入参数:要结束的进程的句柄及退出状态。因此我们需要通过传入的进程句柄判断要结束的进程是否为正在保护的进程,从而决定是否阻止此次结束操作。 

要想通过句柄得到进程的 PID,需要首先执行 ObReferenceObjectByHandle()这个内核系统调用,它的作用是根据一个句柄返回内存中的 FileObject。因为此处的句柄为进程句柄,所以 FileObject 为EPROCESS,即进程的执行体,里面不仅包括了进程的许多属性,还包扩了许多指向其他数据结构的指针,我们可以通过此结构获取进程的 PID。具体的代码如下: 

// GetProcessInfo:从指定进程执行体中获取进程 PID及进程名 void GetProcessInfo(IN PEPROCESS pEprocess, OUT PULONG uProcessId, OUT PUCHAR * szProccessName) { PUCHAR eprocess = (PUCHAR) pEprocess; * uProcessId = *((PULONG)(eprocess + 0x084)); * szProccessName = eprocess + 0x0174; } NTSTATUS __stdcall MyNtTerminateProcess(IN HANDLE ProcessHandle, IN NTSTATUS ExitStatus){ //获取进程句柄随对应的内存块地址 rc = ObReferenceObjectByHandle(ProcessHandle, FILE_READ_DATA, 0, KernelMode, &pEprocess, 0); if(rc != STATUS_SUCCESS) {return (NTSTATUS)(NTTERMINATEPROCESS)RealNtTerminateProcess(ProcessHandle, ExitStatus); }//从进程执行体数据结构里获取进程PID GetProcessInfo(pEprocess,&uProcessId,&szProccessName); //判断是否为正在保护的进程 for(i = 0; i < MAX_ANTITERM_NUM; i++) {if(ANTITERM_PIDS[i] == uProcessId){ //如果是则返回结束失败 rc = STATUS_ACCESS_DENIED; return rc; } }//非保护进程则调用原结束函数结束 return (NTSTATUS)(NTTERMINATEPROCESS)RealNtTerminateProcess(ProcessHandle, ExitStatus); }

其中 ANTITERM_PIDS[]数组中存着要防结束的进程的 PID。



PS:《基于SSDT HOOK技术的Ring0级进程保护组件设计与实现》系北理工2006级的学长杨蛟龙的文章,此处原文转载,因为是类似论文的文章,所以对一个问题描述的比较透彻,也比较适合拿来学习,Blog只摘录了驱动部分,也就是SSDT HOOK部分,对于DLL的控制以及驱动层和应用的通信没有涉及。没有拿到相关工程文件,上述代码只为理解文章,无法正确编译也是正常的,后续我自己也会进行实践,贴出可编译的代码(其实Google一大片)。

文章原版下载:链接


原创粉丝点击