64位内核开发第十二讲,内核下的MDL操作

MDL 内存描述符列表

一丶MDL简介

1.1 MDL描述

虚拟地址会跨越一系列连续的虚拟地址,IO缓冲区也可以分布在多个物理页上,并且这些物理页是不连续的。操作系统就会使用 内存描述符表(MDL)这个结构来描述虚拟内存缓冲区的物理页面布局的。

MDL其实是一个MDL结构体,他后面跟着一个描述 I/O 缓冲区所在的物理内存的数据数组。 MDL的大小根据MDL描述的 I/O缓冲区的特征而变化的。 系统例程可以用于计算MDL所需要的大小,以及分配和释放MDL

要说一句的是 MDL是半透明的。我们的驱动程序不应该直接访问 MDL结构体中的成员。而是使用操作系统提供的宏来进行操作。

MDL 后面 还跟着一个 PFN_NUMBER (无符号整数)的数组,每个元素描述着缓冲区所覆盖的一个月面。 而整个MDL列表则是对于一个缓冲区页面的描述

1.2 MDL的结构与宏操作

先看下MDL 结构体是啥样

typedef struct _MDL {
  struct _MDL      *Next;   
  CSHORT           Size;    
  CSHORT           MdlFlags; 
  struct _EPROCESS *Process;
  PVOID            MappedSystemVa; 
  PVOID            StartVa;        
  ULONG            ByteCount;      
  ULONG            ByteOffset;      
} MDL, *PMDL;

参数详解:

参数 含义
Next 指向下一MDL的指针
Size 整个MDL的长度(包含后面的数组)
MdlFlags MDL的标记值
Process 所属进程的EPROCES
MappedSystemVa 该段缓冲区映射在系统空间的地址
StartVa 虚拟地址(对齐4KB)
ByteCount 该段缓冲区的长度
ByteOffset 虚拟地址偏移

其中要说的是 startVa + byteoffset =该段缓冲在Process进程空间中的虚拟地址

MDLflags 可以判断是否是锁定状态 是的话就进行释放。

其字段有如下:(未公开也是看资料得来的)

  1. MDL_MAPPED_TO_SYSTEM_VA MDL_SOURCE_IS_NONPAGED_POOL 如果设置了这两个位 那么 MappedSystemVa 才是有效的

  2. MDL_WRITE_OPERATION MDL_PAGES_LOCKED 此字段代表 MmProbeAndLockPages 函数设置的。代表锁定了页面。

下面代码演示的是 从IRP中释放MDL链

VOID MyFreeMdl(PMDL Mdl)
{
    PMDL currentMdl, nextMdl;

    for (currentMdl = Mdl; currentMdl != NULL; currentMdl = nextMdl) 
    {
        nextMdl = currentMdl->Next;
        if (currentMdl->MdlFlags & MDL_PAGES_LOCKED) 
        {
            MmUnlockPages(currentMdl);
        }
        IoFreeMdl(currentMdl);
    }
}

MDL 宏 是用来操作 MDL的 可以看下如下 宏

名称 作用
MmGetMdlVirtualAddress 返回 I/O 缓冲区的大小(以字节为单位)
MmGetMdlByteCount 返回 I/O 缓冲区的大小(以字节为单位)
MmGetMdlByteOffset 返回 I/O 缓冲区开头的物理页内的偏移量
MmGetMdlBaseVa 返回I/O缓冲区物理页的虚拟地址
MmGetSystemAddressForMdlSafe 获取缓冲映射在系统空间的地址
MmGetSystemAddressForMdl 同上,一个安全一个不安全。

下面看一下宏的本质实现

