驱动开发:挂接SSDT内核钩子

简介: SSDT 中文名称为系统服务描述符表,该表的作用是将Ring3应用层与Ring0内核层,两者的API函数连接起来,起到承上启下的作用,SSDT并不仅仅只包含一个庞大的地址索引表,它还包含着一些其它有用的信息,诸如地址索引的基址、服务函数个数等,SSDT 通过修改此表的函数地址可以对常用 Windows 函数进行内核级的Hook,从而实现对一些核心的系统动作进行过滤、监控的目的,接下来将演示如何通过编写简单的驱动程序,来实现搜索 SSDT 函数的地址,并能够实现简单的内核 Hook 挂钩。

SSDT 中文名称为系统服务描述符表,该表的作用是将Ring3应用层与Ring0内核层,两者的API函数连接起来,起到承上启下的作用,SSDT并不仅仅只包含一个庞大的地址索引表,它还包含着一些其它有用的信息,诸如地址索引的基址、服务函数个数等,SSDT 通过修改此表的函数地址可以对常用 Windows 函数进行内核级的Hook,从而实现对一些核心的系统动作进行过滤、监控的目的,接下来将演示如何通过编写简单的驱动程序,来实现搜索 SSDT 函数的地址,并能够实现简单的内核 Hook 挂钩。

在开始编写驱动之前,我们先来分析一下Ring3到Ring0是如何协作的,这里通过C语言调用 OpenProcess 函数,并分析它的执行过程,先来创建一个C程序。

#include <windows.h>

int main(int argc, char* argv[])
{
    HANDLE handle = OpenProcess(PROCESS_ALL_ACCESS,FALSE,2548);
    return 0;
}

通过VC6编译器编译,并使用OD载入程序,找到程序的OEP,分析第一次调用,可以看到CALL的地址是 <&KERNEL32.OpenProcess> 此处我们F7直接跟进这个CALL。

00401028  |.  8BF4          mov     esi, esp
0040102A  |.  68 F4090000   push    0x9F4                                       ; /ProcessId = 0x9F4
0040102F  |.  6A 00         push    0x0                                         ; |Inheritable = FALSE
00401031  |.  68 FF0F1F00   push    0x1F0FFF                                    ; |Access = PROCESS_ALL_ACCESS
00401036  |.  FF15 4CA14200 call    dword ptr [<&KERNEL32.OpenProcess>]         ; \OpenProcess

此时我们已经进入到了 00401036这个地址中,观察下方的代码,发现其调用了&ntdll.NtOpenProcess 这个函数,我们继续F7跟进。

75BC83C3    50              push    eax
75BC83C4    8975 FC         mov     dword ptr [ebp-0x4], esi
75BC83C7    C745 E0 1800000>mov     dword ptr [ebp-0x20], 0x18
75BC83CE    8975 E4         mov     dword ptr [ebp-0x1C], esi
75BC83D1    8975 E8         mov     dword ptr [ebp-0x18], esi
75BC83D4    8975 F0         mov     dword ptr [ebp-0x10], esi
75BC83D7    8975 F4         mov     dword ptr [ebp-0xC], esi
75BC83DA    FF15 D411BC75   call    dword ptr [<&ntdll.NtOpenProcess>]   ; ntdll.ZwOpenProcess

当我们进入到NtOpenProcess这个函数时,会看到以下代码,其中0xBE将其转换成十进制是190

77A05D88 >  B8 BE000000     mov     eax, 0xBE
77A05D8D    BA 0003FE7F     mov     edx, 0x7FFE0300
77A05D92    FF12            call    dword ptr [edx]
77A05D94    C2 1000         retn    0x10

通过使用Xuetr工具对比,可以发现这个0xBE正好就是 NtOpenProcess函数在内核中的调用号,此时我们继续F7进入到call dword ptr [edx] 地址中,可以看到以下代码片段。

77A070AC    8D6424 00       lea     esp, dword ptr [esp]
77A070B0 >  8BD4            mov     edx, esp
77A070B2    0F34            sysenter
77A070B4 >  C3              retn

发现已经到达Ring3层的终点了,其中 sysenter 指令就是用来快速调用一个 Ring0 层的系统过程,简单来说就是将用户层代码向内核层发起的系统调用,由 ntoskrnl.exe 程序向内核发送IO请求,然后内核与驱动程序返回执行的结果。

