news 2026/10/7 13:34:05

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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 上直接能加载。用 VS2019+WDK10 也能编,但需要手动把目标平台设成 Win7,且要装 Win7 的调试符号。

编译配置上,DriverEntry的DriverObject->DriverUnload一定要设,否则驱动加载后卸载不掉,只能重启。INF文件里[Version]段的Signature用$WINDOWS NT$,Class用System,ClassGuid填{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 "CN=DrvTest" 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 DrvHeaded3me

bcdedit /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块只包RtlCopyMemory,KeStackAttachProcess和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下能用,但大块内存读写时效率一般,而且目标地址跨页时容易触发异常。更稳的做法是用 MDL(Memory 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创建 MDL,MmProbeAndLockPages锁定目标页面并校验可读性,MmGetSystemAddressForMdlSafe拿到内核映射地址。这样读的时候不依赖当前进程上下文,RtlCopyMemory从映射地址读,不会触发目标进程的页错误。MmUnlockPages和IoFreeMdl必须成对调用,否则内存泄漏。

写操作把IoReadAccess改成IoWriteAccess,RtlCopyMemory的参数顺序反过来。注意MmProbeAndLockPages在目标地址无效时会抛异常,必须用__try/__except包住。

对比项RtlCopyMemory 方案MDL 方案
实现复杂度低中
大块读写效率一般高
跨页处理易触发异常自动处理
适用场景小数据量、快速验证大数据量、稳定读写

验证驱动是否正常工作,我习惯用 WinDbg 双机调试。windbg -k com:pipe,port=\\.\pipe\com_1,resets=0连上目标机,在DrvDeviceControl下断点,看req->Pid和req->Address是不是预期值。如果断点没命中,说明 IRP 没走到这个分支,检查控制码是否匹配。

最后说个血泪教训:每次改完驱动代码,先在本机测试签名模式下加载,确认能sc start再sc stop,再去目标机跑。我早期有次直接往目标机扔,结果驱动卸载不掉,只能强制重启,浪费一下午。另外PsLookupProcessByProcessId的 PID 用ULONG传,别用HANDLE直接强转,32 位和 64 位下HANDLE宽度不一样,容易翻车。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/7 13:33:59

Java 构建中医药知识数据库:表结构设计与多条件检索优化实战

简介&#xff1a;这份资源是一套基于Java开发的传统中医药知识数据库源码&#xff0c;面向中医药信息化开发者、计算机专业学生及需要构建知识库系统的技术人员&#xff0c;用于解决中医药知识从纸质文献向数字化存储、检索与传播转型的问题。压缩包共1024个文件&#xff0c;约…

作者头像 李华
网站建设 2026/10/7 13:33:26

DeepSeek Harness 桌面端实操:从安装部署到插件工作流全记录

DeepSeek Harness 出了桌面端&#xff0c;这消息一出来我当天就装上了。这工具我之前主要拿它做 AI 编码辅助和自动化工作流&#xff0c;命令行版本用得挺顺手&#xff0c;但很多操作得翻文档、敲命令&#xff0c;团队里非技术背景的同事基本用不起来。看到桌面端出现&#xff…

作者头像 李华
网站建设 2026/10/7 13:33:23

花类识别五分类实战:从数据集预处理到迁移学习模型训练

简介&#xff1a;一份面向图像分类与植物识别训练的花类数据集&#xff0c;适合深度学习初学者、算法开发者及计算机视觉课设。图片采集自多个网络来源&#xff0c;覆盖洋甘菊、郁金香、玫瑰、向日葵、蒲公英五个常见类别&#xff0c;每类约八百张照片&#xff0c;图像分辨率约…

作者头像 李华
网站建设 2026/10/7 13:31:29

大模型接口碎片化怎么办?统一适配层、重试与路由实战指南

你有过这种经历吗&#xff1f;周末想把自己写的小应用从 GPT 换到 Claude&#xff0c;结果改了一晚上接口&#xff0c;聊天还没跑起来。我在做多模型应用开发的时候&#xff0c;这种经历差不多每周一次&#xff1a;接入的模型越多&#xff0c;接口碎片化问题就越明显——各家给…

作者头像 李华
网站建设 2026/10/7 13:31:28

从零部署OpenClaw:打造24小时在线的AI数字打工仔

说实话&#xff0c;我以前对AI的印象就是“聊天机器人”&#xff0c;问一句答一句&#xff0c;偶尔还能写点文案。直到我把OpenClaw装到一台吃灰的迷你主机上&#xff0c;让它每天凌晨自动拉取数据、生成日报、再去检查邮件附件&#xff0c;我才意识到&#xff1a;所谓“数字打…

作者头像 李华
网站建设 2026/10/7 13:31:27

AI实战手册:大模型、Agent、AI编程与内容生成全解析

今天整理了一份AI领域的信息简报&#xff0c;正好趁着假期把最近这段时间圈子里讨论比较多的方向、工具和一些实操中踩过的坑&#xff0c;统一梳理一遍。这份内容不是什么新闻稿&#xff0c;更多是站在从业者角度&#xff0c;围绕我这两天看到的动态、热搜词背后的技术点&#…

作者头像 李华