Windows内核安全

11、模块监控

1、课程目标

  1. 掌握模块加载回调的注册和使用
  2. 实现DLL加载监控
  3. 学会阻止恶意DLL加载
  4. 理解模块监控在安全防护中的应用

2、名词解释

术语 解释
PsSetLoadImageNotifyRoutine 注册模块加载回调
PLOAD_IMAGE_NOTIFY_ROUTINE 模块加载回调函数类型
ImageInfo 模块加载信息结构
模块基址 模块在内存中的加载地址

3、使用工具

工具 用途
Process Monitor 监控模块加载
WinDbg lm命令查看模块
Dependency Walker 分析DLL依赖

4、技术原理

4.1、模块加载回调机制

┌─────────────────────────────────────────────────────────────┐
│                    模块加载回调流程                          │
│                                                             │
│   LoadLibrary() / LdrLoadDll()                              │
│        │                                                    │
│        ↓                                                    │
│   MmLoadSystemImage() / MiMapViewOfImageSection()           │
│        │                                                    │
│        ↓                                                    │
│   ┌─────────────────────────────────────────┐              │
│   │     调用模块加载回调                      │              │
│   │  PsSetLoadImageNotifyRoutine            │              │
│   │                                         │              │
│   │  参数:                                  │              │
│   │  - FullImageName (模块完整路径)          │              │
│   │  - ProcessId     (目标进程ID)           │              │
│   │  - ImageInfo     (模块信息)             │              │
│   └─────────────────────────────────────────┘              │
│                                                             │
│  注意:回调在模块映射后调用,无法直接阻止加载                 │
│  需要通过其他方式(如修改入口点)实现拦截                    │
└─────────────────────────────────────────────────────────────┘

5、代码实现

5.1、示例1:基础模块监控

// ImageMonitor.c - 基础模块监控
#include <ntddk.h>

// 模块加载回调
VOID ImageLoadNotifyCallback(
    PUNICODE_STRING FullImageName,
    HANDLE ProcessId,
    PIMAGE_INFO ImageInfo
) {
    // 判断是内核模块还是用户模块
    if (ImageInfo->SystemModeImage) {
        // 内核模块(驱动)加载
        DbgPrint("[Image] Kernel module loaded: %wZ\n", FullImageName);
        DbgPrint("[Image]   Base: 0x%p, Size: 0x%X\n", 
                 ImageInfo->ImageBase, (ULONG)ImageInfo->ImageSize);
    } else {
        // 用户模块(DLL)加载
        PEPROCESS process;
        if (NT_SUCCESS(PsLookupProcessByProcessId(ProcessId, &process))) {
            PUCHAR processName = PsGetProcessImageFileName(process);
            
            DbgPrint("[Image] User module loaded in %s (PID=%d)\n",
                     processName, (ULONG)(ULONG_PTR)ProcessId);
            DbgPrint("[Image]   Module: %wZ\n", FullImageName);
            DbgPrint("[Image]   Base: 0x%p, Size: 0x%X\n",
                     ImageInfo->ImageBase, (ULONG)ImageInfo->ImageSize);
            
            ObDereferenceObject(process);
        }
    }
}

// 注册模块回调
NTSTATUS RegisterImageCallback() {
    return PsSetLoadImageNotifyRoutine(ImageLoadNotifyCallback);
}

// 注销模块回调
VOID UnregisterImageCallback() {
    PsRemoveLoadImageNotifyRoutine(ImageLoadNotifyCallback);
}

5.2、示例2:DLL加载黑名单

// DllBlacklist.c - DLL黑名单监控
#include <ntddk.h>

// 黑名单DLL
WCHAR* g_DllBlacklist[] = {
    L"\\inject.dll",
    L"\\hook.dll",
    L"\\malware.dll",
    L"\\keylogger.dll"
};
#define BLACKLIST_COUNT (sizeof(g_DllBlacklist) / sizeof(g_DllBlacklist[0]))