#define MmGetMdlVirtualAddress(Mdl)                                     \
    ((PVOID) ((PCHAR) ((Mdl)->StartVa) + (Mdl)->ByteOffset)

#define MmGetMdlByteCount(Mdl)  ((Mdl)->ByteCount)

#define MmGetMdlByteOffset(Mdl)  ((Mdl)->ByteOffset)

#define MmGetMdlBaseVa(Mdl)  ((Mdl)->StartVa)


#define MmGetSystemAddressForMdl(MDL)                                  \
     (((MDL)->MdlFlags & (MDL_MAPPED_TO_SYSTEM_VA |                    \
                        MDL_SOURCE_IS_NONPAGED_POOL)) ?                \
                             ((MDL)->MappedSystemVa) :                 \
                             (MmMapLockedPages((MDL),KernelMode)))


#define MmGetSystemAddressForMdlSafe(MDL, PRIORITY)                    \
     (((MDL)->MdlFlags & (MDL_MAPPED_TO_SYSTEM_VA |                    \
                        MDL_SOURCE_IS_NONPAGED_POOL)) ?                \
                             ((MDL)->MappedSystemVa) :                 \
                             (MmMapLockedPagesSpecifyCache((MDL),      \
                                                           KernelMode, \
                                                           MmCached,   \
                                                           NULL,       \
                                                           FALSE,      \
                                                           (PRIORITY))))

二丶MDL实战操作

2.1 MDL的申请和释放

MDL 申请 与释放是使用的如下API

PMDL IoAllocateMdl(
  [in, optional]      __drv_aliasesMem PVOID VirtualAddress,
  [in]                ULONG                  Length,
  [in]                BOOLEAN                SecondaryBuffer,
  [in]                BOOLEAN                ChargeQuota,
  [in, out, optional] PIRP                   Irp
);
void IoFreeMdl(
  [in] PMDL Mdl
);

其中还可以使用 MmCreateMdl(与IoAllocateMdl类似,只不过参数少了几个)

但是MmCreateMdl 本质是 申请一个非分页内存。然后初始化为MDL的。 IoAllocateMdl也可以使用。但是要注意你的内存是什么内存。

释放自然不用多说 主要说下申请的参数意思。

参数 含义
VirtualAddres 指向MDL要描述的缓冲区的虚拟地址的指针
Leng 表示参数1缓冲的长度
SecondaryBuf 是否是 主/副缓冲区,只是将 此MDL插入到IRP中(Irp->MdlAddress)还是替换此值 TRUE 插入 FALSE 替换。如果没有IRP与MDL关联 则必须为FALSE
ChargeQ 保留给系统使用,必须是FALSE
Irp 给定的IRP指针,根据参数3来确定是否将MDL插入或者替换到此IRP中。如果IRP为NULL则给FALSE即可。

简单使用:

 PVOID pBuffer = ExAllocatePoolWithTag(NonPagedPool, 0x1000, 'abcd');
    PMDL pMdl = IoAllocateMdl(pBuffer, 0x1000, FALSE, FALSE, NULL);
    PVOID pSystemVa = MmGetSystemAddressForMdlSafe(pMdl, NormalPagePriority);
    if (pSystemVa != NULL)
    {
        RtlFillBytes(pSystemVa, 0x1000, 0xAA);
    }
    if (pMdl)
    {
        IoFreeMdl(pMdl);
    }
    if (pBuffer)
    {
        ExFreePoolWithTag(pBuffer, 'abcd');
    }

2.2 描述用户虚拟地址 并且锁定内存

对于可分页内存PagedPool) 虚拟内存和物理内存的对应关系都是临时的,因此如果使用MDL结构的数据数组的时候是仅仅在特定情况下才有效。 我们需要调用如下两个API

MmProbeAndLockPages
MmUnlockPages 

来 将 可分页内存锁定 并且为当前MDL初始化此数组数组。 在我使用 MmUnlockPages 的时候是不会将此内存换出到磁盘的。也就是说不会分页此内存。

使用例子如下:

