【技术分享】r0下的进程保护
安全客 发布于 2022-01-17 17:00:36 阅读 1852 https://www.wangan.com/p/7fy7fg9985331389前言
进程保护是众多 AV 或者病毒都要所具备的基础功能,本文就 0 环下通过 SSDT 来对进程进行保护进行探究,SSDT 也不是什么新技术,但作为学习,老的技术我们同样需要掌握。
什么是 SSDT
SSDT 的全称是 System Services Descriptor Table,系统服务描述符表。
首先要明确的是他是一张表,通过 windbg 查看这张表。
dd KeServiceDescriptorTable
这个表就是一个把 Ring3 的 Win32 API 和 Ring0 的内核 API 联系起来。
当我们在 r 3 调用一个 API 时,实际上调用的是一个接口,这里拿 ReadProcessMemory 举例。
ReadProcessMemory 函数在 kernel32.dll 中导出,通过断点可以找到对应的反汇编代码。在汇编代码中,可以看到 ReadProcessMemory 调用了 ntdll.dll 中的 ZwReadVirtualMemory 函数。
在 ZwReadVirtualMemory 函数开始的地方下断点。
bp ZwReadVirtualMemory
实际上功能代码也没有在 ZwReadVirtualMemory 函数中实现,只是拿着一个索引号并跳转到一个地址。
这个索引号实际上就是 SSDT 表中的索引号,回到 windbg,我们现在拿到索引号 0xBA 去 SSDT 表中找。
kd> dd KeServiceDescriptorTable80553fa0 80502b8c 00000000 0000011c 8050300080553fb0 00000000 00000000 00000000 0000000080553fc0 00000000 00000000 00000000 0000000080553fd0 00000000 00000000 00000000 0000000080553fe0 00002710 bf80c0b6 00000000 0000000080553ff0 f8b67a80 f82e7b60 821bfa90 806e2f4080554000 00000000 00000000 22bc349b 0000000180554010 afa8a15b 01d7eb4f 00000000 00000000kd> dd 80502b8c + 0xba*480502e74 805aa712 805c99e0 8060ea76 8060c43c80502e84 8056f0d2 8063ab56 8061aca8 8061d33280502e94 8059b804 8059c7cc 8059c1d4 8059baee80502ea4 805bf456 80598d62 8059908e 805bf26480502eb4 806064b6 8051ee82 8061cc3e 805cbd4080502ec4 805cbc22 8061cd3a 8061ce20 8061cf4880502ed4 8059a07c 8060db50 8060db50 805c892a80502ee4 8063d80e 8060be28 80607fb8 8060882akd> u 805aa712
可以看到在 0 环调用的是 NtReadVirtualMemory,这实际上才是真正实现功能的地方。而 SSDT 将 r 3 和 r 0 联系到一起。
SSDT 结构
在 NT 4.0 以上的 Windows 操作系统中,默认就存在两个系统服务描述表,这两个调度表对应了两类不同的系统服务,
这两个调度表为:KeServiceDescriptorTable 和 KeServiceDescriptorTableShadow,
其中 KeServiceDescriptorTable 主要是处理来自 Ring3 层 Kernel32.dll 中的系统调用,
而 KeServiceDescriptorTableShadow 则主要处理来自 User32.dll 和 GDI32.dll 中的系统调用,
并且 KeServiceDescriptorTable 在 ntoskrnl.exe (Windows 操作系统内核文件,包括内核和执行体层) 是导出的,
而 KeServiceDescriptorTableShadow 则是没有被 Windows 操作系统所导出,
而关于 SSDT 的全部内容则都是通过 KeServiceDescriptorTable 来完成的。
SSDT 表的结构通过结构体表示为如下:
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。通过结构体表示为如下:
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;
通过看图形化界面可以更加直观,下图是 ntoskrnl.exe 和 win32k.sys 的服务函数结构。
HOOK SSDT
有了上面的知识储备,理解 SSDT HOOK 就很容易了。
当 3 环程序执行后,操作系统拿着索引去 SSDT 表中找对应的 0 环程序,这时我们就可以在 SSDT 表中做点手脚,将某一个 api 函数的指针改成我们自己函数的指针,这样执行的将会是我们自己的代码。
首先需要定义我们自己的函数
ULONG g_Pid = 568;NTSTATUS NTAPI MyOpenProcess(PHANDLE ProcessHandle, ACCESS_MASK DesiredAccess, POBJECT_ATTRIBUTES ObjectAttributes, PCLIENT_ID ClientId){ NTSTATUS status; status = STATUS_SUCCESS; //当此进程为要保护的进程时 if (ClientId->UniqueProcess == (HANDLE)g_Pid) { //设为拒绝访问 DesiredAccess = 0; } return NtOpenProcess(ProcessHandle, DesiredAccess, ObjectAttributes, ClientId);}
g_Pid 定义为全局的,我们想保护哪个进程就将该进程的 pid 赋值给 g_Pid。
比如这里就保护 Dbgview.exe。
这里函数准备好以后,就要将该函数的指针覆盖原来 NtOpenProcess 的指针。但是需要注意的是:我们自己改自己的代码是不用管权限的,改别人的代码很有可能这块内存是只读的,并不可写。
那么本质上就是 SSDT 对应的物理页是只读的,这里有两种办法,我们都知道物理页的内存 R/W 位的属性是由 PDE 和 PTE 相与而来的,那么我们就可以改变 SSDT 对应的 PDE 和 PTE 的 R/W 属性,将物理页设置为可读可写的。通过 CR4 寄存器判断是 2-9-9-12 分页还是 10-10-12 分页。
if(RCR4 & 0x00000020){//说明是2-9-9-12分页 KdPrint(("2-9-9-12分页 %p",RCR4)); KdPrint(("PTE1 %p",*(DWORD*)(0xC0000000 + ((HookFunAddr >> 9) & 0x007FFFF8)))); *(DWORD64*)(0xC0000000 + ((HookFunAddr >> 9) & 0x007FFFF8)) |= 0x02; KdPrint(("PTE1 %p",*(DWORD*)(0xC0000000 + ((HookFunAddr >> 9) & 0x007FFFF8))));}else{//说明是10-10-12分页 KdPrint(("10-10-12分页")); KdPrint(("PTE1 %p",*(DWORD*)(0xC0000000 + ((HookFunAddr >> 10) & 0x003FFFFC)))); *(DWORD*)(0xC0000000 + ((HookFunAddr >> 10) & 0x003FFFFC)) |= 0x02; KdPrint(("PTE2 %p",*(DWORD*)(0xC0000000 + ((HookFunAddr >> 10) & 0x003FFFFC))));}
还有一种方式就是通过 Cr0 寄存器。CR0 寄存器的第 16 位叫做保护属性位,控制着页的读或写属性。
WP 为 1 时,不能修改只读的内存页,WP 为 0 时,可以修改只读的内存页。那么我们就可以将 WP 位置为 0,暂时关闭可读属性。
VOID PageProtectOn(){ __try { _asm { mov eax, cr0 or eax, 10000h mov cr0, eax sti } } __except (1) { DbgPrint("PageProtectOn执行失败!"); }}VOID PageProtectOff(){ __try { _asm { cli mov eax, cr0 and eax, not 10000h //and eax,0FFFEFFFFh mov cr0, eax } } __except (1) { DbgPrint("PageProtectOff执行失败!"); }}
可以修改 SSDT 表后,就要写函数来修改 NtOpenProcess 指针,也就是我们的 HOOK 函数。
NTSTATUS _hook(){ NTSTATUS status; status = STATUS_SUCCESS; PageProtectOff(); PoldAddress = KeServiceDescriptorTable->ntoskrnl.ServiceTableBase[0x7a]; KeServiceDescriptorTable->ntoskrnl.ServiceTableBase[0x7a] = (ULONG)MyOpenProcess; PageProtectOn(); return status;}
在修改 SSDT 表前先关闭物理页保护,修改完后要开启物理页保护,保证其他任务能够顺利完成。这里的索引可以通过 ida 或者 debug 工具去看。
然后就是卸载钩子,用于驱动卸载的时候使用。
NTSTATUS _unhook(){ NTSTATUS status; status = STATUS_SUCCESS; PageProtectOff(); KeServiceDescriptorTable->ntoskrnl.ServiceTableBase[0x7a] = PoldAddress; PageProtectOn(); return status;}
最后是入口和卸载函数
VOID DriverUnload(PDRIVER_OBJECT driver){ _unhook(); DbgPrint("卸载了。。。。。");} NTSTATUS DriverEntry(PDRIVER_OBJECT driver, PUNICODE_STRING reg_path){ _hook(); DbgPrint("跑起来了。。。"); driver->DriverUnload = DriverUnload; return STATUS_SUCCESS;}
最后编译,加载驱动,当我们尝试用任务管理器杀死 Dbgview 时会被拒绝。
如果通过 taskkill 同样不行。
后记
在 SSDT 上写钩子,在 0 环是最低级的方式,可以看到编写代码十分简单,但是也是非常容易被检测的,比如我们通过 PChunter 这样的内核工具去看一下。
可以看到 NtOpenProcess 赫然在列。实际上 SSDT 已作为基础需要被了解。
标签:status,10,函数,00000000,hook,pchunter,TABLE,todo,SSDT From: https://www.cnblogs.com/bonelee/p/17020586.html