通过KiSystemServiceUser获取SSDT基址

通过KiSystemServiceUser获取SSDT基址

技术原理

KeServiceDescriptorTable

在64位Windows中,SSDT被隐藏起来了,但是通过windbg调试发现,nt中导出了KeServiceDescriptorTable,该数组的第一个元素的第一个字段就是SSDT的基址。

下图是KeServiceDescriptorTable数组中元素的类型定义:

下图是KeServiceDescriptorTable数组的定义:

c 复制代码
DECLSPEC_CACHEALIGN KSERVICE_TABLE_DESCRIPTOR KeServiceDescriptorTable[NUMBER_SERVICE_TABLES];
DECLSPEC_CACHEALIGN KSERVICE_TABLE_DESCRIPTOR KeServiceDescriptorTableShadow[NUMBER_SERVICE_TABLES];

所以我们如果可以找到KeServiceDescriptorTable,就可以找到SSDT。

KiSystemServiceUser

调试发现,KiSystemServiceUser中有这样一段代码:

txt 复制代码
fffff804`7e8bc3d4 4c8d15e554b400  lea     r10,[nt!KeServiceDescriptorTable (fffff804`7f4018c0)]
fffff804`7e8bc3db 4c8d1ddeae9000  lea     r11,[nt!KeServiceDescriptorTableShadow (fffff804`7f1c72c0)]

那么我们就可以扫描KiSystemServiceUser的字节码,定位到上面的指令,就可以得到KeServiceDescriptorTable的地址了。

问题是,KiSystemServiceUser并不是一个文档化的函数,在ntoskrnl中也没有被导出,如何得到KiSystemServiceUser的地址?

我们需要从一个可以确定读出的地址,加上偏移得到KiSystemServiceUser的地址。

MSR(0xC0000082)

这个MSR就是syscall之后执行的首条指令的地址,可以通过rdmsr指令读出:

txt 复制代码
0: kd> u fffff8047edbc200
nt!KiSystemCall64Shadow:
fffff804`7edbc200 0f01f8          swapgs
fffff804`7edbc203 654889242510b00000 mov   qword ptr gs:[0B010h],rsp
fffff804`7edbc20c 65488b242500b00000 mov   rsp,qword ptr gs:[0B000h]
fffff804`7edbc215 650fba242518b0000001 bt  dword ptr gs:[0B018h],1
fffff804`7edbc21f 7203            jb      nt!KiSystemCall64Shadow+0x24 (fffff804`7edbc224)
fffff804`7edbc221 0f22dc          mov     cr3,rsp
fffff804`7edbc224 65488b242508b00000 mov   rsp,qword ptr gs:[0B008h]
fffff804`7edbc22d 6a2b            push    2Bh

就是KiSystemCall64Shadow的首地址。然后我们查看KiSystemServiceUser的地址:

复制代码
0: kd> u KiSystemServiceUser
nt!KiSystemServiceUser:
fffff804`7e8bc2e2 c645ab02        mov     byte ptr [rbp-55h],2
fffff804`7e8bc2e6 c645a801        mov     byte ptr [rbp-58h],1
fffff804`7e8bc2ea 65488b1c2588010000 mov   rbx,qword ptr gs:[188h]
fffff804`7e8bc2f3 c6833202000001  mov     byte ptr [rbx+232h],1
fffff804`7e8bc2fa 0f0d8b90000000  prefetchw [rbx+90h]
fffff804`7e8bc301 0fae5dac        stmxcsr dword ptr [rbp-54h]
fffff804`7e8bc305 650fae142580010000 ldmxcsr dword ptr gs:[180h]
fffff804`7e8bc30e 4c8945c8        mov     qword ptr [rbp-38h],r8

用两者的首地址相减,就得到了KiSystemServiceUser相对于KiSystemCall64Shadow的偏移:

txt 复制代码
0: kd> ? fffff804`7edbc200 - fffff804`7e8bc2e2
Evaluate expression: 5242654 = 00000000`004fff1e

在利用前面扫描到的加载KeSystemServiceUser的指令,就可以得到SDT,进而得到SSDT。


为什么要这样麻烦?其实还是因为ASLR,每次系统启动后,KiSystemCall64Shadow、KiSystemServiceUser等的地址都会发生变化,所以得这样层层计算得到SSDT地址。

下面看看代码实现。

示例代码

c 复制代码
/*
fffff804`7e8bc3d4 4c8d15e554b400  lea     r10,[nt!KeServiceDescriptorTable (fffff804`7f4018c0)]
fffff804`7e8bc3db 4c8d1ddeae9000  lea     r11,[nt!KeServiceDescriptorTableShadow (fffff804`7f1c72c0)]
*/

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

#pragma intrinsic(__readmsr)

const ULONGLONG offset = 0x4fff1e;

static ULONGLONG sg_ullSSDTAddr = 0;

