找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

4020

积分

0

好友

533

主题
发表于 昨天 02:25 | 查看: 6| 回复: 0

逆向工程领域,我们之前探讨过用户模式下最简单的Hook原理及MinHook框架。然而,当战场转移到系统内核(Ring 0)时,核心原理虽然不变,但在实现细节上必须进行重大调整。

本文仅从安全研究与防御识别的角度进行技术探讨。

TL;DR:本文将详细解析内核模式下两种基础但经典的Hook技术:Inline Hook与SSDT Hook。

一、基础 Inline Hook 原理与实现

其核心步骤与用户态一致:

  1. 定位函数地址:找到目标函数在内核中的内存地址。
  2. 备份原指令:保存函数起始处的若干条完整汇编指令(n条)。
  3. 植入跳转:用一段跳转到我们自定义hook_func的“跳板代码”覆盖原函数起始处的指令。
  4. 构建蹦床:在保存原指令的“蹦床”区域末尾,添加跳回原函数后续地址的指令,确保原始逻辑能被正确调用。

那么,在内核中我们尝试沿用此思路(注意:本次实验在关闭内核隔离并开启调试器的测试模式下进行,暂时规避了PatchGuard。在现代生产环境的Windows系统上直接操作是不可行的)。

内核中的函数寻址

在内核编程中,可以使用 MmGetSystemRoutineAddress 来获取已导出内核函数的地址。

MmGetSystemRoutineAddress函数文档截图

基础驱动代码如下:

#include<ntifs.h>
#include<windef.h>

VOID DriverUnload(PDRIVER_OBJECT DriverObject)
{
    DbgPrint("Driver Stopping -> %wZ\n", &DriverObject->DriverName);
}

NTSTATUS DriverEntry(PDRIVER_OBJECT  DriverObject, PUNICODE_STRING RegistryPath)
{
    UNREFERENCED_PARAMETER(RegistryPath);

    DbgPrint("Driver Running -> %wZ\n", &DriverObject->DriverName);
    DriverObject->DriverUnload = DriverUnload;

    NTSTATUS status = STATUS_SUCCESS;

    UNICODE_STRING funcName = { 0 };
    RtlInitUnicodeString(&funcName, L"NtOpenFile");
    PVOID funcPtr = MmGetSystemRoutineAddress(&funcName);

    DbgPrint("[test driver] NtOpenFile at: 0x%p\n", funcPtr);

    return status;
}

运行后,可以获取到 NtOpenFile 函数的地址。

驱动运行输出显示NtOpenFile地址

内核调试器中查看NtOpenFile地址

关键点:加载两份驱动得到的地址是相同的。这意味着,按照此思路实施的Hook能够拦截所有内核模块(其他驱动)对 NtOpenFile 的调用。

实施 Inline Hook

逆向分析 NtOpenFile 的起始代码,确定需要备份和覆盖17个字节(因为用于跳转的mov rax, addr; jmp rax指令共12字节,覆盖长度必须大于此值)。

NtOpenFile函数反汇编代码

详细实现代码请见文末附录

其中,与用户态Hook一个显著的不同是,需要操作 CR0 寄存器来临时关闭写保护(WP)。

VOID DisableWP()
{
    __writecr0(__readcr0() & (~0x10000));
    _disable();
}

VOID EnableWP()
{
    __writecr0(__readcr0() | 0x10000);
    _enable();
}

修改 CR0 寄存器 WP 位的目的是临时关闭CPU对只读内存页的写保护机制。只有这样,内核代码页(如 NtOpenFile 所在的 .text 段)才能被直接修改以植入Hook。

CR0寄存器位域结构图

Intel手册中关于页保护机制的说明

“一旦进入保护模式,便不再存在用于开启或关闭该保护机制的控制位...然而,通过执行以下操作,仍可禁用页级保护:

  • 清除控制寄存器 CR0 中的 WP 标志。
  • 为每一个页目录项和页表项设置读/写(R/W)标志及用户/管理(U/S)标志。此操作将使每一个页面均变为可写的用户页面,从而在实质上禁用了页级保护功能。”

详细内容可参阅《英特尔® 64 位和 IA-32 架构开发人员手册合订本》第三卷第6章。

Hook效果演示

