ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Win7内核驱动实现进程内存读写:从编译加载到MDL进阶

Win7内核驱动实现进程内存读写:从编译加载到MDL进阶 简介这是一份面向Windows内核驱动初学者与系统安全研究者的Win7内存读写驱动实例围绕Ring 0权限下的物理内存读写展开可用于调试、性能优化及系统级任务的学习实践。资源包共37个文件约27.67MB以Visual Studio工程文件为主包含Driver.c驱动实现源码、Types.h数据结构定义、Driver.sln与vcxproj工程配置以及pdb调试符号、tlog编译日志、ipch预编译缓存等中间产物便于直接编译与调试。内容涉及WDM驱动模型、IRP请求处理、MmAllocateContiguousMemory等内存管理API调用以及内核调试与安全兼容性考量对理解Windows内核操作与驱动编程具有参考价值。目前已有228人学习适合具备一定C语言与系统基础、希望入门内核驱动开发的读者参考借鉴。1. 从 Drv_headed3me 说起Win7 内存读写驱动到底在解决什么问题如果你在 Win7 上做过进程内存读取大概率遇到过这个场景用用户态ReadProcessMemory去读一个带保护的游戏进程返回永远是ERROR_ACCESS_DENIED换管理员权限、调SeDebugPrivilege都没用。这时候绕不开的方案就是写一个内核驱动把读写操作下沉到 Ring0。Drv_headed3me 这个标题指向的正是这类东西——一个跑在 Win7 上的简单内存读写驱动核心能力就两个读目标进程内存、写目标进程内存。它适合谁适合需要在 Win7 环境做进程内存分析、调试辅助、逆向学习验证的工程师。不适合想直接拿去做商业外挂的人那是另一条路。这篇笔记把驱动从编译、加载、通信到读写进程内存的完整链路拆开参数怎么设、坑在哪都落到可复现的步骤上。Win7 虽然老但大量工控机、测试机、老游戏环境还跑着它这套东西现在依然有实际用武之地。2. 驱动读写的基本原理与 Win7 上的选型理由2.1 为什么用户态读写会被拦内核态为什么能过用户态调OpenProcess拿进程句柄时内核的ObOpenObjectByPointer会走一遍访问检查目标进程如果设置了PsProtectedProcess或者被反作弊驱动挂了ObRegisterCallbacks回调句柄请求直接被拒。就算拿到句柄ReadProcessMemory最终走MmCopyVirtualMemory中间还有一层ProbeForRead校验用户缓冲区。内核驱动不一样。驱动运行在 Ring0可以直接操作目标进程的EPROCESS结构拿到DirectoryTableBase也就是 CR3然后手动做地址翻译或者直接调MmCopyVirtualMemory但绕过句柄检查。关键点是驱动里调KeStackAttachProcess挂到目标进程上下文后用RtlCopyMemory就能直接读因为此时当前进程的页表就是目标的页表。提示Win7 x64 有 PatchGuard直接改内核结构比如 SSDT会触发蓝屏。读写内存本身不碰这些相对安全但别顺手去 hook 系统调用。2.2 Win7 上驱动开发的工具链选择Win7 驱动开发有两套工具链WDK 7600对应 Win7 原生和 WDK 8.1/10 配合 Win7 目标平台。我一般用 Visual Studio 2013 WDK 8.1因为 VS2013 对 Win7 驱动项目的支持最稳生成的驱动在 Win7 SP1 上直接能加载。用 VS2019WDK10 也能编但需要手动把目标平台设成 Win7且要装 Win7 的调试符号。编译配置上DriverEntry的DriverObject-DriverUnload一定要设否则驱动加载后卸载不掉只能重启。INF文件里[Version]段的Signature用$WINDOWS NT$Class用SystemClassGuid填{4d36e97d-e325-11ce-bfc1-08002be10318}。2.3 通信方式DeviceIoControl 还是共享内存驱动和用户态通信常见三种DeviceIoControl、共享内存映射、命名管道。做内存读写驱动DeviceIoControl最直接控制码传参清晰同步性好。共享内存适合高频大数据量但同步麻烦。命名管道在驱动里实现复杂不推荐。我一般用DeviceIoControl定义两个控制码一个读、一个写。用户态传结构体包含 PID、目标地址、缓冲区指针、长度。驱动里用METHOD_BUFFERED方式系统会自动把用户缓冲区拷到内核省得自己做ProbeForWrite。3. 从零编译加载一个 Win7 内存读写驱动3.1 驱动入口与设备对象创建先看DriverEntry里要做什么。创建控制设备对象、设置卸载例程、创建符号链接这三步是标配。#include ntddk.h #define DEVICE_NAME L\\Device\\DrvHeaded3me #define SYM_LINK L\\DosDevices\\DrvHeaded3me NTSTATUS DriverEntry(PDRIVER_OBJECT pDriverObj, PUNICODE_STRING pRegistryPath) { UNICODE_STRING devName, symLink; PDEVICE_OBJECT pDevObj NULL; NTSTATUS status; RtlInitUnicodeString(devName, DEVICE_NAME); // 创建设备对象FILE_DEVICE_UNKNOWN 表示自定义设备 status IoCreateDevice(pDriverObj, 0, devName, FILE_DEVICE_UNKNOWN, 0, FALSE, pDevObj); if (!NT_SUCCESS(status)) return status; RtlInitUnicodeString(symLink, SYM_LINK); // 符号链接让用户态能通过 CreateFile 打开设备 status IoCreateSymbolicLink(symLink, devName); if (!NT_SUCCESS(status)) { IoDeleteDevice(pDevObj); return status; } pDriverObj-MajorFunction[IRP_MJ_CREATE] DrvCreateClose; pDriverObj-MajorFunction[IRP_MJ_CLOSE] DrvCreateClose; pDriverObj-MajorFunction[IRP_MJ_DEVICE_CONTROL] DrvDeviceControl; pDriverObj-DriverUnload DrvUnload; return STATUS_SUCCESS; }IoCreateDevice的第四个参数DeviceType用FILE_DEVICE_UNKNOWN因为不是标准设备。IoCreateSymbolicLink把\Device\DrvHeaded3me映射到\DosDevices\DrvHeaded3me用户态才能用CreateFile打开。DriverUnload必须设否则sc stop会失败。3.2 读写内存的 IRP 处理DrvDeviceControl里根据控制码分发。读操作的核心是KeStackAttachProcess挂到目标进程然后直接RtlCopyMemory。#define IOCTL_READ_MEM CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800, METHOD_BUFFERED, FILE_ANY_ACCESS) #define IOCTL_WRITE_MEM CTL_CODE(FILE_DEVICE_UNKNOWN, 0x801, METHOD_BUFFERED, FILE_ANY_ACCESS) typedef struct _MEM_REQUEST { ULONG Pid; PVOID Address; PVOID Buffer; ULONG Size; } MEM_REQUEST, *PMEM_REQUEST; NTSTATUS DrvDeviceControl(PDEVICE_OBJECT pDevObj, PIRP pIrp) { PIO_STACK_LOCATION pStack IoGetCurrentIrpStackLocation(pIrp); ULONG code pStack-Parameters.DeviceIoControl.IoControlCode; PMEM_REQUEST req (PMEM_REQUEST)pIrp-AssociatedIrp.SystemBuffer; NTSTATUS status STATUS_INVALID_PARAMETER; if (req req-Pid req-Address req-Buffer req-Size) { PEPROCESS pEprocess NULL; if (NT_SUCCESS(PsLookupProcessByProcessId((HANDLE)(ULONG_PTR)req-Pid, pEprocess))) { KAPC_STATE apcState; KeStackAttachProcess(pEprocess, apcState); __try { if (code IOCTL_READ_MEM) { RtlCopyMemory(req-Buffer, req-Address, req-Size); } else if (code IOCTL_WRITE_MEM) { RtlCopyMemory(req-Address, req-Buffer, req-Size); } status STATUS_SUCCESS; } __except (EXCEPTION_EXECUTE_HANDLER) { status GetExceptionCode(); } KeUnstackDetachProcess(apcState); ObDereferenceObject(pEprocess); } } pIrp-IoStatus.Status status; pIrp-IoStatus.Information NT_SUCCESS(status) ? req-Size : 0; IoCompleteRequest(pIrp, IO_NO_INCREMENT); return status; }PsLookupProcessByProcessId拿到EPROCESS指针用完必须ObDereferenceObject否则引用计数泄漏。KeStackAttachProcess挂到目标进程后当前线程的页表切换成目标的RtlCopyMemory就能直接访问目标地址。__try/__except必须加目标地址无效时RtlCopyMemory会触发异常不捕获直接蓝屏。注意METHOD_BUFFERED下系统缓冲区大小取输入输出缓冲区中较小的那个。读写大块内存时用户态缓冲区要够大否则IoStatus.Information会小于预期。3.3 用户态调用代码用户态用CreateFile打开符号链接然后DeviceIoControl传MEM_REQUEST。#include windows.h #include stdio.h typedef struct _MEM_REQUEST { ULONG Pid; PVOID Address; PVOID Buffer; ULONG Size; } MEM_REQUEST; int main() { HANDLE hDev CreateFileA(\\\\.\\DrvHeaded3me, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL); if (hDev INVALID_HANDLE_VALUE) { printf(open device failed: %lu\n, GetLastError()); return 1; } DWORD pid 1234; // 目标进程 PID PVOID addr (PVOID)0x400000; // 目标地址 BYTE buf[64] {0}; MEM_REQUEST req { pid, addr, buf, sizeof(buf) }; DWORD ret 0; BOOL ok DeviceIoControl(hDev, IOCTL_READ_MEM, req, sizeof(req), req, sizeof(req), ret, NULL); if (ok) { printf(read ok, first byte: %02X\n, buf[0]); } else { printf(ioctl failed: %lu\n, GetLastError()); } CloseHandle(hDev); return 0; }CreateFileA的路径是\\.\DrvHeaded3me对应驱动里的符号链接。DeviceIoControl的输入输出缓冲区都用req因为METHOD_BUFFERED下系统缓冲区是同一块。IOCTL_READ_MEM和驱动里的定义必须一致否则返回ERROR_INVALID_FUNCTION。3.4 编译、签名与加载Win7 x64 强制驱动签名测试阶段有两个办法一是开机按 F8 选「禁用驱动程序签名强制」二是用测试签名模式。我一般用测试签名因为不用每次重启。# 管理员权限运行 bcdedit /set testsigning on # 重启后生效 # 生成测试证书 makecert -r -pe -ss PrivateCertStore -n CNDrvTest DrvTest.cer # 签名驱动 signtool sign /v /s PrivateCertStore /n DrvTest /t http://timestamp.digicert.com DrvHeaded3me.sys # 注册并启动服务 sc create DrvHeaded3me type kernel binPath C:\path\DrvHeaded3me.sys sc start DrvHeaded3mebcdedit /set testsigning on后桌面右下角会显示「测试模式」这是正常的。sc create的type kernel和binPath后面的空格不能省这是sc命令的格式要求。启动失败用sc query DrvHeaded3me看状态错误码 577 是签名问题错误码 1275 是驱动被阻止加载。4. 避坑Win7 内存读写驱动最容易翻车的五个地方4.1 加载时报错 577 或 1275现象sc start返回错误 577或错误 1275驱动加载不上。原因577 是签名验证失败1275 是驱动被系统策略阻止。Win7 x64 对未签名驱动零容忍测试签名没开或者证书链不对都会报 577。解决先确认bcdedit /enum里testsigning是Yes。如果开了还报 577检查signtool签名时用的证书是不是在PrivateCertStore里/s参数要和makecert的-ss一致。1275 一般是驱动 INF 里的Class或ClassGuid不对改成System和{4d36e97d-e325-11ce-bfc1-08002be10318}。4.2 读写目标进程直接蓝屏现象DeviceIoControl一调用系统立刻蓝屏错误码0x00000050或0x0000001E。原因目标地址无效RtlCopyMemory触发页错误而__try/__except没包住或者包了但异常发生在KeStackAttachProcess之前。解决确认__try块只包RtlCopyMemoryKeStackAttachProcess和PsLookupProcessByProcessId放在外面。另外检查req-Size是不是超过了用户态缓冲区实际大小METHOD_BUFFERED下系统缓冲区大小取输入输出较小值传大了会越界。4.3 读到的数据全是 0 或者乱码现象DeviceIoControl返回成功但buf里全是 0 或者随机值。原因KeStackAttachProcess挂载后目标进程的页表生效但如果目标地址在目标进程里本身就没映射比如未初始化的堆读出来就是 0。乱码一般是地址翻译到了错误的物理页。解决先用VirtualQueryEx在用户态确认目标地址在目标进程里是可读的。如果目标进程是 32 位而驱动是 64 位地址要按 32 位截断。另外确认req-Pid是目标进程的真实 PID不是线程 ID。4.4 驱动卸载不掉sc stop挂起现象sc stop DrvHeaded3me一直卡住最后报错误 1053。原因DriverUnload没设或者设了但里面没删设备对象和符号链接。IRP 处理里有未完成的请求也会导致卸载挂起。解决DriverUnload里必须IoDeleteSymbolicLink和IoDeleteDevice。如果有挂起的 IRP用IoCancelIrp取消。另外DeviceIoControl是同步的一般不会有挂起 IRP但如果有异步操作要额外处理。4.5 目标进程退出后驱动还持有 EPROCESS 引用现象目标进程退出后系统变慢或者驱动再次读写时蓝屏。原因PsLookupProcessByProcessId拿到的EPROCESS引用没释放进程对象泄漏。解决每次PsLookupProcessByProcessId成功后无论读写是否成功都要ObDereferenceObject。用__try/__finally或者确保所有返回路径都走到ObDereferenceObject。我一般把ObDereferenceObject放在KeUnstackDetachProcess之后不管status是什么都执行。5. 进阶用 MDL 映射做更稳的大块内存读写RtlCopyMemory在KeStackAttachProcess下能用但大块内存读写时效率一般而且目标地址跨页时容易触发异常。更稳的做法是用 MDLMemory Descriptor List把目标地址映射到内核空间然后直接访问映射后的虚拟地址。NTSTATUS ReadMemoryByMdl(ULONG pid, PVOID addr, PVOID buf, ULONG size) { PEPROCESS pEprocess NULL; if (!NT_SUCCESS(PsLookupProcessByProcessId((HANDLE)(ULONG_PTR)pid, pEprocess))) return STATUS_NOT_FOUND; KAPC_STATE apcState; KeStackAttachProcess(pEprocess, apcState); PMDL pMdl IoAllocateMdl(addr, size, FALSE, FALSE, NULL); if (!pMdl) { KeUnstackDetachProcess(apcState); ObDereferenceObject(pEprocess); return STATUS_INSUFFICIENT_RESOURCES; } __try { MmProbeAndLockPages(pMdl, KernelMode, IoReadAccess); } __except (EXCEPTION_EXECUTE_HANDLER) { IoFreeMdl(pMdl); KeUnstackDetachProcess(apcState); ObDereferenceObject(pEprocess); return GetExceptionCode(); } PVOID pMapped MmGetSystemAddressForMdlSafe(pMdl, NormalPagePriority); if (pMapped) { RtlCopyMemory(buf, pMapped, size); } MmUnlockPages(pMdl); IoFreeMdl(pMdl); KeUnstackDetachProcess(apcState); ObDereferenceObject(pEprocess); return pMapped ? STATUS_SUCCESS : STATUS_UNSUCCESSFUL; }IoAllocateMdl创建 MDLMmProbeAndLockPages锁定目标页面并校验可读性MmGetSystemAddressForMdlSafe拿到内核映射地址。这样读的时候不依赖当前进程上下文RtlCopyMemory从映射地址读不会触发目标进程的页错误。MmUnlockPages和IoFreeMdl必须成对调用否则内存泄漏。写操作把IoReadAccess改成IoWriteAccessRtlCopyMemory的参数顺序反过来。注意MmProbeAndLockPages在目标地址无效时会抛异常必须用__try/__except包住。对比项RtlCopyMemory 方案MDL 方案实现复杂度低中大块读写效率一般高跨页处理易触发异常自动处理适用场景小数据量、快速验证大数据量、稳定读写验证驱动是否正常工作我习惯用 WinDbg 双机调试。windbg -k com:pipe,port\\.\pipe\com_1,resets0连上目标机在DrvDeviceControl下断点看req-Pid和req-Address是不是预期值。如果断点没命中说明 IRP 没走到这个分支检查控制码是否匹配。最后说个血泪教训每次改完驱动代码先在本机测试签名模式下加载确认能sc start再sc stop再去目标机跑。我早期有次直接往目标机扔结果驱动卸载不掉只能强制重启浪费一下午。另外PsLookupProcessByProcessId的 PID 用ULONG传别用HANDLE直接强转32 位和 64 位下HANDLE宽度不一样容易翻车。希望帮到你。本文还有配套的精品资源点击获取
返回列表