锁定用户虚拟地址让我们可以进行操作。

    PMDL pmdl = IoAllocateMdl(UserVa, userVaSize, FALSE, FALSE, NULL);
    ASSERT(pmdl);
    __try
    {
        MmProbeAndLockPages(pmdl, UserMode, IoReadAccess); // IoWriteAccess
    }
    __except (EXCEPTION_EXECUTE_HANDLER)
    {
        DbgPrint("error code = %d", GetExceptionCode);
    }
    PVOID kva = MmGetSystemAddressForMdlSafe(pmdl, NormalPagePriority);
    //opt kva 
    MmUnlockPages(pmdl);
    IoFreeMdl(pmdl);

必须使用 try except包含 上述代码就是锁定用户的虚拟地址,然后保证此内存不会换出。然后使用 MmGetSystemAddressForMdlSafe来获取映射在系统空间的位置(地址) 进行安全的操作。

MmProbeAndLockPages 重点要关注第二个参数还有第三个参数, 第二个参数代表你锁定的地址 用户态的还是内核态的(UserMode/KernelMod) 第三个参数代表你描述的这块内存是 可读取还是可写入。

重点:

  在释放内存的时候请先解锁此内存。然后在进行释放。否则你试试

在上面我们的 有段代码叫做

PVOID kva = MmGetSystemAddressForMdlSafe(pmdl,NormalPagePriority);

它也可以换成更强大的函数,他能突破微软的 CopyOnWrite 机制,也就是内存可读写。MmMapLockedPagesSpecifyCache()

函数如下:

低版本是:MmMapLockedPages() 与之配套的是 MmUnmapLockedPages()

PVOID MmMapLockedPagesSpecifyCache(
  [in]     PMDL MemoryDescriptorList,
  [in]     KPROCESSOR_MODE AccessMode,
  [in]     MEMORY_CACHING_TYPE CacheType,
  [in, optional] PVOID RequestedAddress,
  [in]    ULONG BugCheckOnFailure,
  [in]    ULONG Priority
);

此函数参数如下:

参数 含义
MemoryDescriptorList 必须是一个MDL 而且此MDL鄙俗描述了锁定的物理页
AccessMode MDL的访问模式,内核还是用户。针对内核都要使用kernelMode 用户都要使用UserMode
CacheType 指示了MDL的缓存属性,参考MEMORY_CACHING_TYPE此结构
RequestedAddress 如果AccessMode是用户模式,那么此参数则指定将MDL映射到起始用户虚拟地址,如果为NULL,则系统选择起始地址。系统它会四舍五入来寻找合适地址来适应边界要求。所以我们要检查返回值。
BugCheckOnFailure 系统资源不足无法映射MDL(accmode=kernelMode)的时候(TRUE),那么就会发生错误检查,如果为FALSE 那么此函数返回NULL。驱动程序中必须将此参数设置为FALSE.
Priority 一个 MM_PAGEPRIORITY值。 指示了PTE稀缺是成功的重要性。从WIN8开始,指定的优先级可以使用 MdlMappingNoWrite 或者 MdlMappingNoExecute 优先级的详细信息查看 MmGetSystemAddressForMdlSafe 函数介绍。

所以上边的代码 在锁定内存后可以进行如下操作

_try
{
pAddr = MmMapLockedPagesSpecifyCache(pMDL,KernelMode,MmCached,NULL,FALSE,NormalPagePriority);
if (pAddr  == NULL)
{
return 0;
}

}
_except(EXCEPTION_EXECUTE_HANDLER)
{

}

2.3 实现对用户内存锁定,进行MDL方式内存读写.

其实主要用的的API 为如下

MmCreateMdl() 用来指定用户的内存(也就是用户的虚拟地址) 根据这个虚拟地址创建一大块MDL
MmBuildMdlForNonPagedPool() 主要作用就是对我们生成的Mdl进行一个更新. 更新对物理内存的描述
MmMapLockedPages() 锁定内存
锁定之后则可以读写进程内存了
//资源释放
IoFreeMdl()
MmUnmapLockedPaged(); 取消锁定

