对MDL(Memory Descriptor List)的初步学习

首先查阅CSDN,在其中有这样的定义:
内存描述符列表 (MDL) 是一个系统定义的结构,通过一系列物理地址描述缓冲区。执行直接 I/O 的驱动程序从 I/O 管理器接收一个 MDL 的指针,并通过 MDL 读写数据。一些驱动程序在执行直接 I/O 来满足设备 I/O 控制请求时也使用 MDL。

MDL分为两部分:固定长部分和变长部分,固定长部分结构如下:
在这里插入图片描述

    Next:MDL可以连接成一个单链表,因此可以将分散的虚拟机地址串接起来
    Size:整个MDL列表的长度,MDL只是整个列表的头部,后面跟着一块内存,整个MDL列表则是对于一个缓冲区页面的描述。
    MdlFlags:很重要的字段,用于描述和操控虚拟地址的各种属性,指明Mdl的映射方式。
    Process:如果虚拟地址是某一进程的用户地址空间,那么MDL代表的这块虚拟地址必须是从属于某一个进程,这个成员指向从属进程的结构
    MappedSystemVa:该MDL结构对应的物理页面可能被映射到内核地址空间,这个成员代表这个内核地址空间下的虚拟地址。对MmBuildMdlForNonPagedPool的逆向表明,MappedSystemVa = StartVa +ByteOffset。这是因为这个函数的输入MDL,其StartVa是由ExAllocatePoolWithTag决定的,所以已经从内核空间到物理页面建立了映射,MappedSystemVa自然就可以这样算。 可以猜测,如果是调用MmProbeAndLockPages 返回,则MappedSystemVa不会与StartVa有这样的对应关系,因为此时对应的物理页面还没有被映射到内核空间。(此处未定,MmProbeAndLockPages 是否会到PDE与PTE中建立映射,未知。)
    StartVa:虚拟地址空间的首地址,当这块虚拟地址描述的是一个用户进程地址空间的一块时,这个地址从属于某一个进程。(页对齐)
    ByteCount:虚拟地址块的大小,字节数
    ByteOffset:业内的偏移StartVa+ByteCount等于缓冲区的开始地址

经过整理学习,我简单地理解为MDL是用来建立一块虚拟地址空间与物理页面之间的映射,当Driver要存取某段内存位置时,确保MDL所描述的内存位置不会引起缺页错误。

虚拟地址在mdl中可以通过一个宏获得,物理地址的页帧存放在紧接着mdl的内存当中(MDL是一个变长结构,在MDL头后的一个数组才是真正存放着物理地址的地方,这个可以通过(PPFN_NUMBER) (Mdl + 1)得到物理页帧)。
需要特别注意的:MDL只能在内核态使用,但它指定的虚拟内存即可以是内核态地址也可以是用户态地址。

它可以被用来解决一些问题,最常见的是:
《内核情景分析》一书中给出了具体的问题说明和原因,总结如下:
在这里插入图片描述
解决方案:
比较常用的做法是通过MDL进行内存的重映射。简单地说就是将同一块物理内存同时映射到用户态空间和内核态空间:
示意图如下:
在这里插入图片描述
临时为用户空间增添一个系统空间映射,通过以物理页面就有了俩个虚拟地址区间,其一就是原来的用户空间虚拟地址区间,其二则是系统空间的虚拟地址区间。通过系统空间的虚拟地址访问用户空间缓冲区,完成操作后撤销系统空间的映射。
注意:直接方法对于很小的缓冲区是不划算的,因为临时映射的建立和撤销需要一定的开销,对于大一点的缓冲区才合适。
因为有俩个虚拟地址空间,所以也就有了俩种情况。

1.内核态分配空间,用户态进程去映射。
2.用户态进程分配空间,内核态去映射

一、内核态分配空间,用户态进程去映射

(1)、非分页内存

	WCHAR* v1 = L"HelloWorld";
	WCHAR* BufferData = ExAllocatePool(NonPagedPool, sizeof(WCHAR)*BufferLength);
	memcpy(BufferData, v1, 20);

先申请一块内存,这里我们先以“非分页”为例。长度为20,拷贝数据“HelloWorld”,如下图,我们可以看到我们申请内存在内核空间和里面的数据。
在这里插入图片描述

/*申请mdl*/
		Mdl = IoAllocateMdl(BufferData, BufferLength, FALSE, FALSE, NULL);

