1、课程目标
- 掌握模块加载回调的注册和使用
- 实现DLL加载监控
- 学会阻止恶意DLL加载
- 理解模块监控在安全防护中的应用
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、课后作业
- 实现DLL加载白名单机制
- 添加模块哈希校验功能
- 实现驱动加载监控和阻止
- 结合进程监控检测DLL注入
7、扩展阅读
- Windows模块加载机制
- DLL劫持防护技术
- 代码签名验证