网上找到一张图,可以很好的解释这个调用的顺序。

进入用户层:kernel32(OpenProcess) -> ntdll(NTOpenProcess)->ntdll(SysEnter)
进入内核层:ntoskrnl.exe(nt!ZWOpenProcess) -> ntoskrnl.exe(nt!KiSystemService) -> ntoskrnl.exe(nt!NtOpenProccess)

image.png


读取 SSDT 获得函数地址

上面的实验我们通过一个函数的调用流程了解到了用户层与内核层的通信过程,其中提到了SSDT索引号的相关概念,SSDT索引号在系统中是固定不变的,利用这个特性就可以定位到原始API函数地址。

Windows 系统提供的SSDT表其作用就是方便应用层之间API的调用,所有的API调用都会转到SSDT这张表中进行参考,这样就能够使不同的API调用全部都转到对应的SSDT表中,从而方便管理。

在SSDT表中有一个 KeServiceDescriptorTable的结构,该结构是由内核导出的表,该表拥有一个指针,指向SSDT中包含由 Ntoskrnl.exe 实现的核心系统服务的相应部分,ntoskrnl.exe 中导出了PSERVICE_DESCRPITOR_TABLE类型指针,变量为 KeServiceDescriptorTable 它是内核的主要组成部分,该表结构如下:

typedef struct _SERVICE_DESCRIPTOR_TABLE
{
    PULONG ServiceTableBase;                      // SSDT 指针,服务表基址
    PULONG ServiceCounterTableBase;         // 包含 SSDT 中每个服务被调用次数的计数器
    ULONG  NumberOfService;                        // SSDT 索引数目
    PUCHAR ParamTableBase;             // 包含每个系统服务参数字节数表的基地址-系统服务参数表 
} SERVICE_DESCRIPTOR_TABLE,*PSERVICE_DESCRIPTOR_TABLE;

表结构中的 SERVICE_DESCRIPTOR_TABLE 包含了所有内核导出函数的地址,在32位系统中每个地址长度为4个字节,所以要获得某个函数在SSDT中的偏移量,可以使用 KeServiceDescriptorTable->ServiceTableBase + 函数ID * 4 的方式来得到。

上方都是一些理论部分,接着我们通过使用WinDBG来具体查看一些这个表的一些结构信息,此处测试系统是XP

打开WinDBG调试器,选择【File -> Kernel Debug -> Local -> OK】输入以下命令完成符号文件的加载。

lkd> .sympath srv*d:\symbols*http://msdl.microsoft.com/download/symbol
lkd> .reload

Connected to Windows XP 2600 x86 compatible target at (Sat Sep 21 07:23:56.796 2019 (UTC + 8:00)), ptr64 FALSE
Loading Kernel Symbols

当符号文件加载完成以后,在命令窗口输入 dd KeServiceDescriptorTable 命令。

lkd> dd KeServiceDescriptorTable

8055d700  80505570 00000000 0000011c 805059e4
8055d710  00000000 00000000 00000000 00000000
8055d720  00000000 00000000 00000000 00000000
8055d730  00000000 00000000 00000000 00000000
8055d740  00000002 00002710 bf80c401 00000000
8055d750  b69c4a80 b8e4ab60 8ad620f0 806f80c0
8055d760  00000000 00000000 fee134ac ffffffff
8055d770  5a5a626c 01d56f51 00000000 00000000

从以上结构定义可看出,SSDT 的首地址为 80505570 该地址对应结构中的 ServiceTableBase,可索引的函数有11c对应结构中的NumberOfService,由于SSDT是数组结构,所以里面存放了所有的 nt!nt* 函数的地址,使用 dd kiservicetable 查看 SSDT 下的所有数组成员信息。

lkd> dd Kiservicetable

80505570  805a5664 805f23ea 805f5c20 805f241c
80505580  805f5c5a 805f2452 805f5c9e 805f5ce2
80505590  80616e80 806180e4 805ed7e8 805ed440
805055a0  805d5c0c 805d5bbc 806174a6 805b6fea
805055b0  80616ac2 805a9aee 805b15fe 805d76d0
805055c0  805028e8 805c96a4 80577b04 80539d88
805055d0  80610090 805bd564 805f615a 80624e3a
805055e0  805fa66e 805a5d52 8062508e 805a5604

