我们知道在windows下面要想强行关闭一个进程的话是通过OpenProcess获取到进程的句柄,然后通过TerminateProcess这个函数关闭这个进程就好了,那么对于任务管理器毋庸置疑它也是这么干的,而如果我们要想防止任务管理器关闭我们的进程的话,只需要对TerminateProcess这个函数做点手脚就好了.
在前面的文章中已经提到,Win32API函数只是一个外壳,在内核中会有一个与之相对应的函数用于完成它的实际调用功能,Win32API将对应的服务函数的索引放到寄存器EAX中然后系统服务分发函数根据索引从服务函数表中获取到对应的函数地址完成函数的调用,这样一个R3层的Win32API就完成了它的调用功能.
如果我们要想保护我们的进程防止被强行关闭的话我们只需要把服务函数表中的对应位置指定的函数地址修改为我们的函数,让我们的函数完成对应的功能,我们用下图来描述一下我们的基本原理:
我们将原先服务表中对应的函数地址修改成我们自定义的函数地址,然后对于服务的请求当然就会调用到我们自定义的函数了,当然你也需要将原先的服务函数地址保存下来,要不就没办法完成默认处理了.
我们知道在ntoskrnl.exe 中导出了很多变量,这些变量我们在驱动中直接使用extern 就能直接使用,如下图
其中的KeServiceDescriptorTable也在其中,他是一个系统服务描述表的变量,这个变量是一个KSERVICE_TABLE_DESCRIPTOR结构体的指针(其实是个数组),我们可以通过WRK来看一下这个结构体的定义,他在WRK的\base\ntos\inc\ke.h的这个头文件中定义,如图所示:
简单的介绍一下这四个成员变量的意思:
Base:SSDT函数表的首地址(说白了就是数组首地址==);
Count:SSDT中每个函数被调用的次数
Limit:SSDT对应的函数表中服务函数的总数
Number: SSPT的基地址,我也不太清楚是干嘛的= =
我们再在ke.h文件中看一下 KeServiceDescriptorTable 这个变量到底是什么东东
可以从定义中看出,这货是一个数组,也就是说SSDT服务表是不止一个的,但是我们这里所说的这个SSDT服务表是位于这个数组的0号位置的ntoskrnl.exe的服务函数,他的1号位置是win32k.sys的服务函数(用于gdi.dll/user.dll的内核支持),通过KeServiceDescriptorTable来索引这个位置的话对应的全是00000,至于为什么是0本文暂时不做介绍。
通过上面的介绍,我们可以分析得知,通过KeServiceDescriptorTable可以获取到ntoskrnl.exe的SSDT,然后我们修改这个SSDT中的NtTerminateProcess函数的地址,让其对应到我们自定义的HookNtTerminateProcess中去,这样我们就可以完成对这个服务函数的过滤,就可以用来防止进程被强行干掉了.
下面我们来瞄瞄我的代码是如何给写的(这里只是一个示例,风格不好请见谅)
<ntddk.h>是没有这个结构体的,我们还是得声明一下
这货是 SSDT结构体
typedef struct _KSERVICE_TABLE_DESCRIPTOR { PULONG_PTR Base; PULONG Count; ULONG Limit; PUCHAR Number; } KSERVICE_TABLE_DESCRIPTOR, *PKSERVICE_TABLE_DESCRIPTOR; //直接extern出 ntoskrnl.exe导出的这个变量 extern "C" PKSERVICE_TABLE_DESCRIPTOR KeServiceDescriptorTable; //这个宏用于获取Zw函数的索引,上一篇文章已经介绍了 #define SYSCALL_INDEX(ServiceFunction) (*(PULONG)((PUCHAR)ServiceFunction + 1)) //结合上面的宏,直接作为SSDT服务函数表的数组 arr[n]; #define SYSCALL_FUNCTION(ServiceFunction) KeServiceDescriptorTable->Base[SYSCALL_INDEX(ServiceFunction)]
我们的自定义HookNtTerminateProcess函数,这里就直接返回错误信息,让所有的进程禁止关闭QAQ(我太懒了)
NTSTATUS HookNtTerminateProcess( __in_opt HANDLE ProcessHandle, __in NTSTATUS ExitStatus ) { return STATUS_ACCESS_DENIED; }
另外记得定义一个变量用于保存原本的NtTerminateProcess,驱动退出的时候总的给人家恢复吧~
//保存原始的函数地址 ULONG oldTerminalProcess = NULL;
我们在写一个函数直接在驱动的入口掉用,完成对SSDT的修改
void TestHookTerminal(){ //打印出函数表的首地址 KdPrint(("%08X \n",KeServiceDescriptorTable)); //打印一下函数表的地址看看 for(int i=0;i<KeServiceDescriptorTable->Limit;i++) { KdPrint(("%d函数地址%0x8\n",i,(KeServiceDescriptorTable->Base)[i])); } //插入一段asm修改CR0的值,让内存可读,因为默认情况下SSDT数只读的 //要不会 蓝屏的 ULONG oldcr0; _asm { cli; mov eax, cr0; mov oldcr0, eax; and eax, 0FFFEFFFFh; // CR0 16 BIT = 0 mov cr0, eax; }; //保存旧的函数地址 oldTerminalProcess = SYSCALL_FUNCTION(ZwTerminateProcess); //修改函数地址 SYSCALL_FUNCTION(ZwTerminateProcess) = (ULONG)HookNtTerminateProcess; //恢复Cr0的状态 _asm { mov eax, oldcr0 mov cr0, eax sti; } }
要记得 驱动Unload的时候要恢复原先的SSDT函数表信息哈,代码和上面的一样,就是地址信息不一样.
我们在DriverEntry函数直接调用一下上面的那个Test,测试一下效果,如图已经打印出了函数表的全部服务函数的地址:
喃后我们打开一个文本文件,试着用任务管理器强行终止,看到了吧,无情的拒绝访问.