// 检查是否在黑名单中
BOOLEAN IsDllBlacklisted(PUNICODE_STRING ImageName) {
    if (!ImageName || !ImageName->Buffer) return FALSE;
    
    for (ULONG i = 0; i < BLACKLIST_COUNT; i++) {
        UNICODE_STRING blackItem;
        RtlInitUnicodeString(&blackItem, g_DllBlacklist[i]);
        
        // 检查后缀
        if (ImageName->Length >= blackItem.Length) {
            UNICODE_STRING suffix;
            suffix.Buffer = ImageName->Buffer + 
                           (ImageName->Length - blackItem.Length) / sizeof(WCHAR);
            suffix.Length = blackItem.Length;
            suffix.MaximumLength = blackItem.Length;
            
            if (RtlEqualUnicodeString(&suffix, &blackItem, TRUE)) {
                return TRUE;
            }
        }
    }
    
    return FALSE;
}

// 终止加载恶意DLL的进程
VOID TerminateProcessWithMaliciousDll(HANDLE ProcessId) {
    PEPROCESS process;
    
    if (NT_SUCCESS(PsLookupProcessByProcessId(ProcessId, &process))) {
        // 警告:终止进程可能导致系统不稳定
        // 生产环境应使用更优雅的方式
        
        DbgPrint("[Image] Terminating process with malicious DLL: PID=%d\n",
                 (ULONG)(ULONG_PTR)ProcessId);
        
        // ZwTerminateProcess需要进程句柄
        HANDLE hProcess;
        if (NT_SUCCESS(ObOpenObjectByPointer(process, OBJ_KERNEL_HANDLE, 
                NULL, PROCESS_TERMINATE, *PsProcessType, KernelMode, &hProcess))) {
            ZwTerminateProcess(hProcess, STATUS_ACCESS_DENIED);
            ZwClose(hProcess);
        }
        
        ObDereferenceObject(process);
    }
}

// 黑名单模块回调
VOID BlacklistImageCallback(
    PUNICODE_STRING FullImageName,
    HANDLE ProcessId,
    PIMAGE_INFO ImageInfo
) {
    UNREFERENCED_PARAMETER(ImageInfo);
    
    // 只检查用户模块
    if (ImageInfo->SystemModeImage) return;
    
    if (IsDllBlacklisted(FullImageName)) {
        DbgPrint("[Image] !!! BLACKLISTED DLL DETECTED !!!\n");
        DbgPrint("[Image] DLL: %wZ in PID=%d\n", 
                 FullImageName, (ULONG)(ULONG_PTR)ProcessId);
        
        // 可以选择终止进程或采取其他措施
        // TerminateProcessWithMaliciousDll(ProcessId);
    }
}

5.3、示例3:模块签名验证

// ImageSignature.c - 模块签名验证
#include <ntddk.h>

// 检查模块是否有有效签名
NTSTATUS VerifyImageSignature(PUNICODE_STRING ImagePath) {
    NTSTATUS status;
    
    // 使用 Ci.dll 的签名验证函数(需要导入)
    // 这里使用简化的方法:检查文件属性
    
    OBJECT_ATTRIBUTES objAttr;
    IO_STATUS_BLOCK ioStatus;
    HANDLE hFile;
    
    InitializeObjectAttributes(&objAttr, ImagePath, 
        OBJ_KERNEL_HANDLE | OBJ_CASE_INSENSITIVE, NULL, NULL);
    
    status = ZwOpenFile(&hFile, FILE_READ_ATTRIBUTES | SYNCHRONIZE,
        &objAttr, &ioStatus, FILE_SHARE_READ,
        FILE_SYNCHRONOUS_IO_NONALERT);
    
    if (NT_SUCCESS(status)) {
        // 在这里可以读取PE头验证签名
        // 实际实现需要使用CiCheckSignedFile等API
        ZwClose(hFile);
    }
    
    return status;
}

// 记录模块信息
typedef struct _MODULE_INFO {
    HANDLE      ProcessId;
    PVOID       ImageBase;
    SIZE_T      ImageSize;
    WCHAR       ImagePath[260];
    BOOLEAN     IsKernel;
    BOOLEAN     IsSigned;
    LARGE_INTEGER LoadTime;
    LIST_ENTRY  ListEntry;
} MODULE_INFO, *PMODULE_INFO;

LIST_ENTRY g_ModuleList;
KSPIN_LOCK g_ModuleLock;

VOID InitModuleList() {
    InitializeListHead(&g_ModuleList);
    KeInitializeSpinLock(&g_ModuleLock);
}