成功安装Inline Hook后,对 NtOpenFile 的调用会被重定向。

安装Hook后的函数地址

调试器显示Hook已安装,跳转至自定义函数

执行流程进入我们的Hook函数

调试器单步执行进入HookNtOpenFile函数

Hook函数打印的系统文件访问记录

DebugView显示Hook拦截到的文件打开操作

二、SSDT Hook 技术详解

SSDT(System Service Descriptor Table,系统服务描述符表)存储了操作系统底层服务的入口点,其中包含许多未导出的函数(无法通过 MmGetSystemRoutineAddress 找到)。

定位 SSDT

这里需要用到 MSR(Model-Specific Register,型号特定寄存器)。MSR是CPU提供的一组特殊寄存器,用于控制或查询处理器的特定功能。其在Intel手册中的定义位于第一卷(3-4 Vol. 1):

Intel手册中关于MSR的说明

“特定型号寄存器(MSRs)——处理器提供多种特定型号寄存器,用于控制和报告处理器的性能。几乎所有的 MSR 均负责处理系统相关功能,且应用程序无法直接访问。此规则的一个例外是时间戳计数器...”

通过读取特定的MSR,我们可以获取 SYSCALL 指令进入内核后的入口点地址。

unsigned __int64 syscall_entry = __readmsr(0xC0000082);

示例驱动代码:

#include<ntifs.h>
VOID DriverUnload(PDRIVER_OBJECT DriverObject)
{
    DbgPrint("Driver Unloaded\n");
}
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
    UNREFERENCED_PARAMETER(RegistryPath);

    DbgPrint("Driver Loaded\n");
    DriverObject->DriverUnload = DriverUnload;

    DWORD64 dmsr = __readmsr(0xC0000082);
    DbgPrint("KiSystemCall64 at %p\n", dmsr);

    return STATUS_SUCCESS;
}

运行结果:

调试输出显示KiSystemCall64地址

解析 SSDT 结构

在开启内核隔离的情况下,获取到的可能是 KiSystemCall64Shadow,未开启则是 KiSystemCall64。本文实验环境未开启内核隔离。

通过反汇编 KiSystemCall64 并查找其内部的 KiSystemServiceRepeat 例程,可以定位到 KeServiceDescriptorTable(或KeServiceDescriptorTableShadow)的引用。在机器码中,这通常表现为 4C 8D 15lea r10, [addr])这样的指令模式。

KiSystemServiceRepeat反汇编代码,包含SSDT引用

由此可找到SSDT在内存中的地址。其结构定义大致如下:

typedef struct _SYSTEM_SERVICE_TABLE
{
    PVOID tableBase;
    PVOID serviceCountBase;
    ULONG64 numberOfServices;
    PVOID unkown;
}SYSTEM_SERVICE_TABLE, *PSYSTEM_SERVICE_TABLE;

通过手动计算和解析,我们可以找到SSDT的基地址和函数地址表(tableBase)。

内存查看器显示KeServiceDescriptorTable地址及内容

  • SSDT 地址: 0xfffff805170018c0
  • tableBase 地址: 0xfffff805162c79f0

tableBase 是一个指向ULONG数组的指针,每个ULONG值编码了对应系统调用服务函数的偏移量。

SSDT的tableBase内存数据

服务函数地址计算公式
函数地址 = tableBase + (tableBase[index] >> 4)

例如,计算第0号函数的地址:
offset = SSDT->tableBase[0] >> 4 = 0x27fe004 >> 4 = 0x27fe000
funcAddr = SSDT->tableBase + offset = 0xfffff805162c79f0 + 0x27fe000 = 0xfffff805165477f0

验证计算结果:

调试器确认NtAccessCheck函数地址

编写程序自动化遍历SSDT:

#include<ntifs.h>

typedef struct _SYSTEM_SERVICE_TABLE
{
    PVOID tableBase;
    PVOID serviceCountBase;
    ULONG64 numberOfServices;
    PVOID unkown;
}SYSTEM_SERVICE_TABLE, *PSYSTEM_SERVICE_TABLE;

LONG64 GetFuncAddr(size_t index, PSYSTEM_SERVICE_TABLE ssdt){
    if (index > ssdt->numberOfServices)
        return 0;
    LONG offset = ((PLONG)(ssdt->tableBase))[index] >> 4;
    return (DWORDLONG)(ssdt->tableBase) + offset;
}

