通过MDL读写进程内存
本文总结于lyshark的《Windows内核安全编程技术实践》。
MDL
Windows采用了分页机制来管理内存。考虑一个虚拟地址,要经过逐级查表,才可以得到物理地址。
逻辑上连续的虚拟页,很大概率会被映射到不连续的物理页中。
假设现在要求对一块内存进行高效的操作(比如IO),频繁地进行查表转换操作效率太低,所以可以把需要用到的页表的物理地址,以链表的形式进行存储,这样就避免了反复查表的开销。
MDL的具体结构Windows并未公开,这里只能参考其他人逆向得到的布局:
typedef struct _MDL {
struct _MDL *Next; // 指向下一个 MDL,形成 MDL 链
CSHORT Size; // 本 MDL 结构的总大小(字节)
CSHORT MdlFlags; // 标志位(见下表)
struct _EPROCESS *Process; // 所属进程(若为用户模式缓冲区)
PVOID MappedSystemVa; // 映射后的系统空间虚拟地址(若已映射)
PVOID StartVa; // 缓冲区起始页的虚拟地址(页对齐)
ULONG ByteCount; // 缓冲区总字节数
ULONG ByteOffset; // 缓冲区在起始页内的字节偏移
} MDL, *PMDL;
引用自Microsoft官方文档链接:
MDL 结构是半不透明的。 驱动程序应仅直接访问此结构的 Next 和 MdlFlags 成员。
通过MDL读取进程内存
主要步骤
1. KeStackAttachProcess() // 附加到目标进程
2. IoAllocateMdl() // 创建 MDL
3. MmProbeAndLockPages() // 探测并锁定页面
4. KeUnstackDetachProcess() // 脱离目标进程
5. MmGetSystemAddressForMdlSafe() // 获取系统空间地址
6. RtlCopyMemory() // 读取数据
7. MmUnlockPages() + IoFreeMdl() // 清理资源
附加到目标进程
首先我们需要切换当前线程的地址空间到目标进程中,因为IoAllocateMdl需要知道目标虚拟地址在哪个进程上下文中,并且我们还要在目标进程的地址空间中验证地址有效性,确保后续的MmProbeAndLockPages能正确访问目标进程的页表。
创建MDL
PMDL pMdl = IoAllocateMdl(
(PVOID)data->pTargetAddr, // 目标虚拟地址
data->dwSize, // 要读取的大小
false, // SecondaryBuffer(通常为false)
false, // ChargeQuota(通常为false)
nullptr // Irp(可选)
);
创建一个 MDL 来描述目标内存区域。
关键:探测并锁定页面
MmProbeAndLockPages(pMdl, UserMode, IoReadAccess);
这是最关键的一步,包含两个操作:
探测(Probe)
- 验证地址有效性:确保地址在用户模式地址空间(0x00000000-0x7FFFFFFF)
- 检查访问权限:验证当前是否有读取权限
- 对齐检查:确保地址和大小符合对齐要求
- 异常处理:如果地址无效,会抛出异常(代码中缺少 try/except)
锁定(Lock)
- 确保页面驻留:将相关页面锁定在物理内存中,防止被换出到磁盘
- 建立物理映射:在 MDL 中记录物理页面信息
- 增加引用计数:防止页面被释放
参数说明:
-
UserMode:表示地址是用户模式地址
-
IoReadAccess:指定只读访问权限
脱离目标进程
MDL 已经捕获了物理页面信息,物理页面被锁定在内存中,不再需要目标进程的地址空间上下文。这样一来,我们就可以减少对目标进程的干扰、允许在脱离状态下长时间访问内存、避免保持附加状态可能导致的死锁问题。
获取系统空间地址
PVOID mappedAddress = MmGetSystemAddressForMdlSafe(pMdl, NormalPagePriority);
目的:将 MDL 描述的物理页面映射到系统空间。
工作原理:
-
根据 MDL 中的物理页面信息
-
创建系统空间的虚拟地址映射
-
返回一个在内核中可以直接访问的指针
该函数返回的地址在内核空间中,我们可以直接使用RtlCopyMemory等进行访问。
示例代码
#include <ntifs.h>
#include <windef.h>
struct Data
{
DWORD dwPID;
PVOID pTargetAddr;
PBYTE data;
DWORD dwSize;
};
VOID DriverUnload(PDRIVER_OBJECT pDriverObj)
{
UNREFERENCED_PARAMETER(pDriverObj);
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL,
"Uninstall Driver Successfully!\\n");
}
BOOL MDL_ReadMemory(Data* data)
{
PEPROCESS pEp = nullptr;
NTSTATUS status = PsLookupProcessByProcessId((HANDLE)data->dwPID, &pEp);
if (!NT_SUCCESS(status) || pEp == nullptr)
{
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL,
"find process failed!\\n");
return FALSE;
}
// 附加到进程
KAPC_STATE stack = { 0 };
KeStackAttachProcess(pEp, &stack);
// 创建MDL
PMDL pMdl = IoAllocateMdl((PVOID)data->pTargetAddr, data->dwSize,
false, false, nullptr);
if (!pMdl)
{
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL,
"find process failed!\\n");
KeUnstackDetachProcess(&stack);
ObDereferenceObject(pEp);
return false;
}
// 探测并锁定页面
MmProbeAndLockPages(pMdl, UserMode, IoReadAccess);
// 取消附加
KeUnstackDetachProcess(&stack);
// 获取系统空间映射地址
PVOID mappedAddress = MmGetSystemAddressForMdlSafe(pMdl, NormalPagePriority);
if (!mappedAddress)
{
MmUnlockPages(pMdl);
IoFreeMdl(pMdl);
ObDereferenceObject(pEp);
return false;
}
// 读取数据
RtlCopyMemory(data->data, mappedAddress, data->dwSize);
// 清理资源
MmUnlockPages(pMdl);
IoFreeMdl(pMdl);
ObDereferenceObject(pEp);
return true;
}
EXTERN_C NTSTATUS DriverEntry(PDRIVER_OBJECT pDriverObj, PUNICODE_STRING pRegPath)
{
UNREFERENCED_PARAMETER(pRegPath);
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL,
"Installing Driver…\\n");
pDriverObj->DriverUnload = DriverUnload;
Data data;
data.dwPID = 6776;
data.pTargetAddr = (PVOID)0x00000056ac965000;
data.dwSize = 20;
if (MDL_ReadMemory(&data))
{
for (size_t i = 0; i < data.dwSize / sizeof(DWORD); i++)
{
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL,
"Readed: %x\\n", ((PDWORD)data.data)[i]);
}
}
else
{
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL,
"Installing Driver…\\n");
}
return STATUS_SUCCESS;
}