// 获取nt!KeServiceDescriptorTable
ULONGLONG GetSDTAddr()
{
	ULONGLONG addr = 0;

	ULONGLONG startAddr = __readmsr(0xC0000082) - offset; // 搜索起始地址,就是KiSystemServiceUser开始的地址
	ULONGLONG endAddr = startAddr + 0x1000; // 搜索结束地址

	UCHAR c1, c2, c3;
	ULONGLONG ullSDTOffset = 0;
	for (PUCHAR cursor = (PUCHAR)startAddr; cursor < (PUCHAR)endAddr; cursor++)
	{
		if (MmIsAddressValid(cursor) && MmIsAddressValid(cursor + 1) && MmIsAddressValid(cursor + 2))
		{
			c1 = *cursor;
			c2 = *(cursor + 1);
			c3 = *(cursor + 2);

			if (c1 == 0x4c && c2 == 0x8d && c3 == 0x15)
			{
				memcpy_s(&ullSDTOffset, sizeof(ULONGLONG), cursor + 3, 4);
				addr = ullSDTOffset + (ULONGLONG)cursor + 7; // 相对寻址指令的偏移量是相对于下一条指令的地址计算的
			}
		}
	}

	return addr;
}

ULONGLONG GetSSDTFuncAddr(DWORD dwIndex)
{
	DWORD dwOffset = ((DWORD*)sg_ullSSDTAddr)[dwIndex * 4];

	ULONGLONG ullFuncAddr = sg_ullSSDTAddr + (dwOffset >> 4);
	return ullFuncAddr;
}

VOID DriverUnload(PDRIVER_OBJECT pDriverObj)
{
	UNREFERENCED_PARAMETER(pDriverObj);
}

EXTERN_C NTSTATUS DriverEntry(PDRIVER_OBJECT pDriverObj, PUNICODE_STRING pRegPath)
{
	UNREFERENCED_PARAMETER(pRegPath);

	pDriverObj->DriverUnload = DriverUnload;

	ULONGLONG sdtBase = GetSDTAddr();
	DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "SDT Address: %p", (PVOID)sdtBase);

	sg_ullSSDTAddr = *((ULONGLONG*)sdtBase);

	DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "SSDT Address: %p", (PVOID)sg_ullSSDTAddr);

	ULONGLONG ullFuncAddr = GetSSDTFuncAddr(0);
	DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "nt!NtAccessCheck Address: %p", (PVOID)ullFuncAddr);

	return STATUS_SUCCESS;
}

计算SSDT中函数的实际地址

在 x64 Windows 系统中,从 SDT(KeServiceDescriptorTable)获取的 SSDT 地址(ServiceTableBase)指向的不是直接的函数指针数组,而是一个经过编码的 32 位偏移数组。因此需要"解密"才能得到真实的函数地址。这是 x64 与 x86 在 SSDT 实现上的关键区别。

根本原因:x64 地址是 64 位(8 字节),直接存储会占用双倍内存,且影响缓存效率。Windows 内核采用了一种压缩存储方案:

  • 将 64 位函数地址转换为 32 位有符号偏移(相对于 SSDT 基地址)。

  • 存储时进行 4 位右移编码(即 偏移 = (实际地址 - SSDT基地址) << 4)。

  • 读取时反向操作:实际地址 = SSDT基地址 + (偏移 >> 4)。

所以计算函数真实地址的公式是:

在x64平台上:FuncAddress = KeServiceDescriptorTable+4\*Index>>4 + KeServiceDescriptorTable

更小的 SSDT 表能更好利用 CPU 缓存,加速系统调用查找。偏移相对于 SSDT 基地址计算,与 KASLR(内核地址空间布局随机化)​ 兼容,即使内核基址随机化,相对偏移保持不变。

相关推荐
巷尚UP3D-三维扫描检测逆向建模3 天前
工业部件逆向工程与检测,Artec Leo三维扫描兼顾效率与精度【巷尚UP3D】
逆向工程·工业检测·高精度三维扫描
阿昭L3 天前
Windows键盘过滤
windows·驱动开发·windows内核·过滤驱动
阿昭L5 天前
通过cr3读写进程内存
保护模式·windows内核
阿昭L6 天前
Lab 3-1
windows·安全·逆向工程·恶意代码分析
阿昭L8 天前
Lab 1-2
windows·恶意代码·逆向工程
阿昭L9 天前
调试CreateProcess
windows·进程·逆向工程·windows内核
3DVisionary10 天前
消费电子曲面如何逆向?蓝光3D扫描实现精密件快速迭代
3d·制造·智能制造·逆向工程·蓝光三维扫描·形位公差分析·消费电子制造
阿昭L11 天前
虚表hook
hook·逆向工程·虚函数
阿昭L11 天前
Windows用户态下常见的DLL注入技术总结
windows·逆向工程·dll注入