VOID RecordModuleLoad(
    PUNICODE_STRING FullImageName,
    HANDLE ProcessId,
    PIMAGE_INFO ImageInfo
) {
    PMODULE_INFO info = (PMODULE_INFO)ExAllocatePoolWithTag(
        NonPagedPool, sizeof(MODULE_INFO), 'doMI');
    
    if (!info) return;
    
    info->ProcessId = ProcessId;
    info->ImageBase = ImageInfo->ImageBase;
    info->ImageSize = ImageInfo->ImageSize;
    info->IsKernel = ImageInfo->SystemModeImage;
    info->IsSigned = FALSE;  // 需要实际验证
    KeQuerySystemTime(&info->LoadTime);
    
    if (FullImageName && FullImageName->Buffer) {
        ULONG copyLen = min(FullImageName->Length, sizeof(info->ImagePath) - sizeof(WCHAR));
        RtlCopyMemory(info->ImagePath, FullImageName->Buffer, copyLen);
        info->ImagePath[copyLen / sizeof(WCHAR)] = L'\0';
    }
    
    KIRQL oldIrql;
    KeAcquireSpinLock(&g_ModuleLock, &oldIrql);
    InsertTailList(&g_ModuleList, &info->ListEntry);
    KeReleaseSpinLock(&g_ModuleLock, oldIrql);
}

// 查询进程加载的模块
ULONG GetProcessModules(HANDLE ProcessId, PMODULE_INFO Buffer, ULONG MaxCount) {
    KIRQL oldIrql;
    ULONG count = 0;
    PLIST_ENTRY entry;
    
    KeAcquireSpinLock(&g_ModuleLock, &oldIrql);
    
    for (entry = g_ModuleList.Flink; 
         entry != &g_ModuleList && count < MaxCount; 
         entry = entry->Flink) {
        PMODULE_INFO info = CONTAINING_RECORD(entry, MODULE_INFO, ListEntry);
        
        if (info->ProcessId == ProcessId) {
            RtlCopyMemory(&Buffer[count], info, sizeof(MODULE_INFO) - sizeof(LIST_ENTRY));
            count++;
        }
    }
    
    KeReleaseSpinLock(&g_ModuleLock, oldIrql);
    
    return count;
}

5.4、示例4:DLL劫持检测

// DllHijackDetect.c - DLL劫持检测
#include <ntddk.h>

// 系统DLL的正确路径
typedef struct _SYSTEM_DLL_INFO {
    WCHAR* DllName;
    WCHAR* ExpectedPath;
} SYSTEM_DLL_INFO;

SYSTEM_DLL_INFO g_SystemDlls[] = {
    { L"kernel32.dll", L"\\SystemRoot\\System32\\kernel32.dll" },
    { L"ntdll.dll", L"\\SystemRoot\\System32\\ntdll.dll" },
    { L"user32.dll", L"\\SystemRoot\\System32\\user32.dll" },
    { L"advapi32.dll", L"\\SystemRoot\\System32\\advapi32.dll" },
    { L"ws2_32.dll", L"\\SystemRoot\\System32\\ws2_32.dll" }
};
#define SYSTEM_DLL_COUNT (sizeof(g_SystemDlls) / sizeof(g_SystemDlls[0]))

// 检查DLL是否来自预期路径
BOOLEAN CheckDllPath(PUNICODE_STRING ActualPath, PCWSTR DllName) {
    for (ULONG i = 0; i < SYSTEM_DLL_COUNT; i++) {
        // 检查DLL名称是否匹配
        UNICODE_STRING dllNameStr;
        RtlInitUnicodeString(&dllNameStr, g_SystemDlls[i].DllName);
        
        // 提取实际路径中的文件名
        WCHAR* fileName = wcsrchr(ActualPath->Buffer, L'\\');
        if (fileName) {
            fileName++;  // 跳过反斜杠
            
            UNICODE_STRING actualFileName;
            RtlInitUnicodeString(&actualFileName, fileName);
            
            if (RtlEqualUnicodeString(&actualFileName, &dllNameStr, TRUE)) {
                // DLL名称匹配,检查路径
                UNICODE_STRING expectedPath;
                RtlInitUnicodeString(&expectedPath, g_SystemDlls[i].ExpectedPath);
                
                if (!RtlEqualUnicodeString(ActualPath, &expectedPath, TRUE)) {
                    // 路径不匹配 - 可能是DLL劫持
                    return FALSE;
                }
            }
        }
    }
    
    return TRUE;
}