实现函数如下:

BOOLEAN WriteMemoryOfMdl(PVOID user_va, PVOID write_data, SIZE_T write_data_size)
{
    PMDL mdl_addr = NULL;
    PVOID kernel_lockedaddr = NULL;
    // 创建一个Mdl 大小是根据用户的VA(虚拟地址)进行创建的.
    mdl_addr = MmCreateMdl(NULL, user_va, write_data_size);
    if (NULL == mdl_addr)
    {
        return FALSE;
    }
    // 更新MDL对物理内存的描述
    MmBuildMdlForNonPagedPool(mdl_addr);
    // 内存进行锁定映射.映射到出一个内核地址
    kernel_lockedaddr = MmMapLockedPages(mdl_addr, KernelMode);
    if (NULL == kernel_lockedaddr)
    {
        IoFreeMdl(mdl_addr);
    }
    //进行数据操作 可以读写这块数据
    RtlCopyMemory(kernel_lockedaddr, write_data, write_data_size);
    //先解除锁定,然后在释放.
    MmUnmapLockedPages(kernel_lockedaddr, mdl_addr);
    IoFreeMdl(mdl_addr);
    return TRUE;
}

使用高版本的 IoAllocateMdl也可以实现,代码如下:

BOOLEAN CRemoteThread::WriteProcessMemoryByUserVa(
    PVOID user_vaaddr,
    PVOID write_data,
    SIZE_T data_size)
{
    PMDL mdl_new_user_va = NULL;
    PVOID lock_va = NULL;
    if (user_vaaddr == NULL)
        return FALSE;
    if (write_data == NULL || data_size <= 0)
        return FALSE;

    mdl_new_user_va = IoAllocateMdl(user_vaaddr, (ULONG)data_size, FALSE, FALSE, NULL);
    if (mdl_new_user_va == NULL)
        return FALSE;
    __try
    {

        MmProbeAndLockPages(mdl_new_user_va, KernelMode, IoReadAccess);
        lock_va = MmMapLockedPagesSpecifyCache(mdl_new_user_va, KernelMode, MmCached, NULL, FALSE, NormalPagePriority);
        if (lock_va)
        {
            RtlCopyMemory(lock_va, write_data, data_size);
        }

        if (lock_va != NULL)
        {
            MmUnmapLockedPages(lock_va, mdl_new_user_va);
            lock_va = NULL;
        }
        if (mdl_new_user_va != NULL)
        {
            IoFreeMdl(mdl_new_user_va);
            mdl_new_user_va = NULL;
        }
        return TRUE;
    }
    __except (EXCEPTION_EXECUTE_HANDLER)
    {
        if (lock_va != NULL)
        {
            MmUnmapLockedPages(lock_va, mdl_new_user_va);
            lock_va = NULL;
        }
        if (mdl_new_user_va != NULL)
        {
            IoFreeMdl(mdl_new_user_va);
            mdl_new_user_va = NULL;
        }
    }

    return FALSE;
}

2.4 描述内核内存 与用户态内存共享。

内核内存 与用户共享也很简单。

总共也进行以下几个步骤。

  1. 申请NonpagePool内存

  2. 申请 Mdl描述,来描述申请的 NonpagePool

  3. 锁定到系统中

  4. 使用 MmMapLockedPagesSpecifyCache 映射为 userMode 模式的地址。

伪代码:

 PVOID pKernelAddr = ExAllocatePoolWithTag(NonPagedPool, 0x1000, 'ABCD');
    PMDL pMdl = IoAllocateMdl(pKernelAddr, 0x1024, FALSE, FALSE, NULL);
    __try
    {
        MmBuildMdlForNonPagedPool(pMdl);
    }
    __except (EXCEPTION_EXECUTE_HANDLER)
    {
    }

    PVOID pUserAddress = MmMapLockedPagesSpecifyCache(pMdl, UserMode, MmCached, NULL, FALSE, NormalPagePriority);
    KdPrint(("UserAddr = %d  KernelAddr = %d \r\n", pKernelAddr, pUserAddress));

    if (pUserAddress)
        MmUnmapLockedPages(pUserAddress, pMdl); //与之配套MmMapLockedPagesSpecifyCache
    if (pMdl)
        IoFreeMdl(pMdl);
    if (pKernelAddr)
        ExFreePoolWithTag(pKernelAddr, 'abcd');

