You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.26 09:18:41