// DLL劫持检测回调
VOID DllHijackDetectCallback(
    PUNICODE_STRING FullImageName,
    HANDLE ProcessId,
    PIMAGE_INFO ImageInfo
) {
    if (ImageInfo->SystemModeImage) return;
    if (!FullImageName || !FullImageName->Buffer) return;
    
    // 检查是否是系统DLL从非系统目录加载
    if (!CheckDllPath(FullImageName, NULL)) {
        PEPROCESS process;
        if (NT_SUCCESS(PsLookupProcessByProcessId(ProcessId, &process))) {
            PUCHAR processName = PsGetProcessImageFileName(process);
            
            DbgPrint("[Hijack] !!! POTENTIAL DLL HIJACKING DETECTED !!!\n");
            DbgPrint("[Hijack] Process: %s (PID=%d)\n", 
                     processName, (ULONG)(ULONG_PTR)ProcessId);
            DbgPrint("[Hijack] Suspicious DLL: %wZ\n", FullImageName);
            
            ObDereferenceObject(process);
        }
    }
}

// 检查DLL是否在进程目录(常见劫持方式)
BOOLEAN IsDllInProcessDirectory(HANDLE ProcessId, PUNICODE_STRING DllPath) {
    PEPROCESS process;
    BOOLEAN result = FALSE;
    
    if (NT_SUCCESS(PsLookupProcessByProcessId(ProcessId, &process))) {
        // 获取进程映像路径
        // 比较DLL路径是否在进程目录
        // 如果系统DLL从进程目录加载,可能是劫持
        
        ObDereferenceObject(process);
    }
    
    return result;
}

5.5、示例5:用户模式模块枚举

// ModuleEnum.c - 用户模式模块枚举
#include <windows.h>
#include <stdio.h>
#include <tlhelp32.h>

// 从驱动获取模块加载事件
typedef struct _MODULE_EVENT {
    DWORD   ProcessId;
    PVOID   ImageBase;
    DWORD   ImageSize;
    WCHAR   ImagePath[260];
    BOOL    IsKernel;
} MODULE_EVENT;

// 枚举进程模块(用户模式)
void EnumProcessModules(DWORD pid) {
    HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32, pid);
    
    if (hSnapshot == INVALID_HANDLE_VALUE) {
        printf("Failed to create snapshot: %d\n", GetLastError());
        return;
    }
    
    MODULEENTRY32 me = { sizeof(me) };
    
    if (Module32First(hSnapshot, &me)) {
        printf("Modules in PID %d:\n", pid);
        do {
            printf("  %s\n", me.szModule);
            printf("    Base: 0x%p, Size: 0x%X\n", me.modBaseAddr, me.modBaseSize);
            printf("    Path: %s\n", me.szExePath);
        } while (Module32Next(hSnapshot, &me));
    }
    
    CloseHandle(hSnapshot);
}

// 监控模块加载事件
DWORD WINAPI ModuleMonitorThread(LPVOID lpParam) {
    HANDLE hDevice = (HANDLE)lpParam;
    MODULE_EVENT events[100];
    DWORD bytesReturned;
    
    #define IOCTL_GET_MODULE_EVENTS CTL_CODE(0x8000, 0x850, METHOD_BUFFERED, FILE_ANY_ACCESS)
    
    while (TRUE) {
        if (DeviceIoControl(hDevice, IOCTL_GET_MODULE_EVENTS,
                NULL, 0, events, sizeof(events), &bytesReturned, NULL)) {
            DWORD count = bytesReturned / sizeof(MODULE_EVENT);
            
            for (DWORD i = 0; i < count; i++) {
                printf("[Module] PID=%d, %ws\n", 
                       events[i].ProcessId, events[i].ImagePath);
                
                // 检查是否是可疑模块
                if (wcsstr(events[i].ImagePath, L"inject") ||
                    wcsstr(events[i].ImagePath, L"hook")) {
                    printf("  !!! SUSPICIOUS MODULE !!!\n");
                }
            }
        }
        
        Sleep(100);
    }
    
    return 0;
}

int main() {
    printf("=== Module Monitor ===\n\n");
    
    // 枚举当前进程的模块
    EnumProcessModules(GetCurrentProcessId());
    
    return 0;
}

6、课后作业

  1. 实现DLL加载白名单机制
  2. 添加模块哈希校验功能
  3. 实现驱动加载监控和阻止
  4. 结合进程监控检测DLL注入

7、扩展阅读

  • Windows模块加载机制
  • DLL劫持防护技术
  • 代码签名验证