VOID DriverUnload(PDRIVER_OBJECT DriverObject)
{
    DbgPrint("Driver Unloaded\n");
}

NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
    UNREFERENCED_PARAMETER(RegistryPath);

    DbgPrint("Driver Loaded\n");
    DriverObject->DriverUnload = DriverUnload;

    DWORD64 dmsr = __readmsr(0xC0000082);
    DbgPrint("KiSystemCall64 at %p\n", dmsr);
    PUCHAR tempptr = (PUCHAR)(dmsr);
    LONG offset = 0;
    PSYSTEM_SERVICE_TABLE table = NULL;
    for (size_t i = 0; i < 0x1000; i++)
    {
        if (*(tempptr + i) == 0x4c && *(tempptr + i + 1) == 0x8d && *(tempptr + i + 2) == 0x15) {
            offset = *((PLONG)(tempptr + i + 3));
            table = (PSYSTEM_SERVICE_TABLE)(tempptr + i + 7 + offset);
            break;
        }
    }
    if (offset == 0) {
        DbgPrint("Not found KeServiceDescriptorTable\n");
        return STATUS_NOT_FOUND;
    }
    DbgPrint("KeServiceDescriptorTable at: %p\n tableBase: %p\n", table, table->tableBase);

    for (size_t i = 0; i < table->numberOfServices-1; i++)
    {
        DbgPrint("No.%d func at: %p\n",i, GetFuncAddr(i, table));
    }

    return STATUS_SUCCESS;
}

运行结果成功列出了SSDT中的函数地址:

程序输出SSDT地址及前13个函数地址

调试器中验证部分函数地址

如何确定序号对应的函数? 可以通过解析ntdll.dllsyscall stub的汇编代码来获得(例如,mov eax, 0x33 中的0x33就是系统调用号)。

SSDT Hook 尝试与挑战

找到SSDT后,理论上可以通过修改tableBase数组中对应索引的偏移值来实现Hook。

一个未能成功的示例代码(核心逻辑):

// ... 结构体、函数指针等定义 ...

VOID HookSSDT(PSYSTEM_SERVICE_TABLE ssdt)
{
    PULONG table = ssdt->tableBase;
    ULONG entry = table[NtOpenFileIndex];
    ULONG64 base = (ULONG64)table;
    ULONG64 original = base + (entry >> 4);
    OriginalNtOpenFile = (NTOPENFILE)original;
    ULONG param = entry & 0xF;
    ULONG64 hookAddr = (ULONG64)HookNtOpenFile;
    ULONG newEntry = (ULONG)(((hookAddr - base) << 4) | param);
    DisableWP();
    table[NtOpenFileIndex] = newEntry;
    EnableWP();
}

驱动运行后,虽然显示Hook地址已修改:

驱动输出显示Hook地址已变更

但实际调用并未进入我们的函数:

调试显示实际调用未跳转到Hook函数

问题根源tableBase中存储的偏移值是4字节(32位)有符号整数,这限制了其寻址范围。当我们的Hook函数地址与tableBase的距离超过此范围时,计算出的新偏移就会溢出,导致跳转失败。这类似于在Ring3下Inline Hook时,跳转距离超出jmp指令范围的问题。

潜在的解决方案

  1. 在SSDT附近部署蹦床:在内核模块中(尽量靠近SSDT)分配一小块内存作为蹦床,SSDT Hook先跳转到这个蹦床,再由蹦床跳转到最终的Hook函数。这是Ring3下MinHook等框架采用的方法。
  2. 在ntoskrnl模块附近分配内存:尝试在系统内核模块(ntoskrnl.exe)的地址空间附近分配可执行内存作为蹦床。
  3. 利用未使用空间:在ntoskrnl.exewin32k.sys中寻找未使用的代码“空隙”,将蹦床代码写入这些区域(win32k涉及Shadow SSDT,本文不展开)。

当然,对于已导出的函数,直接使用前述的Inline Hook是更简单直接的选择。

附录:Windows 10 内联钩子完整测试代码

Windows 10 系统版本信息

#include <ntddk.h>

