SSDT Hook实现内核级的进程保护

2024-02-05 20:08

本文主要是介绍SSDT Hook实现内核级的进程保护,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

目录

  1. SSDT Hook效果图
  2. SSDT简介
  3. SSDT结构
  4. SSDT HOOK原理
  5. Hook前准备
  6. 如何获得SSDT中函数的地址呢
  7. SSDT Hook流程
  8. SSDT Hook实现进程保护
  9. Ring3与Ring0的通信
  10. 如何安装启动停止卸载服务
  11. 参考文献
  12. 源码附件
  13. 版权

SSDT Hook效果图

加载驱动并成功Hook  NtTerminateProcess函数:
当对 指定的进程进行保护后,尝试使用“任务管理器”结束进程的时候,会弹出“拒绝访问”的窗口,说明,我们的目的已经达到:

SSDT简介

SSDT 的全称是 System Services Descriptor Table,系统服务描述符表。

 

这个表就是一个把 Ring3 的 Win32 API 和 Ring0 的内核 API 联系起来。

SSDT 并不仅仅只包含一个庞大的地址索引表,它还包含着一些其它有用的信息,诸如地址索引的基地址、服务函数个数等。

通过修改此表的函数地址可以对常用 Windows 函数及 API 进行 Hook,从而实现对一些关心的系统动作进行过滤、监控的目的。

一些 HIPS、防毒软件、系统监控、注册表监控软件往往会采用此接口来实现自己的监控模块。

 

SSDT结构

SSDT即系统服务描述符表,它的结构如下(参考《Undocument Windows 2000 Secretes》第二章):
复制代码
// 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(没有导出)。
两者的区别是,KeServiceDescriptorTable仅有ntoskrnel一项,KeServieDescriptorTableShadow包含了ntoskrnel以及win32k。一般的Native API的服务地址由KeServiceDescriptorTable分派,gdi.dll/user.dll的内核API调用服务地址由KeServieDescriptorTableShadow分派。还有要清楚一点的是win32k.sys只有在GUI线程中才加载,一般情况下是不加载的,所以要Hook KeServieDescriptorTableShadow的话,一般是用一个GUI程序通过IoControlCode来触发(想当初不明白这点,蓝屏死机了N次都想不明白是怎么回事)。

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 KeServiceDescriptorTable8055ab80  804e3d20 00000000 0000011c 804d9f488055ab90  00000000 00000000 00000000 000000008055aba0  00000000 00000000 00000000 000000008055abb0  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 8057559ent!NtOpenProcess:8057559e 68c4000000      push    0C4h805755a3 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 原理所在。
另外,关于Ring3层转入Ring0层的具体流程,可以参考下我的这篇博文,对加深理解SSDT Hook技术还是有帮助的: Ring3转入Ring0跟踪

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{climov eax,cr0and 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;
}
复制代码

Ring3与Ring0的通信

请看考:张帆《Windows驱动开发技术详解》一书第7章:派遣函数

这篇关于SSDT Hook实现内核级的进程保护的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/682017

相关文章

C#提取PDF表单数据的实现流程

《C#提取PDF表单数据的实现流程》PDF表单是一种常见的数据收集工具,广泛应用于调查问卷、业务合同等场景,凭借出色的跨平台兼容性和标准化特点,PDF表单在各行各业中得到了广泛应用,本文将探讨如何使用... 目录引言使用工具C# 提取多个PDF表单域的数据C# 提取特定PDF表单域的数据引言PDF表单是一

使用Python实现高效的端口扫描器

《使用Python实现高效的端口扫描器》在网络安全领域,端口扫描是一项基本而重要的技能,通过端口扫描,可以发现目标主机上开放的服务和端口,这对于安全评估、渗透测试等有着不可忽视的作用,本文将介绍如何使... 目录1. 端口扫描的基本原理2. 使用python实现端口扫描2.1 安装必要的库2.2 编写端口扫

PyCharm接入DeepSeek实现AI编程的操作流程

《PyCharm接入DeepSeek实现AI编程的操作流程》DeepSeek是一家专注于人工智能技术研发的公司,致力于开发高性能、低成本的AI模型,接下来,我们把DeepSeek接入到PyCharm中... 目录引言效果演示创建API key在PyCharm中下载Continue插件配置Continue引言

MySQL分表自动化创建的实现方案

《MySQL分表自动化创建的实现方案》在数据库应用场景中,随着数据量的不断增长,单表存储数据可能会面临性能瓶颈,例如查询、插入、更新等操作的效率会逐渐降低,分表是一种有效的优化策略,它将数据分散存储在... 目录一、项目目的二、实现过程(一)mysql 事件调度器结合存储过程方式1. 开启事件调度器2. 创

使用Python实现操作mongodb详解

《使用Python实现操作mongodb详解》这篇文章主要为大家详细介绍了使用Python实现操作mongodb的相关知识,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录一、示例二、常用指令三、遇到的问题一、示例from pymongo import MongoClientf

SQL Server使用SELECT INTO实现表备份的代码示例

《SQLServer使用SELECTINTO实现表备份的代码示例》在数据库管理过程中,有时我们需要对表进行备份,以防数据丢失或修改错误,在SQLServer中,可以使用SELECTINT... 在数据库管理过程中,有时我们需要对表进行备份,以防数据丢失或修改错误。在 SQL Server 中,可以使用 SE

基于Go语言实现一个压测工具

《基于Go语言实现一个压测工具》这篇文章主要为大家详细介绍了基于Go语言实现一个简单的压测工具,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录整体架构通用数据处理模块Http请求响应数据处理Curl参数解析处理客户端模块Http客户端处理Grpc客户端处理Websocket客户端

Java CompletableFuture如何实现超时功能

《JavaCompletableFuture如何实现超时功能》:本文主要介绍实现超时功能的基本思路以及CompletableFuture(之后简称CF)是如何通过代码实现超时功能的,需要的... 目录基本思路CompletableFuture 的实现1. 基本实现流程2. 静态条件分析3. 内存泄露 bug

C#实现添加/替换/提取或删除Excel中的图片

《C#实现添加/替换/提取或删除Excel中的图片》在Excel中插入与数据相关的图片,能将关键数据或信息以更直观的方式呈现出来,使文档更加美观,下面我们来看看如何在C#中实现添加/替换/提取或删除E... 在Excandroidel中插入与数据相关的图片,能将关键数据或信息以更直观的方式呈现出来,使文档更

C#实现系统信息监控与获取功能

《C#实现系统信息监控与获取功能》在C#开发的众多应用场景中,获取系统信息以及监控用户操作有着广泛的用途,比如在系统性能优化工具中,需要实时读取CPU、GPU资源信息,本文将详细介绍如何使用C#来实现... 目录前言一、C# 监控键盘1. 原理与实现思路2. 代码实现二、读取 CPU、GPU 资源信息1.