Windows 驱动开发:SSDT 地址获取与 KiSystemCall64 特征码搜索实战

获取服务函数地址和 SSDT 地址

根据 __readmsr(头文件位 intrin.h)可以获取 KiSystemCall64的地址,通过此地址,查找内核函数KiSystemServiceRepeat 的特征码,这个函数的调用后边有 KeServiceDescriptorTable的地址。

c 复制
#include <ntddk.h>
#include <intrin.h>

#pragma pack(1)
typedef struct _KeServiceDescriptorTable{
	PULONG ServiceTableBase;        // SSDT基址,8字节大小
	PVOID ServiceCounterTableBase;  // SSDT中服务被调用次数计数器,8字节大小
	ULONGLONG NumberOfService;      // SSDT服务函数的个数,8字节大小
	PVOID ParamTableBase;           // 系统服务参数表基址,8字节大小。实际指向的数组是以字节为单位的记录着对应服务函数的参数个数
} KeServiceDescriptorTable, * PKeServiceDescriptorTable;
#pragma pack()

// 对比源地址内容是否跟目的地址内容一致.
BOOLEAN CompareStr(PCUCHAR, PCUCHAR);
// 搜索特征码(内存字节串),返回搜索到的内存地址值.
ULONGLONG SearchFeature(const VOID* startAddr, const SIZE_T size, PCUCHAR str);
// 获取 SSDT 结构指针.
PKeServiceDescriptorTable getSSDT64();
// 获取指定索引号的服务方法
PVOID64 getServiceMethodAddr(PKeServiceDescriptorTable, SIZE_T index);

VOID DriverUnload(PDRIVER_OBJECT pDriverObject)
{
	KdPrint(("Unload success..."));
}

NTSTATUS DriverEntry(PDRIVER_OBJECT pDriverObject, PUNICODE_STRING pRegPath) 
{

	DbgPrint("This is my first driver.");
	pDriverObject->DriverUnload = DriverUnload;
	PKeServiceDescriptorTable pSsdt = getSSDT64();
	// 打印 ssdt 地址, 服务接口数量.
	KdPrint(("This is service base addr:%p, service num:%lld\n", pSsdt->ServiceTableBase, pSsdt->NumberOfService));
	// 打印第 5 个接口的地址.
	SIZE_T index = 5;
	PVOID64 methodAddr = getServiceMethodAddr(pSsdt, index);
	KdPrint(("Method index:%llu, addr:%p\n", index, methodAddr));
	return STATUS_SUCCESS;
}

PVOID64 getServiceMethodAddr(PKeServiceDescriptorTable pKdt, SIZE_T index)
{
	// 判断索引号是否超过最大的索引值.
	if (index > pKdt->NumberOfService) {
		return NULL64;
	}
	// 获取偏移量
	ULONG offset = *(pKdt->ServiceTableBase + index) >> 4;
	KdPrint(("method offset:%X\n", offset));
	return (PVOID64)((ULONGLONG)pKdt->ServiceTableBase + offset);
}

PKeServiceDescriptorTable getSSDT64()
{
	// 获取模型寄存器中的地址,为搜索的开始地址(KiSystemCall64).
	PUCHAR pKiSystemCall64 = (PUCHAR)__readmsr(0xC0000082);
	// 设置特征码(lea r10,[nt!KeServiceDescriptorTable]).
	UCHAR feature[] = { 0x4c, 0x8d, 0x15, 0 };
	// 查找特征码(KiSystemServiceRepeat).
	ULONGLONG addressVal = SearchFeature(pKiSystemCall64, 0x500, feature);
	// 指令特征码中的偏移地址 + 下一指令的起始地址 = KeServiceDescriptorTable.
	ULONG offset = *(ULONG*)(addressVal + strlen(feature));

	// 获取 SSDT 的地址.
	return (PKeServiceDescriptorTable)(offset + addressVal + 7);
}

// 比较两个字符串是否相等.
BOOLEAN CompareStr(PCUCHAR src, PCUCHAR dest)
{
	size_t len = strlen(dest);
	for (size_t i = 0; i < len; ++i) {
		if (dest[i] != src[i]) {
			return FALSE;
		}
	}
	return TRUE;
}

// 搜索指定地址开始,size 范围内有没有特征字符串 str.
ULONGLONG SearchFeature(const VOID* startAddr, const SIZE_T size, PCUCHAR str)
{
	// 设置搜索的开始地址.
	PCUCHAR start = startAddr;
	for (SIZE_T i = 0; i < size; ++i) {
		// 比较当前地址的字符串是否跟目的字符串16进制是否相同.
		if (CompareStr(start + i, str)) {
			return (ULONGLONG)(start + i);
		}
	}
	return 0;
}

注意点:

  • KeServiceDescriptorTable 并没有公开,所以要自己定义一个,并且注意数据对齐,#pragma pack(1),内存中这个结构是紧密相连的没有对齐的内存。
  • 注意获取多级指针时候的指针运算,数值运算和指针运算是两种不同的运算方式。使用错了就无法找到正确的地址。
  • 注意各种类型的数值的打印方式,打印指针(%p),打印 ULONG(%lu),打印16进制(%x,%X),打印十进制的 ULONGLONG(%llu),等等类型。详细可以查看 C 语言 print 不同类型的值。

KiSystemCall64是内核的一部分,对普通用户和应用程序来说是不可见的。应用程序通过标准的API函数(如CreateFileReadFile等)发起系统调用,而这些API函数最终会调用KiSystemCall64或类似的内核入口点。

KiSystemCall64是Windows内核中的一个关键函数,它用于处理从用户模式到内核模式的系统调用。在64位版本的Windows操作系统中,当一个用户模式的应用程序需要请求内核服务(如文件I/O、进程管理、内存管理等)时,它会触发一个系统调用,这通常通过调用__syscallsyscall指令来实现。

当系统调用发生时,控制权会转移到KiSystemCall64函数。这个函数负责解析系统调用号和参数,然后调用相应的内核服务例程。系统调用号是一个整数,它标识了应用程序想要执行的特定系统服务。参数则包含了系统调用所需的全部信息,如文件句柄、内存地址等。

KiSystemCall64的主要职责包括:

  1. 解析系统调用号:确定应用程序请求的系统服务类型。
  2. 验证参数:检查传入的参数是否有效,防止非法访问或越界。
  3. 调用内核服务例程:根据系统调用号,调用适当的内核函数来执行请求的服务。
  4. 处理返回值:将内核服务的结果返回给用户模式的应用程序。