之前的文章 【逆向】MinHook框架代码解读简述了在用户模式下最简单的hook原理和一种框架。但是在内核当中使用hook遵循一样的原理,但是需要在部分细节上进行修改。

TL,DR:讲了内核中使用inline hook和ssdt表这两种比较基础的方式

windows rookit防护-Kernel Hook 1

基础 inline Hook

这里再次简述:

  1. 找到函数的地址
  2. 保存前n条合法的汇编指令
  3. 覆盖前n跳指令为一段跳到我们hook_func的跳板代码
  4. 在保存旧有n条指令的区域加上返回到后续地址的跳板代码,确保原始函数能被调用

所以在内核当中我们尝试依旧这样做(这里我开了测试模式,关闭了内核隔离,而且调试器开着的,暂时不需要担心patchguard。在现代windows上肯定是不行的

那么在内核中如何找到要调用的函数呢?

在内核变成中可以使用MmGetSystemRoutineAddress获得内核函数的地址

image-20260314122314654

#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;
}

image-20260314123252147

image-20260314122908029

而且加载两份驱动得到的地址是一样的,所以按照R3的思路就可以hook所有R0中的操作了,也就是说其他驱动调用了NtOpenFile也能被hook到

逆向一下NtOpenFile的代码,需要复制17个字节(jmp rax的跳转是12字节,所以要复制的汇编必须>12)

image-20260315131542808

详细代码参见附录

image-20260315131353125

有点不一样的是代码使用了cr0寄存器

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

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

修改 CR0 的目的是 临时关闭 CPU 的写保护机制(Write Protect),这样内核代码页(如 NtOpenFile 所在的 .text 段)才能被修改。

image-20260314132203340

image-20260314131201472

在寄存器 CR0 中设置 PE 标志,将使处理器切换至保护模式;进而启用段保护机制。一旦进入保护模式,便不再存在用于开启或关闭该保护机制的控制位。不过,即使处于保护模式下,仍可通过为所有段选择符和段描述符分配特权级 0(最高特权级),从而实质上禁用段保护机制中基于特权级的那部分功能。此操作将消除段与段之间的特权级保护屏障,但诸如界限检查和类型检查等其他保护检查仍将照常执行。 当启用分页功能(即在寄存器 CR0 中设置 PG 标志)时,页级保护功能将自动开启。同样地,一旦启用了分页功能,便不再存在用于关闭页级保护的模式位。然而,通过执行以下操作,仍可禁用页级保护:

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

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

最后的效果:

安装hook

image-20260315123038465

运行到了自己的NtOpenFile函数

image-20260315124514384

打印出来的结果

image-20260315131006234

SSDT Hook

SSDT是系统服务描述符表,里面存储了操作系统的一些底层实现,包括没有导出的一些函数(MmGetSystemRoutineAddress找不到)

这里需要用到MSR寄存器,MSR(Model-Specific Register)是 CPU 提供的一组 特殊寄存器,用于控制或查询处理器的特定功能。依旧在IA32手册中找到定义,位于第一卷3.2 OVERVIEW OF THE BASIC EXECUTION ENVIRONMENT(3-4 Vol. 1):

image-20260314212114189

特定型号寄存器(MSRs)——处理器提供多种特定型号寄存器,用于控制和报告处理器的性能。几乎所有的 MSR 均负责处理系统相关功能,且应用程序无法直接访问。此规则的一个例外是时间戳计数器(Time-Stamp Counter)。有关 MSR 的详细描述,请参阅《Intel® 64 和 IA-32 架构软件开发人员手册》第 4 卷中的第 2 章“特定型号寄存器(MSRs)”。

unsigned __int64 syscall_entry = __readmsr(0xC0000082);

这样就能得到SYSCALL 指令进入内核后的 RIP

#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;
}

image-20260314212913344

解析SSDT

在开启系统隔离的情况下,得到的是KiSystemCall64Shadow,没有开启是KiSystemCall64。我这里没有开启内核隔离

然后继续向下查到KiSystemServiceRepeat,主要是为了拿到KeServiceDescriptorTable或者KeServiceDescriptorTableShadow的值,在这里就是

35 05 9F 00AE B6 8E 00

image-20260314213520421

通过这种偏移可以找到保存ssdt的内存地址,其结构体如下

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

image-20260314213658090

我们顺着之前的结果手动查找一下

image-20260314220722628

  • SSDT: 0xfffff805170018c0

  • tableBase: 0xfffff805162c79f0

    是一个四字节长度的数,如下

    image-20260314220855731

开始第0号函数的地址计算:

offset = SSDT->tableBase[0] >> 4 = 0x27fe004>>4 = 0x27fe000
funcAddr = SSDT->tableBase+offset = SSDT->tableBase[DWORD(offset/4)]
= 0xfffff805162c79f0+0x27fe000 = 0xfffff805165477f0

image-20260314221230034

可以编写程序

#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;
}

image-20260314222803519

image-20260314222839397

具体的index对应的函数,可以解析ntdll中的api实现的汇编中填入的值来确定(例如 mov eax, 0x33,然后进入到了KiSystemCall64

尝试hook

找到ssdt后,替换table中的偏移值,从而实现hook,

失败版:

#include <ntifs.h>

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

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

NTOPENFILE OriginalNtOpenFile = NULL;
ULONG NtOpenFileIndex = 51;
PSYSTEM_SERVICE_TABLE table = NULL;

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

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

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 DisableWP()
{
ULONG64 cr0 = __readcr0();
cr0 &= 0xfffffffffffeffff;
__writecr0(cr0);
_disable();
}

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

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();
}
VOID UnhookSSDT(PSYSTEM_SERVICE_TABLE ssdt)
{

PULONG table = ssdt->tableBase;
ULONG entry = table[NtOpenFileIndex];
ULONG param = entry & 0xF;
ULONG64 base = (ULONG64)table;
ULONG64 original = (ULONG64)OriginalNtOpenFile;
ULONG newEntry = (ULONG)(((original - base) << 4) | param);
DisableWP();
table[NtOpenFileIndex] = newEntry;
EnableWP();
}

VOID DriverUnload(PDRIVER_OBJECT DriverObject)
{
//UnhookSSDT(table);
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 MyHook at: %p\n", table, table->tableBase, HookNtOpenFile);

DbgPrint("NtOpenFile at: %p\n", GetFuncAddr(NtOpenFileIndex, table));
HookSSDT(table);
DbgPrint("[hooked] NtOpenFile at: %p\n", GetFuncAddr(NtOpenFileIndex, table));
UnhookSSDT(table);
DbgPrint("[unhooked] NtOpenFile at: %p\n", GetFuncAddr(NtOpenFileIndex, table));


return STATUS_SUCCESS;
}

image-20260315121811822

image-20260315121745509

然后进行hook,但是最后调用的并不是我们的函数

image-20260315121927839

因为table中存储大小是4字节的,所以寻址范围有限,这里就很像我们在ring3下使用inline hook编写Trampoline遇到的问题了,无法跳转到函数。有三种预期的解决方法

  • SSDT 附近的内核模块中放一个 trampoline,而且让他尽量靠近ssdt,这也是ring3下minhook框架中的方法
  • 在 ntoskrnl 附近分配内存,然后作为trampoline。
  • 在 ntoskrnl.exe 或 win32k.sys 找到未使用空间,覆盖他们为trampoline。win32k中的部分可能涉及到Shadow SSDT,该部分不会再本篇进行说明。

或者找到未导出函数的地址,然后inline hook即可

附录

在windows 10 上的inline hook测试代码

image-20260315131659502

#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;
}