在Windows内核开发领域,内存描述符列表(Memory Descriptor List,简称MDL)是一种关键的内核数据结构,它充当了虚拟地址与物理内存之间的桥梁。作为一名长期从事Windows驱动开发的工程师,我发现MDL技术在实际项目中有着广泛的应用场景,特别是在需要安全、高效地操作内存的场合。
MDL本质上是一个系统定义的结构体,它描述了虚拟内存缓冲区的物理页面布局。当我们需要在内核模式下访问用户模式的内存,或者需要确保内存页面被锁定在物理RAM中不被换出时,MDL就成为了不可或缺的工具。与传统的直接内存访问相比,MDL提供了更安全、更可控的内存操作方式。
重要提示:MDL操作属于高级内核编程技术,不当使用可能导致系统不稳定甚至蓝屏。建议在开发环境中充分测试后再部署到生产环境。
现代操作系统都采用虚拟内存管理机制,这意味着我们在程序中看到的地址(虚拟地址)并不是实际的物理内存地址。CPU的内存管理单元(MMU)负责将虚拟地址转换为物理地址,这个过程对应用程序是透明的。
一个关键特性是:多个虚拟地址可以映射到同一个物理页面。例如,进程A的0x1000地址和进程B的0x2000地址可能指向同一块物理内存。这种特性正是MDL技术能够实现跨进程内存操作的基础。
MDL结构体(_MDL)在ntddk.h中定义,包含以下关键字段:
通过分析这些字段,我们可以理解MDL如何精确描述内存区域:
典型的MDL内存操作遵循以下步骤:
这个流程确保了在内存操作期间,相关页面始终驻留在物理内存中,避免了页错误(page fault)导致的潜在问题。
在实现MDL驱动读写前,我们需要建立内核驱动与用户态程序之间的通信通道。Windows提供了多种驱动通信机制,其中最常用的是设备I/O控制(IOCTL)。
c复制// 设备扩展结构体示例
typedef struct _DEVICE_EXTENSION {
PDEVICE_OBJECT DeviceObject;
UNICODE_STRING DeviceName;
UNICODE_STRING SymbolicLink;
} DEVICE_EXTENSION, *PDEVICE_EXTENSION;
// 创建设备对象
NTSTATUS CreateDevice(IN PDRIVER_OBJECT DriverObject) {
NTSTATUS status;
PDEVICE_OBJECT DeviceObject = NULL;
PDEVICE_EXTENSION DeviceExtension = NULL;
// 创建设备名称
UNICODE_STRING DeviceName;
RtlInitUnicodeString(&DeviceName, L"\\Device\\MdlDriver");
// 创建设备对象
status = IoCreateDevice(
DriverObject,
sizeof(DEVICE_EXTENSION),
&DeviceName,
FILE_DEVICE_UNKNOWN,
FILE_DEVICE_SECURE_OPEN,
FALSE,
&DeviceObject);
if (!NT_SUCCESS(status)) {
return status;
}
// 设置设备扩展
DeviceExtension = (PDEVICE_EXTENSION)DeviceObject->DeviceExtension;
DeviceExtension->DeviceObject = DeviceObject;
DeviceExtension->DeviceName = DeviceName;
// 创建符号链接
UNICODE_STRING SymbolicLink;
RtlInitUnicodeString(&SymbolicLink, L"\\DosDevices\\MdlDriver");
DeviceExtension->SymbolicLink = SymbolicLink;
status = IoCreateSymbolicLink(&SymbolicLink, &DeviceName);
if (!NT_SUCCESS(status)) {
IoDeleteDevice(DeviceObject);
return status;
}
return STATUS_SUCCESS;
}
基于前文的理论基础,我们现在可以深入探讨MDL读写的具体实现。以下代码展示了完整的MDL读写功能:
c复制// MDL读取目标进程内存
NTSTATUS ReadProcessMemoryByMDL(
PEPROCESS TargetProcess,
PVOID SourceAddress,
PVOID TargetAddress,
SIZE_T Size) {
NTSTATUS status = STATUS_SUCCESS;
PMDL mdl = NULL;
PVOID mappedAddress = NULL;
KAPC_STATE apcState;
// 1. 分配MDL
mdl = IoAllocateMdl(TargetAddress, (ULONG)Size, FALSE, FALSE, NULL);
if (!mdl) {
return STATUS_INSUFFICIENT_RESOURCES;
}
__try {
// 2. 构建MDL
MmBuildMdlForNonPagedPool(mdl);
// 3. 映射锁定页面
mappedAddress = MmMapLockedPages(mdl, KernelMode);
// 4. 附加到目标进程
KeStackAttachProcess(TargetProcess, &apcState);
// 5. 执行内存拷贝
RtlCopyMemory(mappedAddress, SourceAddress, Size);
// 6. 分离目标进程
KeUnstackDetachProcess(&apcState);
} __except (EXCEPTION_EXECUTE_HANDLER) {
status = GetExceptionCode();
}
// 7. 清理资源
if (mappedAddress) {
MmUnmapLockedPages(mappedAddress, mdl);
}
if (mdl) {
IoFreeMdl(mdl);
}
return status;
}
// MDL写入目标进程内存
NTSTATUS WriteProcessMemoryByMDL(
PEPROCESS TargetProcess,
PVOID SourceAddress,
PVOID TargetAddress,
SIZE_T Size) {
NTSTATUS status = STATUS_SUCCESS;
PMDL mdl = NULL;
PVOID mappedAddress = NULL;
KAPC_STATE apcState;
// 1. 分配MDL
mdl = IoAllocateMdl(SourceAddress, (ULONG)Size, FALSE, FALSE, NULL);
if (!mdl) {
return STATUS_INSUFFICIENT_RESOURCES;
}
__try {
// 2. 构建MDL
MmBuildMdlForNonPagedPool(mdl);
// 3. 映射锁定页面
mappedAddress = MmMapLockedPages(mdl, KernelMode);
// 4. 附加到目标进程
KeStackAttachProcess(TargetProcess, &apcState);
// 5. 执行内存拷贝
RtlCopyMemory(TargetAddress, mappedAddress, Size);
// 6. 分离目标进程
KeUnstackDetachProcess(&apcState);
} __except (EXCEPTION_EXECUTE_HANDLER) {
status = GetExceptionCode();
}
// 7. 清理资源
if (mappedAddress) {
MmUnmapLockedPages(mappedAddress, mdl);
}
if (mdl) {
IoFreeMdl(mdl);
}
return status;
}
为了使用户态程序能够调用上述功能,我们需要实现IOCTL分发例程:
c复制// IOCTL控制码定义
#define IOCTL_READ_MEMORY CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define IOCTL_WRITE_MEMORY CTL_CODE(FILE_DEVICE_UNKNOWN, 0x801, METHOD_BUFFERED, FILE_ANY_ACCESS)
// 内存操作请求结构体
typedef struct _MEMORY_OPERATION {
ULONG ProcessId;
PVOID Address;
SIZE_T Size;
} MEMORY_OPERATION, *PMEMORY_OPERATION;
// 设备控制处理函数
NTSTATUS DeviceControl(
PDEVICE_OBJECT DeviceObject,
PIRP Irp) {
PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp);
NTSTATUS status = STATUS_SUCCESS;
PMEMORY_OPERATION operation = NULL;
PEPROCESS process = NULL;
switch (stack->Parameters.DeviceIoControl.IoControlCode) {
case IOCTL_READ_MEMORY:
case IOCTL_WRITE_MEMORY:
// 获取请求参数
operation = (PMEMORY_OPERATION)Irp->AssociatedIrp.SystemBuffer;
// 查找目标进程
status = PsLookupProcessByProcessId((HANDLE)operation->ProcessId, &process);
if (!NT_SUCCESS(status)) {
break;
}
// 执行内存操作
if (stack->Parameters.DeviceIoControl.IoControlCode == IOCTL_READ_MEMORY) {
status = ReadProcessMemoryByMDL(
process,
operation->Address,
Irp->AssociatedIrp.SystemBuffer,
operation->Size);
} else {
status = WriteProcessMemoryByMDL(
process,
Irp->AssociatedIrp.SystemBuffer,
operation->Address,
operation->Size);
}
// 释放进程引用
ObDereferenceObject(process);
break;
default:
status = STATUS_INVALID_DEVICE_REQUEST;
break;
}
// 完成请求
Irp->IoStatus.Status = status;
Irp->IoStatus.Information = (stack->Parameters.DeviceIoControl.IoControlCode == IOCTL_READ_MEMORY) ?
operation->Size : 0;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return status;
}
当需要操作大块内存时,直接使用MDL可能会导致性能问题。我们可以采用分块处理策略:
c复制#define MAX_MDL_SIZE (4 * 1024) // 4KB分块大小
NTSTATUS ReadLargeMemory(
PEPROCESS TargetProcess,
PVOID SourceAddress,
PVOID TargetAddress,
SIZE_T Size) {
NTSTATUS status = STATUS_SUCCESS;
SIZE_T remaining = Size;
PVOID currentSource = SourceAddress;
PVOID currentTarget = TargetAddress;
while (remaining > 0) {
SIZE_T chunkSize = min(remaining, MAX_MDL_SIZE);
status = ReadProcessMemoryByMDL(
TargetProcess,
currentSource,
currentTarget,
chunkSize);
if (!NT_SUCCESS(status)) {
break;
}
currentSource = (PVOID)((ULONG_PTR)currentSource + chunkSize);
currentTarget = (PVOID)((ULONG_PTR)currentTarget + chunkSize);
remaining -= chunkSize;
}
return status;
}
对于非连续内存区域,可以使用MDL链来描述:
c复制NTSTATUS BuildMdlChain(
PVOID* Addresses,
SIZE_T* Sizes,
ULONG Count,
PMDL* MdlChain) {
NTSTATUS status = STATUS_SUCCESS;
PMDL firstMdl = NULL;
PMDL previousMdl = NULL;
for (ULONG i = 0; i < Count; i++) {
PMDL mdl = IoAllocateMdl(Addresses[i], (ULONG)Sizes[i], FALSE, FALSE, NULL);
if (!mdl) {
status = STATUS_INSUFFICIENT_RESOURCES;
break;
}
MmBuildMdlForNonPagedPool(mdl);
if (previousMdl) {
previousMdl->Next = mdl;
} else {
firstMdl = mdl;
}
previousMdl = mdl;
}
if (NT_SUCCESS(status)) {
*MdlChain = firstMdl;
} else {
FreeMdlChain(firstMdl);
}
return status;
}
VOID FreeMdlChain(PMDL MdlChain) {
PMDL current = MdlChain;
while (current) {
PMDL next = current->Next;
IoFreeMdl(current);
current = next;
}
}
健壮的驱动应该能够处理各种内存访问异常:
c复制NTSTATUS SafeMemoryOperation(
PEPROCESS TargetProcess,
PVOID Address,
SIZE_T Size,
BOOLEAN IsWrite,
PVOID Buffer) {
NTSTATUS status = STATUS_SUCCESS;
PMDL mdl = NULL;
PVOID mappedAddress = NULL;
KAPC_STATE apcState;
// 验证地址范围
if (MmIsAddressValid(Address) == FALSE ||
MmIsAddressValid((PVOID)((ULONG_PTR)Address + Size - 1)) == FALSE) {
return STATUS_ACCESS_VIOLATION;
}
mdl = IoAllocateMdl(IsWrite ? Buffer : Address, (ULONG)Size, FALSE, FALSE, NULL);
if (!mdl) {
return STATUS_INSUFFICIENT_RESOURCES;
}
__try {
MmProbeAndLockPages(mdl, KernelMode, IsWrite ? IoWriteAccess : IoReadAccess);
mappedAddress = MmMapLockedPages(mdl, KernelMode);
KeStackAttachProcess(TargetProcess, &apcState);
if (IsWrite) {
RtlCopyMemory(Address, mappedAddress, Size);
} else {
RtlCopyMemory(mappedAddress, Address, Size);
}
KeUnstackDetachProcess(&apcState);
} __except (EXCEPTION_EXECUTE_HANDLER) {
status = GetExceptionCode();
}
if (mappedAddress) {
MmUnmapLockedPages(mappedAddress, mdl);
}
if (mdl) {
MmUnlockPages(mdl);
IoFreeMdl(mdl);
}
return status;
}
驱动代码必须严格验证所有来自用户态的输入:
c复制NTSTATUS ValidateMemoryOperationRequest(
PMEMORY_OPERATION Request,
SIZE_T InputBufferLength) {
// 验证缓冲区大小
if (InputBufferLength < sizeof(MEMORY_OPERATION)) {
return STATUS_BUFFER_TOO_SMALL;
}
// 验证进程ID
if (Request->ProcessId == 0 ||
Request->ProcessId == (ULONG)PsGetCurrentProcessId()) {
return STATUS_INVALID_PARAMETER;
}
// 验证内存地址
if ((ULONG_PTR)Request->Address < MM_USER_PROBE_ADDRESS) {
return STATUS_ACCESS_VIOLATION;
}
// 验证大小
if (Request->Size == 0 || Request->Size > MAX_ALLOWED_SIZE) {
return STATUS_INVALID_BUFFER_SIZE;
}
return STATUS_SUCCESS;
}
确保只有授权用户可以使用驱动功能:
c复制NTSTATUS CheckCallerAccess() {
PEPROCESS callerProcess = PsGetCurrentProcess();
HANDLE callerProcessId = PsGetProcessId(callerProcess);
// 检查调用者是否在允许的进程列表中
if (!IsProcessAllowed(callerProcessId)) {
return STATUS_ACCESS_DENIED;
}
// 检查调用者权限
if (!SeSinglePrivilegeCheck(SeDebugPrivilege, PsGetCurrentThreadPreviousMode())) {
return STATUS_PRIVILEGE_NOT_HELD;
}
return STATUS_SUCCESS;
}
确保所有分配的资源都被正确释放:
c复制VOID CleanupResources(
PMDL Mdl,
PVOID MappedAddress,
PEPROCESS AttachedProcess,
PKAPC_STATE ApcState) {
if (MappedAddress && Mdl) {
MmUnmapLockedPages(MappedAddress, Mdl);
}
if (Mdl) {
IoFreeMdl(Mdl);
}
if (AttachedProcess && ApcState) {
KeUnstackDetachProcess(ApcState);
ObDereferenceObject(AttachedProcess);
}
}
记录关键操作以便审计和调试:
c复制VOID LogMemoryOperation(
ULONG ProcessId,
PVOID Address,
SIZE_T Size,
BOOLEAN IsWrite,
NTSTATUS Status) {
LARGE_INTEGER systemTime;
KeQuerySystemTime(&systemTime);
DbgPrintEx(
DPFLTR_IHVDRIVER_ID,
DPFLTR_ERROR_LEVEL,
"[%lld] %s operation on process %u at address 0x%p, size %zu - Status: 0x%X\n",
systemTime.QuadPart,
IsWrite ? "Write" : "Read",
ProcessId,
Address,
Size,
Status);
}
MDL技术常用于实现进程内存扫描功能,以下是查找特定内存模式的实现:
c复制NTSTATUS FindMemoryPattern(
PEPROCESS TargetProcess,
PVOID StartAddress,
SIZE_T SearchSize,
PCUCHAR Pattern,
SIZE_T PatternSize,
PVOID* FoundAddress) {
NTSTATUS status = STATUS_NOT_FOUND;
PMDL mdl = NULL;
PVOID mappedAddress = NULL;
KAPC_STATE apcState;
PVOID currentAddress = StartAddress;
SIZE_T remainingSize = SearchSize;
while (remainingSize >= PatternSize) {
SIZE_T chunkSize = min(remainingSize, MAX_MDL_SIZE);
mdl = IoAllocateMdl(currentAddress, (ULONG)chunkSize, FALSE, FALSE, NULL);
if (!mdl) {
return STATUS_INSUFFICIENT_RESOURCES;
}
__try {
MmProbeAndLockPages(mdl, UserMode, IoReadAccess);
mappedAddress = MmMapLockedPagesSpecifyCache(
mdl,
KernelMode,
MmCached,
NULL,
FALSE,
NormalPagePriority);
if (!mappedAddress) {
status = STATUS_INSUFFICIENT_RESOURCES;
__leave;
}
KeStackAttachProcess(TargetProcess, &apcState);
// 执行模式匹配
for (SIZE_T i = 0; i <= chunkSize - PatternSize; i++) {
if (RtlCompareMemory(
(PUCHAR)mappedAddress + i,
Pattern,
PatternSize) == PatternSize) {
*FoundAddress = (PVOID)((ULONG_PTR)currentAddress + i);
status = STATUS_SUCCESS;
__leave;
}
}
KeUnstackDetachProcess(&apcState);
} __except (EXCEPTION_EXECUTE_HANDLER) {
status = GetExceptionCode();
}
// 清理资源
if (mappedAddress) {
MmUnmapLockedPages(mappedAddress, mdl);
mappedAddress = NULL;
}
if (mdl) {
MmUnlockPages(mdl);
IoFreeMdl(mdl);
mdl = NULL;
}
if (NT_SUCCESS(status)) {
break;
}
currentAddress = (PVOID)((ULONG_PTR)currentAddress + chunkSize);
remainingSize -= chunkSize;
}
return status;
}
MDL也可用于驱动间的高效数据传输:
c复制NTSTATUS SendMdlToAnotherDriver(
PDEVICE_OBJECT TargetDevice,
PVOID Buffer,
SIZE_T Size) {
NTSTATUS status;
PMDL mdl = NULL;
PIRP irp = NULL;
KEVENT event;
IO_STATUS_BLOCK ioStatus;
// 初始化事件
KeInitializeEvent(&event, NotificationEvent, FALSE);
// 创建MDL
mdl = IoAllocateMdl(Buffer, (ULONG)Size, FALSE, FALSE, NULL);
if (!mdl) {
return STATUS_INSUFFICIENT_RESOURCES;
}
__try {
MmBuildMdlForNonPagedPool(mdl);
// 创建IRP
irp = IoBuildDeviceIoControlRequest(
IOCTL_RECEIVE_MDL,
TargetDevice,
NULL,
0,
NULL,
0,
FALSE,
&event,
&ioStatus);
if (!irp) {
status = STATUS_INSUFFICIENT_RESOURCES;
__leave;
}
// 设置MDL
irp->MdlAddress = mdl;
// 发送IRP
status = IoCallDriver(TargetDevice, irp);
if (status == STATUS_PENDING) {
KeWaitForSingleObject(&event, Executive, KernelMode, FALSE, NULL);
status = ioStatus.Status;
}
} __finally {
if (!NT_SUCCESS(status) && irp) {
IoFreeIrp(irp);
}
// 不释放MDL,接收方负责释放
}
return status;
}
使用MDL实现低开销的性能监控:
c复制NTSTATUS MonitorProcessMemory(
PEPROCESS TargetProcess,
PVOID MonitorAddress,
SIZE_T MonitorSize,
PMEMORY_CHANGE_CALLBACK Callback,
PVOID Context) {
NTSTATUS status;
PMONITOR_CONTEXT monitorContext = NULL;
// 分配监控上下文
monitorContext = ExAllocatePoolWithTag(NonPagedPool, sizeof(MONITOR_CONTEXT), 'MDLM');
if (!monitorContext) {
return STATUS_INSUFFICIENT_RESOURCES;
}
// 初始化上下文
monitorContext->TargetProcess = TargetProcess;
monitorContext->MonitorAddress = MonitorAddress;
monitorContext->MonitorSize = MonitorSize;
monitorContext->Callback = Callback;
monitorContext->Context = Context;
ObReferenceObject(TargetProcess);
// 创建工作项
ExInitializeWorkItem(&monitorContext->WorkItem, MonitorMemoryWorkItem, monitorContext);
// 提交工作项
ExQueueWorkItem(&monitorContext->WorkItem, CriticalWorkQueue);
return STATUS_SUCCESS;
}
VOID MonitorMemoryWorkItem(
PVOID Parameter) {
PMONITOR_CONTEXT context = (PMONITOR_CONTEXT)Parameter;
PMDL mdl = NULL;
PVOID mappedAddress = NULL;
KAPC_STATE apcState;
PUCHAR currentBuffer = NULL;
// 分配MDL和缓冲区
mdl = IoAllocateMdl(context->MonitorAddress, (ULONG)context->MonitorSize, FALSE, FALSE, NULL);
if (!mdl) {
goto Cleanup;
}
currentBuffer = ExAllocatePoolWithTag(PagedPool, context->MonitorSize, 'BUFM');
if (!currentBuffer) {
goto Cleanup;
}
__try {
// 构建MDL并映射
MmProbeAndLockPages(mdl, KernelMode, IoReadAccess);
mappedAddress = MmMapLockedPagesSpecifyCache(
mdl,
KernelMode,
MmCached,
NULL,
FALSE,
NormalPagePriority);
if (!mappedAddress) {
__leave;
}
// 附加到目标进程
KeStackAttachProcess(context->TargetProcess, &apcState);
// 读取初始内存内容
RtlCopyMemory(currentBuffer, mappedAddress, context->MonitorSize);
// 分离进程
KeUnstackDetachProcess(&apcState);
// 持续监控
while (TRUE) {
KeDelayExecutionThread(KernelMode, FALSE, &context->Interval);
KeStackAttachProcess(context->TargetProcess, &apcState);
// 比较内存变化
if (RtlCompareMemory(
mappedAddress,
currentBuffer,
context->MonitorSize) != context->MonitorSize) {
// 调用回调函数
context->Callback(
context->MonitorAddress,
currentBuffer,
mappedAddress,
context->MonitorSize,
context->Context);
// 更新缓冲区
RtlCopyMemory(currentBuffer, mappedAddress, context->MonitorSize);
}
KeUnstackDetachProcess(&apcState);
}
} __except (EXCEPTION_EXECUTE_HANDLER) {
// 处理异常
}
Cleanup:
// 清理资源
if (mappedAddress) {
MmUnmapLockedPages(mappedAddress, mdl);
}
if (mdl) {
MmUnlockPages(mdl);
IoFreeMdl(mdl);
}
if (currentBuffer) {
ExFreePoolWithTag(currentBuffer, 'BUFM');
}
ObDereferenceObject(context->TargetProcess);
ExFreePoolWithTag(context, 'MDLM');
}