为了能够定位到我们所需要的函数调用号,我们还需要手动查找一下 ZwOpenProcess 这个函数的ID号,可以使用WinDBG来获取,如下显示调用号为 7A

lkd> u ZwOpenProcess

ntdll!ZwOpenProcess:
7c92d5fe b87a000000      mov     eax,7Ah
7c92d603 ba0003fe7f      mov     edx,offset SharedUserData!SystemCallStub (7ffe0300)
7c92d608 ff12            call    dword ptr [edx]
7c92d60a c21000          ret     10h
7c92d60d 90              nop

上方代码可以得到 nt!NTOpenProcess地址在SSDT表中的索引号。

lkd> dd kiservicetable +0x7A * 4 l 1
80502d74  805c2296

lkd> u 805c2296
nt!NtOpenProcess:
805c2296 68c4000000      push    0C4h
805c229b 68a8aa4d80      push    offset nt!ObWatchHandles+0x25c (804daaa8)
805c22a0 e86b6cf7ff      call    nt!_SEH_prolog (80538f10)

如果符号文件没有加载成功,可以使用下面的方式来查询,找到结构的首地址,然后与函数编号相加来获取。

lkd> dd KeServiceDescriptorTable
80553fa0  80502b8c 00000000 0000011c 80503000

lkd> dd 80502b8c+0x7A*4
80502d74  805c2296 805e49fc 805e4660 805a0722

lkd> u 805c2296
nt!NtOpenProcess:
805c2296 68c4000000      push    0C4h
805c229b 68a8aa4d80      push    offset nt!FsRtlLegalAnsiCharacterArray+0x2008 (804daaa8)
805c22a0 e86b6cf7ff      call    nt!wctomb+0x45 (80538f10)
805c22a5 33f6            xor     esi,esi

注意:在验证的时候需要请关闭杀毒软件,因为杀毒软件会Hook这些地址来达到防御的目的,Hook后这些地址会发生变化无法完成整个查询过程,另外ZwOpenProcessNtOpenProcess其实是一回事。

编写驱动程序: 接着我们分别使用C语言和汇编实现读取系统的SSDT表,此处使用的系统是Win7,由于 Win7 系统默认情况下本地内核调试功能被屏蔽了,所以必须在控制台下运行 bcdedit -debug on 命令并且重启来进入调试模式。

进入调试模式后,我们首先通过WinDBG调试器,来查询一下ZwOpenProcess函数的调用号,执行命令如下。

lkd> u ZwOpenProcess
nt!ZwOpenProcess:
83c8a62c b8be000000      mov     eax,0BEh
83c8a631 8d542404        lea     edx,[esp+4]
83c8a635 9c              pushfd
83c8a636 6a08            push    8
83c8a638 e8b1190000      call    nt!ZwYieldExecution+0x95a (83c8bfee)
83c8a63d c21000          ret     10h
nt!ZwOpenProcessToken:
83c8a640 b8bf000000      mov     eax,0BFh
83c8a645 8d542404        lea     edx,[esp+4]

上方代码中可以看到mov eax,0BEh其中的BE就是ZwOpenProcess函数在当前系统下的调用号,我们将其转换为十进制是190 当然也可以使用Xuetr等工具来查询。

image.png

接着我们来编译以下驱动代码,重要的内容已经备注好了,唯一需要更改的地方是 SSDT_Adr = (PLONG)(STB_addr + 0x7A * 4); 其中的0x7A需要改为0xBE

#include <ntddk.h>

//声明:服务描述表 结构 4个参数
typedef struct _ServiceDescriptorTable {
    PULONG ServiceTableBase;                  // 服务表基址 
    PULONG ServiceCounterTable;               // 服务计数器
    ULONG NumberOfServices;           // 服务的数目  
    PUCHAR ParamTableBase;                    // 系统服务参数表 
}*PServiceDescriptorTable;

// 用指针PServiceDescriptorTable指向:_ServiceDescriptorTable服务描述表结构
// 必须extern "C" ,因为文件为CPP
extern "C" PServiceDescriptorTable KeServiceDescriptorTable;

void UnloadDriver(PDRIVER_OBJECT pDriver)
{
    DbgPrint("驱动已卸载!\n");
}

