驱动开发:通过MDL映射实现多次通信
在前几篇文章中LyShark
通过多种方式实现了驱动程序与应用层之间的通信,这其中就包括了通过运用SystemBuf
缓冲区通信,运用ReadFile
读写通信,运用PIPE
管道通信,以及运用ASYNC
反向通信,这些通信方式在应对一收一发
模式的时候效率极高,但往往我们需要实现一次性吐出多种数据,例如ARK工具中当我们枚举内核模块时,往往应用层例程中可以返回几条甚至是几十条结果,如下案例所示,这对于开发一款ARK反内核工具是必须要有的功能。
- 那么如何实现如上述功能呢?
其实,实现这类功能可以从两个方面入手,但不论使用哪一种方式本质上都是预留一段缓冲区以此来给内核与应用层共享的区域,该区域内可用于交换数据,实现方式有两种要么在应用层分配空间,要么在内核中分配,LyShark先带大家在内核层
实现,通过巧妙地运用MDL映射
机制来实现通信需求。
- MDL是什么呢?
MDL内存读写是最常用的一种读写模式,是用于描述物理地址页面的一个结构,简单的官方解释;内存描述符列表 (MDL) 是一个系统定义的结构,通过一系列物理地址描述缓冲区。执行直接I/O的驱动程序从I/O管理器接收一个MDL的指针,并通过MDL读写数据。一些驱动程序在执行直接I/O来满足设备I/O控制请求时也使用MDL。
通过运用MDL的方式对同一块物理内存同时映射到R0和R3,这样我们只需要使用DeviceIoControl
向驱动发送一个指针,通过对指针进行读写就可以实现数据的交换,本人在网络上找到了如下两段被转载的烂大街的片段,这两段代码明显是存在缺陷的如果你也在寻找映射方法那么不要被这两段代码坑了,多数人也根本没有能力将其变为可用的,也就只能转载,不知道哪个大哥挖的坑。
用户态进程分配空间,内核态去映射。
// assume uva is a virtual address in user space, uva_size is its size |
内核态分配空间,用户态进程去映射。
PVOID kva = ExAllocatePoolWithTag(NonPagedPool, 1024, (ULONG)'PMET'); |
如上的代码看看就好摘出来只是要提醒大家这个是无法使用的,如下将进入本篇文章的正题。
以内核中开辟空间为例,首先在代码中要做的就是定义一段非分页内存#define FILE_DEVICE_EXTENSION 4096
这段区域用于给全局变量使用,其次我们需要传输结构体那么结构体中的成员就要事先定义好,例如此处使用StructAll
来定义结构结构体成员变量如下所示,通过使用static
将结构体定义为静态,预先空出1024
的内存空间并初始化为0,当然了这种方式是存在弊端的,例如最大只支持1024个结构如果超过了则可能会溢出,当然最好的办法是用户空间开辟,在下次章节中再介绍。
// ------------------------------------------------- |
为了能够达到输出结构体的效果这里我定义一个ShowProcess
用于模拟当前系统内进程数,并自动填充为特定的数据,此处结构体内部count
成员则用于标注当前共有多少个结构体,用于在用户层读取判断,当然了这种方式的另一个弊端就是浪费空间,因为每一个结构体中都存在一个被填充为0的整数类型。但如果只是实现功能的话其实也不是那么重要。
// 模拟进程列表赋值测试 |
内核态映射数据
当定义好如上这些方法时,接下来就是最重要的驱动映射部分了,如下代码所示,首先当用户调用派遣时第一个执行的函数是ShowProcess()
它用于获取到当前系统中有多少个进程,接着通过sizeof(MyData) * count
计算出当前MyData
需要分配的内存池大小并返回给pool_size
,调用ExAllocatePool
分配一块非分页内核空间,创建IoAllocateMdl
MDL映射,将数据MmMapLockedPagesSpecifyCache
映射到用户空间,最后将指针pShareMM_User
返回给用户态。
- ShowProcess(715) 获取当前进程数,并返回数量
- sizeof(MyData) * count 计算得到结构体长度
- ExAllocatePool(NonPagedPool, pool_size) 分配非分页内存,长度是pool_size
- IoAllocateMdl() 分配MDL空间,并放入内核态
- MmMapLockedPagesSpecifyCache() 将内核态指针映射到用户态
- RtlCopyMemory(pShareMM_SYS, &ptr, sizeof(ptr[0]) * count) 将总进程数放入到count计数变量内
- *(PVOID *)pIrp->AssociatedIrp.SystemBuffer = pShareMM_User 直接将指针传递给用户态
// 获取到当前列表数据 |
用户态读取数据
与内核层一致,用户层同样需要定义StructAll
结构体用于接收内核中返回过来的结构,而重要的代码则是接收部分,通过IoControl
发送控制码,并得到ptr
内存指针,此处区域就是内核态分配过的指针,用户只需要通过循环的方式依次读出里面的数据即可。
// ------------------------------------------------- |
如上就是内核层与应用层的部分代码功能分析,接下来我将完整代码分享出来,大家可以自行测试效果。
驱动程序WinDDK.sys
完整代码;
|
应用层客户端程序lyshark.exe
完整代码;
|
手动编译这两个程序,将驱动签名后以管理员身份运行lyshark.exe
客户端,此时屏幕中即可看到滚动输出效果,如此一来就实现了循环传递参数的目的。