免责声明:内容仅供学习参考,请合法利用知识,禁止进行违法犯罪活动!
本次游戏没法给
内容参考于:微尘网络安全
上一个内容:16.驱动的加载和卸载-Windows驱动
效果图:首先通过CE附加txt文本文档,然后搜索一个8字节的数据(为什么是8字节,因为现在的驱动只实现了读取8字节),然后在我们的MFC程序里输入进程id和要读取的内存地址,然后得到内存地址里的值,然后就实现了使用内核(驱动)读取进程里的数据的功能,这样就可以过保护了
有一个网站可以查看蓝屏原因:
https://learn.microsoft.com/zh-cn/windows-hardware/drivers/debugger/bug-check-code-reference2
如下图红框位置,当操作系统蓝屏会在Windbg中出现,先看0x44
拿着0x44去下图红框位置找
如下图0x44的问题说明,这个问题一般是重复执行 IoCompleteRequest(Irp,IO_NO_INCREMENT); 这个代码导致的
实现原理:
正常的方式是通过注入进去或通过跨进程读取内存(跨进程读写)的方式,然后读取游戏的内存,现在我们可以使用内核(驱动)的方式来读写,驱动的方式会比较安全
首先内核层的内存是所有进程共享的,所以在内核层申请一个内存,所有进程就都可以访问这个内存,所以我们可以通过DeviceIoControl进入内核,然后在内核中申请一块内存,然后使用驱动附加到游戏的进程,然后把游戏的内存复制到我们在内核中申请的内存空间里,然后再通过DeviceIoControl返回给我们,这样就能读到游戏里的值了
涉及的内核api
1.PsLookupProcessByProcessId :通过进程id得到进程的结构(EProcess,通过这个结构可以得到进程中所有的数据,这里得到的是PEProcess结构,也就是EProcess的地址,也就是指针)
2.KeStackAttachProcess:通过PsLookupProcessByProcessId 得到进程结构(EProcess),然后通过KeStackAttachProcess把我们驱动附加到进程(游戏)里
3.ExAllocatePool:在内核中申请一块内存,这个内存是堆内存
4.KeUnstackDetachProcess:KeStackAttachProcess是附加,KeUnstackDetachProcess是退出附加
驱动添加的代码
代码说明:注意在内核中申请了内存或创建得到了某结构,必须用完就释放,否侧会蓝屏
// 函数功能:读取指定进程的指定内存地址的8字节数据(通过IRP返回给用户态) // 参数:Irp - 内核请求包裹,包含用户态传入的参数和返回数据的缓冲区 void ReadProcessBAtt(IRP* Irp) { // 触发调试断点(仅调试用!发布版必须删除,否则会导致系统断点崩溃) // 作用:WinDbg调试时会暂停在这里,方便查看变量/内存 // 如果不挂载Windbg必须删除__debugbreak();这个代码否则会蓝屏 __debugbreak(); // 1. 解析用户态传入的参数(SystemBuffer存储用户态发来的3个64位值) // buf[0] = 目标进程PID(64位)、buf[1] = 目标进程的内存地址(64位)、buf[2] = 备用(暂未用) UINT64* buf = (UINT64*)Irp->AssociatedIrp.SystemBuffer; // 调试日志:打印用户态传入的参数(16进制,方便查看地址/PID) DbgPrintEx(DPFLTR_IHVDRIVER_ID, 0, "IRP_MJ_DEVICE_CONTROL buf[0] = %llx\n", buf[0]); // PID DbgPrintEx(DPFLTR_IHVDRIVER_ID, 0, "IRP_MJ_DEVICE_CONTROL buf[1] = %llx\n", buf[1]); // 目标内存地址 DbgPrintEx(DPFLTR_IHVDRIVER_ID, 0, "IRP_MJ_DEVICE_CONTROL buf[2] = %llx\n", buf[2]); // 备用参数 // 2. 定义EPROCESS指针(EPROCESS是内核中描述进程的核心结构体,相当于进程的"身份证") PEPROCESS pep = NULL; // 3. 通过PID查找目标进程的EPROCESS结构体(必须检查返回值,防止PID无效导致空指针) // PsLookupProcessByProcessId:内核核心函数,将用户态PID转换为内核EPROCESS对象 NTSTATUS status = PsLookupProcessByProcessId((HANDLE)buf[0], &pep); if (!NT_SUCCESS(status)) { Irp->IoStatus.Status = status; // 把失败原因返回给用户态 Irp->IoStatus.Information = 0; IoCompleteRequest(Irp, IO_NO_INCREMENT); return; } // 4. 分配8字节非分页池内存(存储读取到的目标进程数据) /***************************************************************************** * 非分页池(NonPagedPool/NPagedPool)核心说明: * 1. 定义:Windows内核内存分为「分页池」和「非分页池」两类: * - 分页池(PagedPool):内存可被系统换出到硬盘的页面文件(pagefile.sys),节省物理内存; * - 非分页池(NonPagedPool):内存始终驻留在物理内存中,不会被换出,系统任何时候都能访问。 * 2. 适用场景: * - 内核中断请求级别(IRQL)≥ DISPATCH_LEVEL时(如中断处理、DPC回调),只能用非分页池; * - 操作其他进程地址空间(如KeStackAttachProcess附加进程)、线程调度等核心场景; * - 需要保证内存「随时可访问」的场景(分页池换出后访问会导致蓝屏)。 * 3. 安全注意: * - Win10/Win11及以上系统,推荐使用 NonPagedPoolNx(Non-Executable),即「不可执行非分页池」; * - 旧版 NonPagedPool 允许内存执行代码,存在恶意代码注入风险,仅兼容旧系统时使用。 * 4. 内存管理: * - 非分页池是系统稀缺资源(总量远小于分页池),分配后必须用ExFreePool释放; * - 未释放会导致「内核内存泄漏」,长期运行会耗尽非分页池,引发系统卡顿、蓝屏。 *****************************************************************************/ PVOID p = ExAllocatePool(NonPagedPoolNx, 8); // 优先使用安全的不可执行非分页池 // 5. 内存分配失败的容错处理(非分页池分配可能失败,必须检查) if (!p) { Irp->IoStatus.Status = STATUS_INSUFFICIENT_RESOURCES; // 内存不足错误码 Irp->IoStatus.Information = 0; IoCompleteRequest(Irp, IO_NO_INCREMENT); ObDereferenceObject(pep); // 释放EPROCESS引用,避免内存泄漏 return; } // 6. 附加到目标进程地址空间(访问其他进程内存的核心步骤) KAPC_STATE apc; // 保存当前进程上下文,用于解除附加后恢复 KeStackAttachProcess(pep, &apc); // 7. 检查目标地址合法性(防止无效地址导致内存访问错误蓝屏) if (!MmIsAddressValid((PVOID)buf[1])) { Irp->IoStatus.Status = STATUS_ACCESS_VIOLATION; // 地址无效错误码 IoCompleteRequest(Irp, IO_NO_INCREMENT); KeUnstackDetachProcess(&apc); // 先解除附加 ExFreePool(p); // 释放非分页池内存 ObDereferenceObject(pep); // 释放EPROCESS引用 return; } // 8. 读取目标进程内存:从buf[1]地址拷贝8字节到非分页池内存p中 // 此时已附加到目标进程,buf[1]是目标进程的有效虚拟地址,可安全读取 RtlCopyMemory(p, (PVOID)buf[1], 8); // 9. 解除进程附加(必须!否则内核线程会一直挂靠在目标进程,导致系统异常) KeUnstackDetachProcess(&apc); // 10. 把读取到的8字节数据拷贝回SystemBuffer(系统自动同步到用户态OutBuf) RtlCopyMemory(Irp->AssociatedIrp.SystemBuffer, p, 8); // 11. 释放非分页池内存(核心!避免非分页池泄漏) ExFreePool(p); // 12. 设置请求处理结果:成功 + 返回8字节数据 Irp->IoStatus.Status = STATUS_SUCCESS; Irp->IoStatus.Information = 8; // 告知用户态实际返回8字节 IoCompleteRequest(Irp, IO_NO_INCREMENT); // 13. 释放EPROCESS对象引用(内核对象有引用计数,不释放会内存泄漏) ObDereferenceObject(pep); }用户层用到的控件
过保护读内存按钮的代码
代码说明:
void CMFCApplication1Dlg::OnBnClickedButton8() { // 1. 同步界面控件数据到关联的成员变量(关键!) // UpdateData(TRUE/1):把界面上输入框的内容读取到对应的CString变量(如pID、Address) // 如果不调用,pID/Address还是旧值,读取的是错误的PID/地址 UpdateData(1); // 2. 定义临时变量存储转换后的PID和内存地址(64位,适配驱动的UINT64) // LONGLONG = int64_t,和UINT64(uint64_t)都是8字节,可兼容转换 LONGLONG TmpPID = 0, TmpAddress = 0; // 3. 把界面输入的字符串(Unicode)转换为64位整数(支持十六进制) // 参数1:界面输入的字符串(pID是关联PID输入框的CStringW变量) // 参数2:STIF_SUPPORT_HEX = 支持十六进制(比如输入0x1234也能正确转换) // 参数3:输出转换后的64位数值 StrToInt64ExW(pID.GetString(), STIF_SUPPORT_HEX, &TmpPID); // PID字符串转64位整数 StrToInt64ExW(Address.GetString(), STIF_SUPPORT_HEX, &TmpAddress); // 内存地址字符串转64位整数 // 4. 组装传给驱动的输入缓冲区(和驱动约定的格式) // MyBuf[0] = 目标进程PID(64位) // MyBuf[1] = 目标进程的内存地址(64位) // MyBuf[2] = 要读取的内存大小(8字节,驱动暂未用到,但预留参数) UINT64 MyBuf[3] = { (UINT64)TmpPID, // 转换为无符号64位(驱动侧用UINT64接收) (UINT64)TmpAddress, // 目标内存地址(64位虚拟地址) 8 // 计划读取的字节数(驱动固定读8字节,此参数仅预留) }; // 5. 定义变量接收驱动返回的「实际传输字节数」 DWORD len = 0; // 6. 定义输出缓冲区:接收驱动返回的8字节内存数据(初始化为NULL) UINT64 OutBuf = NULL; // 7. 调用DeviceIoControl和驱动通信(核心!) // 参数说明: // nDeviveHandle:驱动设备句柄(需提前用CreateFile打开,必须有效) // 过保护读内存:自定义控制码(需和驱动侧的控制码完全一致) // &MyBuf:输入缓冲区地址(传给驱动的PID+地址+长度) // sizeof(MyBuf):输入缓冲区大小(3*8=24字节) // &OutBuf:输出缓冲区地址(接收驱动读取的8字节数据) // sizeof(OutBuf):输出缓冲区大小(8字节) // &len:输出参数,驱动会填充「实际返回的字节数」(应该是8) // NULL:同步调用(等待驱动处理完成再返回) BOOL ret = DeviceIoControl(nDeviveHandle, 过保护读内存, &MyBuf, sizeof(MyBuf), &OutBuf, sizeof(OutBuf), &len, NULL); // 8. 格式化结果并弹窗显示 char a[1024]; // 注意:原代码用%d打印UINT64会截断(%d是32位),已修正为%llx(十六进制)/%I64u(十进制) sprintf_s(a, "am: 过保护读内存结果\n" "DeviceIoControl返回值:%d(1=成功,0=失败)\n" "错误码:%d\n" "驱动实际返回字节数:%d\n" "读取到的内存值(十六进制):%llx\n" "读取到的内存值(十进制):%I64u", ret, GetLastError(), len, OutBuf, OutBuf); MessageBoxA(0, a, "过保护读内存", MB_OK); }完整的项目代码,去百度网盘中查找