我们将我们申请的内存BufferData作为参数,申请Mdl:
在这里插入图片描述
我们来看一下IoAllocateMdl是如何申请的。(读源码)
在这里插入图片描述
首先判断,我们传进来的长度是否超出2GB。是,就直接退出。
在这里插入图片描述
如果长度小于2GB,调用ADDRESS_AND_SIZE_TO_SPAN_PAGES返回这个虚拟地址跨越了多少虚拟页面
23是MDL的固定大小。如果mdl超过23个虚拟页面,就重新计算大小,代用实际分配的虚拟页面数。
如果没有超过23个页面,就使用固定大小。
这里Flags初始为0,Flags|=MDL_ALLOCATED_FIXED_SIZE,也就是这里为什么是On8

#define MDL_ALLOCATED_FIXED_SIZE    0x0008

在这里插入图片描述
在这里插入图片描述
我们求完Size后,申请一块内存分配一个Mdl。截止到这,申请Mdl的任务完成。
在这里插入图片描述
使用MmInitializeMdl()对Mdl进行初始化。
在这里插入图片描述
查看宏定义,我们就可以知道Mdl的初始化过程。
在这里插入图片描述
所以Mdl经过IoAllocateMdl的调用,成员变化如下:
MdlFlags = On8:MDL_ALLOCATED_FIXED_SIZE 0x0008 MDL分配固定大小
MappedSystemVa这里因为还没有映射,所以0xb9dcc000是无用地址。
在这里插入图片描述
startVa因为是页对齐的,不是虚拟地址的直接起始地址,+ByteOffset才是真正的起始地址。
在这里插入图片描述
因为系统的非分页内存,本来就是锁定在内存的,所有可以直接使用mdl。
MmIsNonPagedSystemAddressValid顾名思义就是判断是否为分页内存
这里因为MmBuildMdlForNonPagedPool和MmProbeAndLockPages没有返回值,所以需要异常处理,防止出现错误。

__try
	{
		if (MmIsNonPagedSystemAddressValid(BufferData))//判断是否为分页内存
		{ 
			MmBuildMdlForNonPagedPool(Mdl);
		}
		else
		{
			MmProbeAndLockPages(Mdl, KernelMode, IoWriteAccess);
		}
}

调用MmBuildMdlForNonPagedPool函数来获得虚拟页面的物理内存。
由于非分页内存是系统内存,所以MDL的成员变量Process设置为NULL,同时成员变量***MappedSystemVa等于传递给IoAllocateMdl的虚拟地址,这个成员变量实际是将原来的虚拟内存地址映射到内核后的虚拟起始地址。***
在这里插入图片描述
在这里插入图片描述
我们可以通过MapedSystemVa看到
在这里插入图片描述
在这里插入图片描述
这里MdlFlags=On12 1000|=100,标记来源本身就是非分页内存。
在这里插入图片描述
MmBuildMdlForNonPagedPool的主要作用是将MDL描述的物理页面集合映射到系统地址空间(4G虚拟地址空间的高2G部分)
获得虚拟地址的大小和跨越页面的数量,然后循环填充Mdl后面的物理页帧。
在这里插入图片描述
接下来我们可以调用MmMapLockedPagesSpecifyCache来获得映射的用户空间的虚拟地址。

__try
		{
			VirtualAddress = MmMapLockedPagesSpecifyCache(Mdl,UserMode, MmCached, NULL, 					FALSE, NormalPagePriority);
		}

因为我们传递是UserMode,所以直接调用MiMapLockedPagesInuserSpace获得用户模式下的虚拟地址.
在这里插入图片描述
查看映射的用户空间下的虚拟内存,和内核模式下一致!
在这里插入图片描述
测试:
调用MmMapLockedPagesSpecifyCache函数生成用户模式下对应的虚拟地址,完成映射,然后就能修改这个地址的内容来达到修改内核内容的目的

memcpy(BufferData, L"abcdefg", 20);//修改用户模式下的虚拟地址的值达到修改内核模式下值的目的

  • 用户模式
  • 在这里插入图片描述
  • 内核模式
    在这里插入图片描述
    最后销毁临时映射,释放MDL。
Exit:
	if (Mdl != NULL)
	{
		IoFreeMdl(Mdl);
		Mdl = NULL;
	}

(2)我们申请一块分页内存

WCHAR* v1 = L"HelloWorld";
WCHAR* BufferData = ExAllocatePool(PagedPool, sizeof(WCHAR)*BufferLength);
memcpy(BufferData, v1, 20);

