进程保护是众多AV或者病毒都要所具备的基础功能,本文就0环下通过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 KeServiceDescriptorTable
80553fa0 80502b8c 00000000 0000011c 80503000
80553fb0 00000000 00000000 00000000 00000000
80553fc0 00000000 00000000 00000000 00000000
80553fd0 00000000 00000000 00000000 00000000
80553fe0 00002710 bf80c0b6 00000000 00000000
80553ff0 f8b67a80 f82e7b60 821bfa90 806e2f40
80554000 00000000 00000000 22bc349b 00000001
80554010 afa8a15b 01d7eb4f 00000000 00000000
kd> dd 80502b8c + 0xba*4
80502e74 805aa712 805c99e0 8060ea76 8060c43c
80502e84 8056f0d2 8063ab56 8061aca8 8061d332
80502e94 8059b804 8059c7cc 8059c1d4 8059baee
80502ea4 805bf456 80598d62 8059908e 805bf264
80502eb4 806064b6 8051ee82 8061cc3e 805cbd40
80502ec4 805cbc22 8061cd3a 8061ce20 8061cf48
80502ed4 8059a07c 8060db50 8060db50 805c892a
80502ee4 8063d80e 8060be28 80607fb8 8060882a
kd> u 805aa712
可以看到在0环调用的是NtReadVirtualMemory,这实际上才是真正实现功能的地方。而SSDT将r 3和r 0联系到一起。
在 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的服务函数结构。
有了上面的知识储备,理解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分页 %pn",RCR4));
KdPrint(("PTE1 %pn",*(Dword*)(0xC0000000 + ((HookFunAddr >> 9) & 0x007FFFF8))));
*(DWORD64*)(0xC0000000 + ((HookFunAddr >> 9) & 0x007FFFF8)) |= 0x02;
KdPrint(("PTE1 %pn",*(DWORD*)(0xC0000000 + ((HookFunAddr >> 9) & 0x007FFFF8))));
}
else
{//说明是10-10-12分页
KdPrint(("10-10-12分页n"));
KdPrint(("PTE1 %pn",*(DWORD*)(0xC0000000 + ((HookFunAddr >> 10) & 0x003FFFFC))));
*(DWORD*)(0xC0000000 + ((HookFunAddr >> 10) & 0x003FFFFC)) |= 0x02;
KdPrint(("PTE2 %pn",*(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("卸载了。。。。。n");
}
NTSTATUS DriverEntry(PDRIVER_OBJECT driver, PUNICODE_STRING reg_path)
{
_hook();
DbgPrint("跑起来了。。。n");
driver->DriverUnload = DriverUnload;
return STATUS_SUCCESS;
}
最后编译,加载驱动,当我们尝试用任务管理器杀死Dbgview时会被拒绝。
如果通过taskkill同样不行。
在SSDT上写钩子,在0环是最低级的方式,可以看到编写代码十分简单,但是也是非常容易被检测的,比如我们通过PChunter这样的内核工具去看一下。
可以看到NtOpenProcess赫然在列。实际上SSDT已作为基础需要被了解。
本文由SD原创发布
转载,请参考转载声明,注明出处: https://www.anquanke.com/post/id/262577
安全客 - 有思想的安全新媒体