typedef NTSTATUS(*NTOPENFILE)(
    PHANDLE FileHandle,
    ACCESS_MASK DesiredAccess,
    POBJECT_ATTRIBUTES ObjectAttributes,
    PIO_STATUS_BLOCK IoStatusBlock,
    ULONG ShareAccess,
    ULONG OpenOptions
    );

NTOPENFILE OriginalNtOpenFile = NULL;

UCHAR OriginalBytes[17];
PVOID Trampoline = NULL;
PVOID TargetFunction = NULL;

VOID DisableWP()
{
    ULONG64 cr0 = __readcr0();
    cr0 &= 0xfffffffffffeffff;
    __writecr0(cr0);
    _disable();
}

VOID EnableWP()
{
    ULONG64 cr0 = __readcr0();
    cr0 |= 0x10000;
    __writecr0(cr0);
    _enable();
}

NTSTATUS HookNtOpenFile(
    PHANDLE FileHandle,
    ACCESS_MASK DesiredAccess,
    POBJECT_ATTRIBUTES ObjectAttributes,
    PIO_STATUS_BLOCK IoStatusBlock,
    ULONG ShareAccess,
    ULONG OpenOptions
)
{
    if (ObjectAttributes && ObjectAttributes->ObjectName)
    {
        DbgPrint("InlineHook NtOpenFile: %wZ\n",
            ObjectAttributes->ObjectName);
    }

    return OriginalNtOpenFile(
        FileHandle,
        DesiredAccess,
        ObjectAttributes,
        IoStatusBlock,
        ShareAccess,
        OpenOptions
    );
}

VOID BuildTrampoline()
{
    Trampoline = ExAllocatePool2(
        POOL_FLAG_NON_PAGED_EXECUTE,
        32,
        'HKTN');

    RtlCopyMemory(Trampoline, OriginalBytes, 17);

    UCHAR* p = (UCHAR*)Trampoline + 17;

    p[0] = 0x48;
    p[1] = 0xB8;
    *(PVOID*)(p + 2) = (PUCHAR)TargetFunction + 17;
    p[10] = 0xFF;
    p[11] = 0xE0;

    OriginalNtOpenFile = (NTOPENFILE)Trampoline;
}

VOID InstallInlineHook()
{
    UNICODE_STRING name;

    RtlInitUnicodeString(&name, L"NtOpenFile");

    TargetFunction = MmGetSystemRoutineAddress(&name);

    if (!TargetFunction)
        return;

    RtlCopyMemory(OriginalBytes, TargetFunction, 17);

    BuildTrampoline();

    DisableWP();

    UCHAR patch[17];

    patch[0] = 0x48;
    patch[1] = 0xB8;
    *(PVOID*)(patch + 2) = HookNtOpenFile;
    patch[10] = 0xFF;
    patch[11] = 0xE0;
    patch[12] = 0x90;
    patch[13] = 0x90;
    patch[14] = 0x90;
    patch[15] = 0x90;
    patch[16] = 0x90;

    RtlCopyMemory(TargetFunction, patch, 17);

    EnableWP();
}

VOID RemoveInlineHook()
{
    if (!TargetFunction)
        return;

    DisableWP();

    RtlCopyMemory(TargetFunction, OriginalBytes, 17);

    EnableWP();

    if (Trampoline)
        ExFreePool(Trampoline);
}

VOID DriverUnload(PDRIVER_OBJECT DriverObject)
{
    RemoveInlineHook();
    DbgPrint("Inline hook removed\n");
}

NTSTATUS DriverEntry(
    PDRIVER_OBJECT DriverObject,
    PUNICODE_STRING RegistryPath
)
{
    UNREFERENCED_PARAMETER(RegistryPath);

    DriverObject->DriverUnload = DriverUnload;

    InstallInlineHook();

    DbgPrint("Inline hook installed\n");

    return STATUS_SUCCESS;
}

本文介绍了两种基础的内核Hook技术,希望能为你在云栈社区进行安全技术探讨时提供一些思路。理解攻击原理,是构建有效防御的第一步。




上一篇:造假神器冲上热搜:微信支付宝伪造工具曝光,转账截图诈骗需警惕
下一篇:智谱AI GLM-OCR多场景OCR评测:轻量模型如何实现多项性能第一
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-3-17 00:20 , Processed in 0.622994 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表