2.4 其它 MDL会用到的函数

函数名称 作用
MmInitializeMdl 初始化MDL结构体但是不会初始化紧跟 MDL 结构的数据数组。也就是说你可以申请一块非分页内存将其格式化为 MDL结构。然后继续进行MDL操作。
MmPrepareMdlForReuse 释放MDL关联的资源,然后重复使用此MDL。配套IoBuildPartialMdl 使用。 可以将IoBuildPartialMdl 参数中的TargetMdl 释放,以便可以重复使用。
IoBuildPartialMdl 使用已经存在的MDL来生成一个新的MDL。用来描述源MDL中描述的缓冲去的子范围。可以理解为将一个大的MDL做了分割。可以将大型传输请求分割为小的传输请求。具体查询下MSDN把。

三丶物理内存与其映射

3.1 物理内存映射到系统中

主要是如下函数:

MmGetPhysicalAddress

MmMapIoSpace

PHYSICAL_ADDRESS MmGetPhysicalAddress(
  [in] PVOID BaseAddress
);

此函数返回有效的非分页虚拟地址所对应的物理地址

也就是给一个 NonPagePool类型的虚拟地址 它会返回对应的物理地址(对应在内存条的地址)

PVOID MmMapIoSpace(
  [in] PHYSICAL_ADDRESS    PhysicalAddress,  物理地址  
  [in] SIZE_T              NumberOfBytes,    >0 的映射字节
  [in] MEMORY_CACHING_TYPE CacheType         > 缓存属性
);

此函数 将一个 物理地址 映射到 非分页的系统空间内

如果失败则返回NULL

void MmUnmapIoSpace(
  [in] PVOID  BaseAddress,
  [in] SIZE_T NumberOfBytes
);

映射之后要取消映射。

函数的使用要使用 ceddk.lib 如果后面实现更改了请查询MSDN文档。

3.2 其它代码

函数 用法
MmAllocateContiguousMemorySpecifyCache 分配连续的非分页物理内存 然后映射到系统空间中(也可将这块空间映射为用户虚拟地址)
MmFreeContiguousMemory 与上面配套,释放内存。

举例:

  //获取虚拟地址的物理内存 然后将此物理内存映射到系统中 是一个系统的虚拟地址 可以直接操作
    PVOID pTestMdl = ExAllocatePoolWithTag(PagedPool, 0x1000, 'ABCD');
    PHYSICAL_ADDRESS phyAddr = MmGetPhysicalAddress(pTestMdl);
    PVOID pMap = MmMapIoSpace(phyAddr, 0x1000, MmCached);
    RtlFillMemory(pMap, 0x1000, 0xaa);
    MmUnmapIoSpace(pMap, 0x1000);

    //集成了上面几步 直接申请连续的非分页的物理内存 然后映射到地址空间中
    //映射的空间属于 NonPagePool
    PHYSICAL_ADDRESS lowValue = {0x0000000000800000};
    PHYSICAL_ADDRESS highValue = {0x0000000000FFFFFF};
    PHYSICAL_ADDRESS N = {0};
    PVOID pLockAddr = MmAllocateContiguousMemorySpecifyCache(0x1000, lowValue, highValue, N, MmCached);
    RtlFillMemory(pLockAddr, 0x1000, 0XBB);
    if (pLockAddr)
        MmFreeContiguousMemory(pLockAddr);
posted @ 2020-11-03 11:02  iBinary  阅读(2184)  评论(0编辑  收藏  举报