免责声明:内容仅供学习参考,请合法利用知识,禁止进行违法犯罪活动!
本次游戏没法给
内容参考于:微尘网络安全
上一个内容:27.驱动自动编译和自动签名和自动vmp-Windows驱动
MDL是内存描述符列表,就是说我们在CE和x64dbg中看到的地址是虚拟地址,虚拟地址会对应一块物理内存地址(物理页),一个物理页可以对应多个虚拟地址,这个MDL的作用就是让一个物理页对应多个虚拟地址,如果对新的虚拟地址读或写,改变了物理页,之前的虚拟地址也会被改变
当要对一块内核内存进行修改的时候,需要先为这块内存创建MDL,那么就会建立一块新的虚拟内存空间,与将要修改内存对应的物理控件向映射,我们通过对虚拟地址对应物理内存进行操作,这是MDL修改内存的实现思路
流程
1.调用IoAllocateMdl函数分配一个足够大的MDL结构
2.调用MmBuildMdlForNonPagedPool函数更新MDL对物理内存的描述
3.调用MmMapLockedPages函数将MDL中描述的物理页映射到虚拟内存,并返回映射的虚拟内存地址,通过返回的虚拟内存地址来操作物理页
效果图:

三环代码
0环驱动代码
![]()
![]()
代码说明:
c++// 【ReadProcessByMDL函数】- 核心!通过MDL(内存描述符列表)安全读取目标进程内存 // 入参:Irp - 设备控制请求的IRP结构体(用户层与内核层通信的核心载体) // 作用:接收用户层传入的「目标进程PID、读取地址、读取长度」,通过MDL映射内存+进程附加,实现无权限检查的内存读取 // 核心优势:MDL是内核层安全操作跨进程内存的方式,绕过用户层ReadProcessMemory的权限校验 void ReadProcessByMDL(IRP* Irp) { //__debugbreak(); // 调试断点(调试时启用,发布时注释) // 步骤1:获取用户层传入的参数缓冲区(SystemBuffer是IRP中存储用户/内核交互数据的区域) // buf是64位数组,约定: // buf[0] = 目标进程PID(UINT64) // buf[1] = 目标进程中要读取的内存地址(UINT64,用户层虚拟地址) // buf[2] = 要读取的内存长度(UINT64,字节数) UINT64* buf = Irp->AssociatedIrp.SystemBuffer; // 调试打印:输出用户层传入的参数(方便WinDbg验证参数是否正确) DbgPrintEx(DPFLTR_IHVDRIVER_ID, 0, "IRP_MJ_DEVICE_CONTROL buf[0] = %llx\n", buf[0]); // 目标PID DbgPrintEx(DPFLTR_IHVDRIVER_ID, 0, "IRP_MJ_DEVICE_CONTROL buf[1] = %llx\n", buf[1]); // 目标地址 DbgPrintEx(DPFLTR_IHVDRIVER_ID, 0, "IRP_MJ_DEVICE_CONTROL buf[2] = %llx\n", buf[2]); // 读取长度 // 步骤2:定义目标进程的EPROCESS指针(内核中描述进程的核心对象) PEPROCESS pep = NULL; // 根据PID查找目标进程的EPROCESS(核心!后续附加进程必须用EPROCESS) // 注意:此处未检查返回值,失败时pep为NULL,会导致后续蓝屏 PsLookupProcessByProcessId(buf[0], &pep); // 步骤3:分配MDL(内存描述符列表)- 内核操作跨进程内存的核心结构体 // IoAllocateMdl参数说明: // 参数1:起始地址 → buf(用户层传入的缓冲区,用于存储读取结果) // 参数2:长度 → buf[2](要读取的内存长度) // 参数3:是否是分段缓冲区 → FALSE(连续缓冲区) // 参数4:是否是高层级MDL → FALSE(基础MDL) // 参数5:关联的IRP → NULL(无关联) PMDL g_mdl = IoAllocateMdl(buf, buf[2], FALSE, FALSE, NULL); // 步骤4:如果MDL分配成功,执行内存读取逻辑 if (g_mdl) { // 构建MDL:将MDL与非分页池内存关联(内核规范,必须调用) // 作用:让MDL描述的内存区域被内核锁定,防止分页交换 MmBuildMdlForNonPagedPool(g_mdl); // 将MDL锁定的内存映射到内核地址空间(返回映射后的内核虚拟地址) // 参数2:KernelMode(内核模式,映射到内核地址空间,用户层不可见) PVOID address = MmMapLockedPages(g_mdl, KernelMode); // 步骤5:附加到目标进程的地址空间(核心!跨进程读取内存的关键) // KAPC_STATE:存储进程上下文的结构体(附加/分离进程时保存当前上下文) KAPC_STATE apc; // KeStackAttachProcess:将当前内核线程附加到目标进程的地址空间 // 效果:当前线程"假装"在目标进程中运行,能直接访问目标进程的虚拟地址 KeStackAttachProcess(pep, &apc); // 步骤6:拷贝目标进程的内存到MDL映射的内核地址 // RtlCopyMemory(目标地址, 源地址, 长度) // 此处:把目标进程buf[1]地址的内存,拷贝length字节到address(MDL映射地址) RtlCopyMemory(address, buf[1], buf[2]); // 步骤7:从目标进程地址空间分离(必须!否则内核线程一直挂在目标进程,导致进程退出时蓝屏) KeUnstackDetachProcess(&apc); // 步骤8:将MDL中的数据拷贝回用户层缓冲区(SystemBuffer) // 最终:用户层能从SystemBuffer拿到目标进程的内存数据 RtlCopyMemory(Irp->AssociatedIrp.SystemBuffer, address, buf[2]); // 步骤9:释放MDL相关资源(内核规范,必须释放,否则内存泄漏) MmUnmapLockedPages(address, g_mdl); // 解除MDL内存映射 IoFreeMdl(g_mdl); // 释放MDL结构体 // 步骤10:设置IRP的完成状态(告诉用户层操作成功) Irp->IoStatus.Status = STATUS_SUCCESS; // 操作成功状态 Irp->IoStatus.Information = buf[2]; // 返回给用户层的字节数(读取的长度) } // 步骤11:释放EPROCESS引用(内核规范,PsLookupProcessByProcessId会增加引用计数,必须释放) // 注意:此处未检查pep是否为NULL,pep=NULL时调用会蓝屏 ObDereferenceObject(pep); }


