如何在C程序中获取SSDT地址以检测函数挂钩?
嘿,我来帮你搞定这个问题!你想用C程序获取SSDT地址来检测函数挂钩,用NtQuerySystemInformation搭配SystemModuleInformation确实是可行的路子,网上资料零散很正常,我给你一步步拆解清楚,附上代码示例:
获取SSDT地址的核心思路
KeServiceDescriptorTable是内核里存储SSDT关键信息的全局结构,但用户态程序没法直接访问它。我们得先通过NtQuerySystemInformation拿到系统内核模块(比如ntoskrnl.exe)的基址,再通过解析模块的导出表或者已知偏移定位到KeServiceDescriptorTable,最后解析这个结构拿到SSDT的地址列表。
第一步:定义所需的未公开结构体
SystemModuleInformation对应的结构体是未公开的,我们需要手动定义来解析NtQuerySystemInformation返回的数据:
#include <windows.h> #include <stdio.h> #include <string.h> // SYSTEM_MODULE结构:存储单个系统模块的信息 typedef struct _SYSTEM_MODULE { HANDLE Section; PVOID MappedBase; PVOID ImageBase; // 模块在内存中的基地址 ULONG ImageSize; ULONG Flags; USHORT LoadOrderIndex; USHORT InitOrderIndex; USHORT LoadCount; USHORT OffsetToFileName; UCHAR FullPathName[256]; // 模块的完整路径 } SYSTEM_MODULE, *PSYSTEM_MODULE; // SYSTEM_MODULE_INFORMATION结构:系统模块列表的容器 typedef struct _SYSTEM_MODULE_INFORMATION { ULONG Count; // 模块数量 SYSTEM_MODULE Modules[1]; // 模块数组(实际长度由Count决定) } SYSTEM_MODULE_INFORMATION, *PSYSTEM_MODULE_INFORMATION; // NtQuerySystemInformation的函数指针定义(未公开API) typedef NTSTATUS(WINAPI* PNtQuerySystemInformation)( ULONG SystemInformationClass, PVOID SystemInformation, ULONG SystemInformationLength, PULONG ReturnLength );
第二步:调用NtQuerySystemInformation获取模块列表
NtQuerySystemInformation在ntdll.dll中,我们需要动态获取它的地址,然后分两次调用:第一次获取所需缓冲区大小,第二次拿到完整的模块列表:
int main() { // 获取NtQuerySystemInformation的函数地址 PNtQuerySystemInformation NtQuerySystemInfo = (PNtQuerySystemInformation)GetProcAddress( GetModuleHandle(L"ntdll.dll"), "NtQuerySystemInformation" ); if (!NtQuerySystemInfo) { printf("Failed to get NtQuerySystemInformation address!\n"); return -1; } ULONG returnLen = 0; // 第一次调用:获取需要的缓冲区大小 NTSTATUS status = NtQuerySystemInfo(11, NULL, 0, &returnLen); if (status != STATUS_INFO_LENGTH_MISMATCH) { printf("First NtQuerySystemInformation call failed!\n"); return -1; } // 分配缓冲区存储模块信息 PSYSTEM_MODULE_INFORMATION moduleInfo = (PSYSTEM_MODULE_INFORMATION)malloc(returnLen); if (!moduleInfo) { printf("Memory allocation failed!\n"); return -1; } // 第二次调用:获取实际的模块列表 status = NtQuerySystemInfo(11, moduleInfo, returnLen, &returnLen); if (!NT_SUCCESS(status)) { printf("Second NtQuerySystemInformation call failed!\n"); free(moduleInfo); return -1; }
第三步:定位ntoskrnl.exe模块
遍历模块列表,找到内核主模块ntoskrnl.exe:
// 遍历模块列表,找到ntoskrnl.exe PSYSTEM_MODULE ntoskrnlMod = NULL; for (ULONG i = 0; i < moduleInfo->Count; i++) { // 提取模块文件名(从FullPathName的偏移位置开始) PCHAR modName = (PCHAR)((ULONG_PTR)moduleInfo->Modules[i].FullPathName + moduleInfo->Modules[i].OffsetToFileName); if (_stricmp(modName, "ntoskrnl.exe") == 0) { ntoskrnlMod = &moduleInfo->Modules[i]; printf("Found ntoskrnl.exe at: 0x%p\n", ntoskrnlMod->ImageBase); break; } } if (!ntoskrnlMod) { printf("Failed to find ntoskrnl.exe!\n"); free(moduleInfo); return -1; }
第四步:解析导出表获取KeServiceDescriptorTable地址
我们可以通过解析ntoskrnl.exe的PE导出表,找到KeServiceDescriptorTable的地址:
// 解析PE导出表,获取指定符号的地址 PVOID GetKernelSymbol(PVOID modBase, LPCSTR symbolName) { PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)modBase; PIMAGE_NT_HEADERS ntHeaders = (PIMAGE_NT_HEADERS)((ULONG_PTR)modBase + dosHeader->e_lfanew); // 验证PE文件签名 if (dosHeader->e_magic != IMAGE_DOS_SIGNATURE || ntHeaders->Signature != IMAGE_NT_SIGNATURE) { return NULL; } // 获取导出表地址 PIMAGE_EXPORT_DIRECTORY exportDir = (PIMAGE_EXPORT_DIRECTORY)( (ULONG_PTR)modBase + ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress ); if (!exportDir) return NULL; PDWORD nameArray = (PDWORD)((ULONG_PTR)modBase + exportDir->AddressOfNames); PDWORD addrArray = (PDWORD)((ULONG_PTR)modBase + exportDir->AddressOfFunctions); PWORD ordinalArray = (PWORD)((ULONG_PTR)modBase + exportDir->AddressOfNameOrdinals); // 遍历导出符号,找到目标符号 for (DWORD i = 0; i < exportDir->NumberOfNames; i++) { LPCSTR currentName = (LPCSTR)((ULONG_PTR)modBase + nameArray[i]); if (_stricmp(currentName, symbolName) == 0) { WORD ordinal = ordinalArray[i]; return (PVOID)((ULONG_PTR)modBase + addrArray[ordinal]); } } return NULL; } // 获取KeServiceDescriptorTable的地址 PVOID keServiceTableAddr = GetKernelSymbol(ntoskrnlMod->ImageBase, "KeServiceDescriptorTable"); if (!keServiceTableAddr) { printf("Failed to find KeServiceDescriptorTable!\n"); free(moduleInfo); return -1; } printf("KeServiceDescriptorTable address: 0x%p\n", keServiceTableAddr);
第五步:解析SSDT结构并输出地址
定义KeServiceDescriptorTable的结构体,解析出SSDT的基地址和服务数量:
// 定义KeServiceDescriptorTable的结构 typedef struct _KSERVICE_DESCRIPTOR_TABLE { PVOID ServiceTableBase; // SSDT的起始地址 PVOID ServiceCounterTableBase; // 服务调用计数器(可选) ULONG NumberOfServices; // SSDT中的服务函数数量 PVOID ParamTableBase; // 服务参数表地址(可选) } KSERVICE_DESCRIPTOR_TABLE, *PKSERVICE_DESCRIPTOR_TABLE; PKSERVICE_DESCRIPTOR_TABLE ssdtTable = (PKSERVICE_DESCRIPTOR_TABLE)keServiceTableAddr; printf("SSDT Base Address: 0x%p\n", ssdtTable->ServiceTableBase); printf("Number of SSDT Services: %lu\n", ssdtTable->NumberOfServices); // 示例:输出前10个SSDT函数的地址 printf("\nFirst 10 SSDT Function Addresses:\n"); PULONG ssdtFuncArray = (PULONG)ssdtTable->ServiceTableBase; for (ULONG i = 0; i < 10 && i < ssdtTable->NumberOfServices; i++) { // 注意:SSDT中存储的是相对于ntoskrnl基址的偏移,需要转换为实际地址 PVOID funcAddr = (PVOID)((ULONG_PTR)ntoskrnlMod->ImageBase + ssdtFuncArray[i]); printf("Service %lu: 0x%p\n", i, funcAddr); } // 清理资源 free(moduleInfo); return 0; }
关键注意事项
- 权限要求:程序必须以管理员权限运行,否则无法获取完整的系统模块信息。
- 版本兼容性:不同Windows版本的
KeServiceDescriptorTable结构和导出情况可能有差异,比如Windows 10及以后部分版本会使用KeServiceDescriptorTableShadow,需要额外处理。 - 用户态限制:用户态程序只能读取到SSDT的地址列表,无法直接验证这些地址是否被挂钩。如果要真正检测挂钩,你需要编写内核驱动程序,或者提前保存正常系统的SSDT地址快照进行对比。
内容的提问来源于stack exchange,提问作者user3725519
相关产品推荐
相关产品推荐