在这里插入图片描述

__try
	{
		if (MmIsNonPagedSystemAddressValid(BufferData))//判断是否为分页内存
		{ 
			MmBuildMdlForNonPagedPool(Mdl);
		}
		else
		{
			MmProbeAndLockPages(Mdl, KernelMode, IoWriteAccess);
		}
}

对于分页内存,虚拟内存和物理内存之间的联系是暂时的,可能其他的程序对它们进行重新分配,为了使其他的程序无法对他们进行修改和重新分配(在我们释放之前),我们就需要把这段内存锁定,防止其他程序修改。所以使用MmProbeAndLockPages。
MmProbeAndLockPages源码非常的大,也非常的重要,建议仔细阅读。
在这里插入图片描述
MdlFlags=On138表明#define MDL_WRITE_OPERATION 0x0080

我们使用MmProbeAndLockPages后,MappedSystemVa的内存是无用地址,并没有像MmBuildMdlForNonPagedPool一样得出映射内存,因为MmProbeAndLockPages只是锁住内存,还没有完成映射。但是已经完成了映射的准备工作,接下来就可以映射了。
在这里插入图片描述
最后调用MmMapLockedPagesSpecifyCache得到虚拟地址

VirtualAddress = MmMapLockedPagesSpecifyCache(Mdl,UserMode, MmCached, NULL, FALSE, NormalPagePriority);

在这里插入图片描述
最后销毁临时映射,释放MDL。

Exit:
	if (Mdl != NULL)
	{
		IoFreeMdl(Mdl);
		Mdl = NULL;
	}

二、用户态进程分配空间,内核态去映射

如果是用户态的地址你必须要自行弄清地址所在的进程上下文,因为不同的进程拥有不同的地址空间,即使地址的值一模一样它们包含的数据也一定完全不同。

  • Ring3:

首先我们在Ring3层申请一块内存

PVOID BufferData = malloc(0x1000);
BufferData = "Hello World";
cout << BufferData << endl;
  • Ring0:

所以在这里我们将进程attach,这样地址就是这个进程空间的地址(也可以通过irp通信获得地址)

PEPROCESS EProcess = 0x87B39870;
KeStackAttachProcess(EProcess, &KApcState);
PVOID BaseAddress = 0x00366B30;

可以看到此时的已经attach成功,地址中的内容是我们进程中申请的地址的内容。
在这里插入图片描述
申请Mdl,具体过程在上面已经描述过

Mdl = IoAllocateMdl(BaseAddress, 0x1000, FALSE, FALSE, NULL);

在这里插入图片描述
此时的MappedSystemVa的地址还没有映射,没有意义。
在这里插入图片描述
对于可分页的内存,虚拟内存和物理内存之间的联系是暂时的,所以MDL的页码数组只在特定的环境和时间段有效,因为很可能其他的程序对它们进行重新分配,为了使其他的程序无法对他们进行修改和重新分配(在我们释放之前),我们就需要把这段内存锁定,防止其他程序修改。
我们用MmProbeAndLockPages来实现,此时进程已经变成我们所需要的进程。

MmProbeAndLockPages(MDL,UserMode,IoReadAccess);

MdlFlags由8变成10,MDL_PAGES_LOCKED ,已经锁定内存。
在这里插入图片描述
此时已经锁定进程和物理页,但是我们可以看到这块地址并没有内容,因为MmProbeAndLockPages锁定的不过是物理页面,还没有完成映射。

MmMapLockedPagesSpecifyCache才是用来保证物理页面所映射的虚拟地址在当前有效。利用MmMapLockedPagesSpecifyCache获取重新映射后的虚拟地址。

MappedAddress = MmMapLockedPagesSpecifyCache(Mdl,KernelMode,MmCached,
NULL,FALSE,NormalPagePriority);

此时已经映射成功,MappedSystemVa就是对应内核态的虚拟地址
MdlFlags由10变为11,MDL_MAPPED_TO_SYSTEM_VA 映射成功。
在这里插入图片描述
测试:
读取到“hello world”,测试成功!
在这里插入图片描述
释放内存和解锁

MmUnlockPages(Mdl);
	if (Mdl != NULL)
	{
		IoFreeMdl(Mdl);
		Mdl = NULL;
	}
发布了19 篇原创文章 · 获赞 21 · 访问量 998

猜你喜欢

转载自blog.csdn.net/weixin_43742894/article/details/104870641