NTSTATUS DriverEntry(PDRIVER_OBJECT pDriver, PUNICODE_STRING str)
{
    //*SSDT_Adr  存放系统描述符号表地址。
    //STB_addr  存放 ServiceTableBase 服务表基址。
    //SDT_Nt    函数 NtOpenProcess的当前地址。
    LONG *SSDT_Adr, STB_addr, SSDT_NtOpenProcess_Addr;

    DbgPrint("驱动程序已加载! \n\r");
    STB_addr = (LONG)KeServiceDescriptorTable->ServiceTableBase;
    DbgPrint("当前服务表基址址 %x \n", STB_addr);
    SSDT_Adr = (PLONG)(STB_addr + 0xBE * 4);                          // 此处需要修改
    DbgPrint("当前STB_addr+0xBE*4= %x \n", SSDT_Adr);
    SSDT_NtOpenProcess_Addr = *SSDT_Adr;
    DbgPrint("当前NtOpenProcess地址 %x \n", SSDT_NtOpenProcess_Addr);

    pDriver->DriverUnload = UnloadDriver;
    return STATUS_SUCCESS;
}

编译程序以后,将其拖入虚拟机,打开DebugVIew工具,然后加载这个驱动程序,观察是否能够读取到我们想要的数据。

image.png

再次编译下方的汇编版本,调试观察,结果与C版本相同。

#include <ntddk.h>

extern "C" LONG KeServiceDescriptorTable;

void UnloadDriver(PDRIVER_OBJECT pDriver)
{
    DbgPrint("驱动卸载成功\n\r");
}
NTSTATUS DriverEntry(PDRIVER_OBJECT pDriver, PUNICODE_STRING str)
{
    ULONG SSDT_Addr;
    DbgPrint("驱动加载成功! \n");
    __asm
    {
        push ebx
        push eax
        mov ebx, KeServiceDescriptorTable    // 系统描述符号表的地址
        mov ebx, [ebx]                       // 取服务表基址给EBX
        mov eax, 0xBE                        // NtOpenProcess=转成十六进制等于BE
        imul eax, eax, 4                      // eax=eax*4 -> 7a*4=1e8
        add ebx, eax                         // eax=1e8与服务表基址EBX相加
        mov ebx, [ebx]                       // 取ebx里面的内容给EBX
        mov SSDT_Addr, ebx                   // 将得到的基址给变量
        pop eax
        pop ebx
    }
    DbgPrint("读取SSDT_NtOpenProcess_Addr=%0x \n", SSDT_Addr);

    pDriver->DriverUnload = UnloadDriver;
    return STATUS_SUCCESS;
}

image.png


Hook 挂钩内核函数

挂钩函数有多种用途,下面的第一种挂钩方式可以实现对特定内核函数的重写,而第二种挂钩方式则可以用于驱动保护。

Hook挂钩重写函数: 挂钩代码如下,关键点已经备注清楚了,编译这段代码,并放入虚拟机执行。

#include <ntddk.h>

#ifdef __cplusplus
extern "C" NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING  RegistryPath);
#endif

void SSDTHookUnload(PDRIVER_OBJECT);
// ******************************************************************************
//这个结构是服务描述表
typedef struct _SERVICE_DESCRIPTOR_TABLE
{
    PULONG ServiceTableBase;
    PULONG ServiceCounterTableBase;
    ULONG  NumberOfService;
    PUCHAR ParamTableBase;
}SERVICE_DESCRIPTOR_TABLE, *PSERVICE_DESCRIPTOR_TALBE;
extern "C" PSERVICE_DESCRIPTOR_TALBE KeServiceDescriptorTable;

// ******************************************************************************
// 此处为函数的原型声明部分,可通过百度查询到
typedef NTSTATUS(*NtOpenProcessEx)(
    OUT PHANDLE ProcessHandle,
    IN ACCESS_MASK AccessMask,
    IN PVOID ObjectAttributes,
    IN PCLIENT_ID Clientld);

NtOpenProcessEx ulNtOpenProcessEx = NULL;       // 函数指针,存放原始函数地址
ULONG ulNtOpenProcessExAddr = 0;                  // 在SSDT中的函数地址的指针

