目录
- SSDT Hook效果图
- SSDT简介
- SSDT结构
- SSDT HOOK原理
- Hook前准备
- 如何得到SSDT中函数的地址呢
- SSDT Hook流程
- SSDT Hook实现进程保护
SSDT Hook效果图
加载驱动并成功Hook NtTerminateProcess函数:html
当对 指定的进程进行保护后,尝试使用“任务管理器”结束进程的时候,会弹出“拒绝访问”的窗口,说明,咱们的目的已经达到:c++
SSDT简介
SSDT 的全称是 System Services Descriptor Table,系统服务描述符表。编程
这个表就是一个把 Ring3 的 Win32 API 和 Ring0 的内核 API 联系起来。数组
SSDT 并不单单只包含一个庞大的地址索引表,它还包含着一些其它有用的信息,诸如地址索引的基地址、服务函数个数等。函数
经过修改此表的函数地址能够对经常使用 Windows 函数及 API 进行 Hook,从而实现对一些关心的系统动做进行过滤、监控的目的。工具
一些 HIPS、防毒软件、系统监控、注册表监控软件每每会采用此接口来实现本身的监控模块。学习
SSDT结构
SSDT即系统服务描述符表,它的结构以下(参考《Undocument Windows 2000 Secretes》第二章):ui
// KSYSTEM_SERVICE_TABLE 和 KSERVICE_TABLE_DESCRIPTOR // 用来定义 SSDT 结构 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; 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;
内核中有两个系统服务描述符表,一个是KeServiceDescriptorTable(由ntoskrnl.exe导出),一个是KeServieDescriptorTableShadow(没有导出)。google
二者的区别是,KeServiceDescriptorTable仅有ntoskrnel一项,KeServieDescriptorTableShadow包含了ntoskrnel以及win32k。通常的Native API的服务地址由KeServiceDescriptorTable分派,gdi.dll/user.dll的内核API调用服务地址由KeServieDescriptorTableShadow分派。还有要清楚一点的是win32k.sys只有在GUI线程中才加载,通常状况下是不加载的,因此要Hook KeServieDescriptorTableShadow的话,通常是用一个GUI程序经过IoControlCode来触发(想当初不明白这点,蓝屏死机了N次都想不明白是怎么回事)。url
SSDT HOOK原理
关于内核 Hook 有多种类型,下面也给出一副图示:
SSDT HOOK只是其中一种Hook技术,本篇文章主要讲解SSDT Hook的使用。
SSDT HOOK原理图
经过Kernel Detective工具,咱们能够发现,SSDT Hook先后,NtTerminateProcess的当前地址会发生变化,其中,变化后的当前地址:0xF885A110为咱们自定义的Hook函数(即:HookNtTerminateProcess)的地址。这样,之后每次执行NtTerminateProcess的时候,就会根据执行“当前地址”所指向的函数了,这也就是SSDT Hook的原理。
另外,看雪的"堕落天才"写的不错,我直接引用下:
SSDT HOOK 的原理其实很是简单,咱们先实际看看KeServiceDescriptorTable是什么样的。
lkd> dd KeServiceDescriptorTable 8055ab80 804e3d20 00000000 0000011c 804d9f48 8055ab90 00000000 00000000 00000000 00000000 8055aba0 00000000 00000000 00000000 00000000 8055abb0 00000000 00000000 00000000 00000000
如上,80587691 805716ef 8057ab71 80581b5c 这些就是系统服务函数的地址了。好比当咱们在ring3调用OpenProcess时,进入sysenter的ID是0x7A(XP SP2),而后系统查KeServiceDescriptorTable,大概是这样KeServiceDescriptorTable.ntoskrnel.ServiceTableBase(804e3d20) + 0x7A * 4 = 804E3F08,而后804E3F08 ->8057559e 这个就是OpenProcess系统服务函数所在,咱们再跟踪看看:
lkd> u 8057559e nt!NtOpenProcess: 8057559e 68c4000000 push 0C4h 805755a3 6860b54e80 push offset nt!ObReferenceObjectByPointer+0x127 (804eb560) 805755a8 e8e5e4f6ff call nt!InterlockedPushEntrySList+0x79 (804e3a92) 805755ad 33f6 xor esi,esi
原来8057559e就是NtOpenProcess函数所在的起始地址。
嗯,若是咱们把8057559e改成指向咱们函数的地址呢?好比 MyNtOpenProcess,那么系统就会直接调用MyNtOpenProcess,而不是原来的NtOpenProcess了。这就是SSDT HOOK 原理所在。
Hook前准备
咱们要修改SSDT表,首先这个表必须是可写的,但在xp之后的系统中他都是只读的,三个办法来修改内存保护机制
(1) 更改注册表
恢复页面保护:HKLM\SYSTEM\CurrentControlset\Control\Session Manger\Memory Management\EnforceWriteProtection=0
去掉页面保护:HKLM\SYSTEM\CurrentControlset\Control\Session Manger\Memory Management\DisablePagingExecutive=1
(2)改变CR0寄存器的第1位
Windows对内存的分配,是采用的分页管理。其中有个CR0寄存器,以下图:
其中第1位叫作保护属性位,控制着页的读或写属性。若是为1,则能够读/写/执行;若是为0,则只能够读/执行。
SSDT,IDT的页属性在默认下都是只读,可执行的,但不能写。
代码以下:
//设置为不可写 void DisableWrite() { __try { _asm { mov eax, cr0 or eax, 10000h mov cr0, eax sti } } __except(1) { DbgPrint("DisableWrite执行失败!"); } } // 设置为可写 void EnableWrite() { __try { _asm { cli mov eax,cr0 and eax,not 10000h //and eax,0FFFEFFFFh mov cr0,eax } } __except(1) { DbgPrint("EnableWrite执行失败!"); } }
(3)经过Memory Descriptor List(MDL)
具体作法能够google下,这里就不介绍了
如何得到SSDT中函数的地址呢?
这里主要使用了两个宏:
①获取指定服务的索引号:SYSCALL_INDEX
②获取指定服务的当前地址:SYSCALL_FUNCTION
这两个宏的具体定义以下:
//根据 ZwServiceFunction 获取 ZwServiceFunction 在 SSDT 中所对应的服务的索引号 #define SYSCALL_INDEX(ServiceFunction) (*(PULONG)((PUCHAR)ServiceFunction + 1)) //根据ZwServiceFunction 来得到服务在 SSDT 中的索引号,而后再经过该索引号来获取ntServiceFunction的地址 #define SYSCALL_FUNCTION(ServiceFunction) KeServiceDescriptorTable->ntoskrnl.ServiceTableBase[SYSCALL_INDEX(ServiceFunction)]
SSDT Hook流程
在驱动的入口函数中(DriverEntry),对未进行SSDT Hook前的SSDT表进行了备份(用一个数组保存),备份时,一个索引号对应一个当前地址,如上图所示。
这样,在解除Hook的时候,就能够从全局数组中根据索引号获取未Hook前的服务名的当前地址,以便将原来的地址写回去,这一步很重要。
当用户选择保护某个进程的时候,就会经过DeviceIoControl发送一个IO_INSERT_PROTECT_PROCESS控制码给驱动程序,此时驱动程序会生成一个IRP:IRP_MJ_DEVICE_CONTROL,咱们事先已经在驱动程序中为
IRP_MJ_DEVICE_CONTROL指定了一个派遣函数:SSDTHook_DispatchRoutine_CONTROL。在该派遣函数中:咱们经过获取控制码(是保护进程仍是取消保护进程),若是是要保护某个进程,则经过 DeviceIoControl的第3个参数将要保护的进程的pid传递给驱动程序。而后在派遣函数SSDTHook_DispatchRoutine_CONTROL中从缓冲区中读取该pid,若是是要保护进程,则将要“保护进程”的pid添加到一个数组中,若是是要“取消保护进程”,则将要取消保护的进程PID从数组中移除。
在Hook NtTermianteProcess函数后,会执行咱们自定义的函数:HookNtTerminateProcess,在HookNtTerminateProcess函数中,咱们判断当前进程是否在要保护的进程数组中,若是该数组中存在该pid,则咱们返回一个“权限不够”的异常,若是进程保护数组中不存在该pid,则直接调用原来 SSDT 中的 NtTerminateProcess 来结束进程。
SSDT Hook实现进程保护
有了上面的理论基础以后,接下来能够谈谈SSDT Hook实现进程保护的具体实现了。
实现进程保护,能够Hook NtTermianteProcess,另外也能够Hook NtOpenProcess,这里,我是Hook NtTermianteProcess。
SSDT Hook原理一节中已经说过,SSDT Hook原理的本质是:自定义一个函数(HookNtTerminateProcess),让系统服务NtTermianteProcess的当前地址指向咱们自定义函数地址。
这一步工做是在驱动入口函数中执行的。当驱动加载的时候,将自定义函数的地址写入SSDT表中NtTermianteProcess服务的当前地址:
// 实现 Hook 的安装,主要是在 SSDT 中用 newService 来替换掉 oldService NTSTATUS InstallHook(ULONG oldService, ULONG newService) { __try { ULONG uOldAttr = 0; EnableWrite(); //去掉页面保护 KdPrint(("伪造NtTerminateProcess地址: %x\n",(int)newService)); //KeServiceDescriptorTable->ntoskrnl.ServiceTableBase[SYSCALL_INDEX(oldService)] = newService; SYSCALL_FUNCTION(oldService) = newService;// DisableWrite(); //恢复页面保护 return STATUS_SUCCESS; } __except(1) { KdPrint(("安装Hook失败!")); } }
这里须要注意的是:在Hook前,须要去掉内存的页面保护属性,Hook后,须要回复内存的页面保护属性。
HookNtTerminateProcess函数的代码以下:
//************************************ // 函数名称 : HookNtTerminateProcess // 描 述 : 自定义的 NtOpenProcess,用来实现 Hook Kernel API // 日 期 : 2013/06/28 // 参 数 : ProcessHandle:进程句柄 ExitStatus: // 返 回 值 : //************************************ NTSTATUS HookNtTerminateProcess(__in_opt HANDLE ProcessHandle,__in NTSTATUS ExitStatus) { ULONG uPID; NTSTATUS rtStatus; PCHAR pStrProcName; PEPROCESS pEProcess; ANSI_STRING strProcName; // 经过进程句柄来得到该进程所对应的 FileObject 对象,因为这里是进程对象,天然得到的是 EPROCESS 对象 rtStatus = ObReferenceObjectByHandle(ProcessHandle, FILE_READ_DATA, NULL, KernelMode, (PVOID*)&pEProcess, NULL); if (!NT_SUCCESS(rtStatus)) { return rtStatus; } // 保存 SSDT 中原来的 NtTerminateProcess 地址 pOldNtTerminateProcess = (NTTERMINATEPROCESS)oldSysServiceAddr[SYSCALL_INDEX(ZwTerminateProcess)]; // 经过该函数能够获取到进程名称和进程 ID,该函数在内核中实质是导出的(在 WRK 中能够看到) // 可是 ntddk.h 中并无处处,因此须要本身声明才能使用 uPID = (ULONG)PsGetProcessId(pEProcess); pStrProcName = _strupr((TCHAR *)PsGetProcessImageFileName(pEProcess));//使用微软未公开的PsGetProcessImageFileName函数获取进程名 // 经过进程名来初始化一个 ASCII 字符串 RtlInitAnsiString(&strProcName, pStrProcName); if (ValidateProcessNeedProtect(uPID) != -1) { // 确保调用者进程可以结束(这里主要是指 taskmgr.exe) if (uPID != (ULONG)PsGetProcessId(PsGetCurrentProcess())) { // 若是该进程是所保护的的进程的话,则返回权限不够的异常便可 return STATUS_ACCESS_DENIED; } } // 对于非保护的进程能够直接调用原来 SSDT 中的 NtTerminateProcess 来结束进程 rtStatus = pOldNtTerminateProcess(ProcessHandle, ExitStatus); return rtStatus; }


好了,文章就写到这吧,最后推荐一下不错的c/c++零基础小白到企业级项目实战课程,天天晚上八点都会有直播,学习编程的朋友不妨点一下免费报名,上课的时候会有通知,有时间的时候就能够去听听哦