在前几篇文章中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
MDL * mdl = IoAllocateMdl(uva, uva_size, FALSE, FALSE, NULL);
ASSERT(mdl);
__try {
MmProbeAndLockPages(mdl, UserMode, IoReadAccess);
} __except(EXCEPTION_EXECUTE_HANDLER) {
DbgPrint("error code = %d", GetExceptionCode);
}
PVOID kva = MmGetSystemAddressForMdlSafe(mdl, NormalPagePriority);
// use kva
// …
MmUnlockPages(mdl);
IoFreeMdl(mdl);
内核态分配空间,用户态进程去映射。
PVOID kva = ExAllocatePoolWithTag(NonPagedPool, 1024, (ULONG)'PMET');
MDL * mdl = IoAllocateMdl(uva, uva_size, FALSE, FALSE, NULL);
ASSERT(mdl);
__try {
MmBuildMdlForNonPagedPool(mdl);
} __except(EXCEPTION_EXECUTE_HANDLER) {
DbgPrint("error code = %d", GetExceptionCode);
}
PVOID uva = MmMapLockedPagesSpecifyCache(mdl, UserMode, MmCached, NULL, FALSE, NormalPagePriority);
如上的代码看看就好摘出来只是要提醒大家这个是无法使用的,如下将进入本篇文章的正题。
以内核中开辟空间为例,首先在代码中要做的就是定义一段非分页内存#define FILE_DEVICE_EXTENSION 4096
这段区域用于给全局变量使用,其次我们需要传输结构体那么结构体中的成员就要事先定义好,例如此处使用StructAll
来定义结构结构体成员变量如下所示,通过使用static
将结构体定义为静态,预先空出1024
的内存空间并初始化为0,当然了这种方式是存在弊端的,例如最大只支持1024个结构如果超过了则可能会溢出,当然最好的办法是用户空间开辟,在下次章节中再介绍。
// -------------------------------------------------
// MDL数据传递变量
// -------------------------------------------------
// 保存一段非分页内存,用于给全局变量使用
#define FILE_DEVICE_EXTENSION 4096
// 定义重复结构(循环传递)
typedef struct
{
char username[256];
char password[256];
int count;
}StructAll;
static StructAll ptr[1024] = { 0 };
为了能够达到输出结构体的效果这里我定义一个ShowProcess
用于模拟当前系统内进程数,并自动填充为特定的数据,此处结构体内部count
成员则用于标注当前共有多少个结构体,用于在用户层读取判断,当然了这种方式的另一个弊端就是浪费空间,因为每一个结构体中都存在一个被填充为0的整数类型。但如果只是实现功能的话其实也不是那么重要。
// 模拟进程列表赋值测试
int ShowProcess(int process_count)
{
memset(ptr, 0, sizeof(StructAll) * process_count);
int x = 0;
for (; x < process_count + 1; x++)
{
strcpy_s(ptr[x].username, 256, "lyshark");
strcpy_s(ptr[x].password, 256, "123456");
}
// 设置总共有多少个结构体,并返回结构体个数
ptr[0].count = x;
return x;
}
内核态映射: 当定义好如上这些方法时,接下来就是最重要的驱动映射部分了,如下代码所示,首先当用户调用派遣时第一个执行的函数是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() 将内核态指针映射