// ******************************************************************************
// SSDT 内核内存页默认只读,需要修改CR0 WP位实现读写
// CR0  的第16位是WP位,为0可读写,为1则只读
void REMOVE_ONLY_READ()
{
    __asm
    {
        push eax
            mov eax, CR0
            and eax, ~10000h //16th bit is 0
            mov CR0, eax
            pop eax
    }
}

void RESET_ONLY_READ()
{
    __asm
    {
        push eax
            mov eax, CR0
            or eax, 10000h   //16th bit is 1
            mov CR0, eax
            pop eax
    }
}
// ******************************************************************************
// 此处就是我们DIY的函数,声明要和NtOpenProcessEx保持一致.
NTSTATUS MyNtOpenProcessEx(
    OUT PHANDLE ProcessHandle,
    IN ACCESS_MASK AccessMask,
    IN PVOID ObjectAttributes,
    IN PCLIENT_ID Clientld)
{
    //DbgPrint("执行我自己的驱动函数\r\n");
    NTSTATUS Status = STATUS_SUCCESS;
    Status = ulNtOpenProcessEx(
        ProcessHandle,
        AccessMask,
        ObjectAttributes,
        Clientld
        );
    return Status;
}
// ******************************************************************************
VOID HookOpenProcess()
{
    ULONG ulSsdt = 0;
    ulSsdt = (ULONG)KeServiceDescriptorTable->ServiceTableBase;              // 读取到SSDT表的基地址
    ulNtOpenProcessExAddr = ulSsdt + 0xBE * 4;                               // 索引到指定的函数
    ulNtOpenProcessEx = (NtOpenProcessEx)*(PULONG)ulNtOpenProcessExAddr;     // 存储原始函数地址
    REMOVE_ONLY_READ();                                                      // 关闭只读保护
    *(PULONG)ulNtOpenProcessExAddr = (ULONG)MyNtOpenProcessEx;               // 将新函数地址赋值
    RESET_ONLY_READ();                                                       // 开启只读保护
}
// ******************************************************************************
NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING  RegistryPath)
{
    DbgPrint("驱动加载完成 !\n");
    DriverObject->DriverUnload = SSDTHookUnload;
    HookOpenProcess();
    return STATUS_SUCCESS;
}
void SSDTHookUnload(IN PDRIVER_OBJECT DriverObject)
{
    REMOVE_ONLY_READ();
    *(PULONG)ulNtOpenProcessExAddr = (ULONG)ulNtOpenProcessEx;
    RESET_ONLY_READ();
    DbgPrint("驱动卸载完成 !\n");
}

当驱动被加载时,可以通过Xuetr查看到内核SSDT层出现了红色的钩子。

image.png

驱动进程保护: 进程的创建离不开 ZwTerminateProcess 这个函数的支持,所以我们只需要Hook这个函数并在其内部判断是否是计算器进程,如果是则返回错误,否则返回原始调用,即可完成进程保护,这里就不演示了,核心伪代码如下。

#define EXE_Name "calc.exe"   // 要检索的进程名

PEPROCESS  processEPROCESS = NULL;          // 保存访问者的EPROCESS
ANSI_STRING  p_StrName1, p_StrName2;        // 保存进程名称

