Windows 驱动开发:SSDT 地址获取与 KiSystemCall64 特征码搜索实战
- 作者: 刘杰
- 来源: 技术那些事
- 阅读:226
- 发布: 2025-07-05 07:17
- 最后更新: 2025-07-13 11:23
获取服务函数地址和 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函数(如CreateFile、ReadFile等)发起系统调用,而这些API函数最终会调用KiSystemCall64或类似的内核入口点。
KiSystemCall64是Windows内核中的一个关键函数,它用于处理从用户模式到内核模式的系统调用。在64位版本的Windows操作系统中,当一个用户模式的应用程序需要请求内核服务(如文件I/O、进程管理、内存管理等)时,它会触发一个系统调用,这通常通过调用__syscall或syscall指令来实现。
当系统调用发生时,控制权会转移到KiSystemCall64函数。这个函数负责解析系统调用号和参数,然后调用相应的内核服务例程。系统调用号是一个整数,它标识了应用程序想要执行的特定系统服务。参数则包含了系统调用所需的全部信息,如文件句柄、内存地址等。
KiSystemCall64的主要职责包括:
- 解析系统调用号:确定应用程序请求的系统服务类型。
- 验证参数:检查传入的参数是否有效,防止非法访问或越界。
- 调用内核服务例程:根据系统调用号,调用适当的内核函数来执行请求的服务。
- 处理返回值:将内核服务的结果返回给用户模式的应用程序。