__declspec(naked) VOID  inline_Hook()
{
    processEPROCESS = IoGetCurrentProcess();               // 获取调用者的EPROCESS保存
    DbgPrint("EPROCESS=%x", processEPROCESS);              // 打印出来

    // 通过遍历将调用者名字保存到p_StrName1中,下方+0x174是表结构 ImageFileName 的偏移地址
    RtlInitAnsiString(&p_StrName1, (PUCHAR)processEPROCESS + 0x174);

    // 将欲对比的字符串保存到p_StrName2中,初始化ANSI字符串
    RtlInitAnsiString(&p_StrName2, EXE_Name);
    // 判断是否相等,相等则说明是calc.exe进程,我们直接返回假
    if (RtlCompareString(&p_StrName1, &p_StrName2, TRUE) == 0)
    {
        // 相等说明是Calc进程调用了该函数
        // 直接返回假,否则执行原函数
    }
}
相关文章
|
Ubuntu Linux 编译器
字符驱动设备原理及其相关函数(一)
字符驱动设备原理及其相关函数(一)
123 0
|
5月前
|
Linux
【Linux驱动学习(1)】USB与input子系统,linux统一设备模型,枚举,USB描述符深入剖析
【Linux驱动学习(1)】USB与input子系统,linux统一设备模型,枚举,USB描述符深入剖析
|
6月前
|
Linux
Linux系统中驱动之设备树添加按键驱动方法
Linux系统中驱动之设备树添加按键驱动方法
232 0
|
6月前
|
存储 安全 Linux
Linux 内核启动流程与入口函数分析
Linux 内核启动流程与入口函数分析
216 0
|
11月前
|
Windows
5.4 Windows驱动开发:内核通过PEB取进程参数
PEB结构`(Process Envirorment Block Structure)`其中文名是进程环境块信息,进程环境块内部包含了进程运行的详细参数信息,每一个进程在运行后都会存在一个特有的PEB结构,通过附加进程并遍历这段结构即可得到非常多的有用信息。在应用层下,如果想要得到PEB的基地址只需要取`fs:[0x30]`即可,TEB线程环境块则是`fs:[0x18]`,如果在内核层想要得到应用层进程的PEB信息我们需要调用特定的内核函数来获取。
116 0
5.4 Windows驱动开发:内核通过PEB取进程参数
|
11月前
|
存储 安全 API
5.2 Windows驱动开发:内核取KERNEL模块基址
模块是程序加载时被动态装载的,模块在装载后其存在于内存中同样存在一个内存基址,当我们需要操作这个模块时,通常第一步就是要得到该模块的内存基址,模块分为用户模块和内核模块,这里的用户模块指的是应用层进程运行后加载的模块,内核模块指的是内核中特定模块地址,本篇文章将实现一个获取驱动`ntoskrnl.exe`的基地址以及长度,此功能是驱动开发中尤其是安全软件开发中必不可少的一个功能。
104 0
5.2 Windows驱动开发:内核取KERNEL模块基址
|
Linux C语言
Linux驱动 | debugfs接口创建
Linux驱动 | debugfs接口创建
驱动开发:摘除InlineHook内核钩子
在笔者上一篇文章`《驱动开发:内核层InlineHook挂钩函数》`中介绍了通过替换`函数`头部代码的方式实现`Hook`挂钩,对于ARK工具来说实现扫描与摘除`InlineHook`钩子也是最基本的功能,此类功能的实现一般可在应用层进行,而驱动层只需要保留一个`读写字节`的函数即可,将复杂的流程放在应用层实现是一个非常明智的选择,与`《驱动开发:内核实现进程反汇编》`中所使用的读写驱动基本一致,本篇文章中的驱动只保留两个功能,控制信号`IOCTL_GET_CUR_CODE`用于读取函数的前16个字节的内存,信号`IOCTL_SET_ORI_CODE`则用于设置前16个字节的内存。
250 0
|
Windows
驱动开发:内核扫描SSDT挂钩状态
在笔者上一篇文章`《驱动开发:内核实现SSDT挂钩与摘钩》`中介绍了如何对`SSDT`函数进行`Hook`挂钩与摘钩的,本章将继续实现一个新功能,如何`检测SSDT`函数是否挂钩,要实现检测`挂钩状态`有两种方式,第一种方式则是类似于`《驱动开发:摘除InlineHook内核钩子》`文章中所演示的通过读取函数的前16个字节与`原始字节`做对比来判断挂钩状态,另一种方式则是通过对比函数的`当前地址`与`起源地址`进行判断,为了提高检测准确性本章将采用两种方式混合检测。
205 0
驱动开发:内核实现SSDT挂钩与摘钩
在前面的文章`《驱动开发:内核解析PE结构导出表》`中我们封装了两个函数`KernelMapFile()`函数可用来读取内核文件,`GetAddressFromFunction()`函数可用来在导出表中寻找指定函数的导出地址,本章将以此为基础实现对特定`SSDT`函数的`Hook`挂钩操作,与`《驱动开发:内核层InlineHook挂钩函数》`所使用的挂钩技术基本一致,不同点是前者使用了`CR3`的方式改写内存,而今天所讲的是通过`MDL映射`实现,此外前者挂钩中所取到的地址是通过`GetProcessAddress()`取到的动态地址,而今天所使用的方式是通过读取导出表寻找。
11276 0