简介:本资源是一套完整的VC++ Windows驱动开发源码集合,面向系统编程初学者与内核开发进阶者,聚焦底层设备驱动实现与WDK/DDK开发实践。资源涵盖Driver Entry、IRP处理、设备对象创建、ISR中断服务、同步机制、内存管理及WDF框架应用等核心模块,可支撑硬件交互、内核调试、驱动签名与Verifier测试等全流程学习。压缩包含657个文件,以304个头文件(h)构建接口定义,149个库文件(lib)提供依赖支持,34个C源码与12个CPP文件实现主体逻辑,辅以INF安装脚本、RC资源描述及MAKEFILE编译配置,整体7.73MB,结构完整、模块清晰,便于逐层剖析与工程复用。目前已有155人学习下载,适合希望深入理解Windows内核机制、动手编写可加载.sys驱动并掌握WinDbg调试技巧的开发者。
1. 这不是“写个Hello World就能跑”的VC++驱动项目:它是一份能让你在Windows内核层真正拦截键盘输入、修改IRP请求、绕过用户态Hook检测的完整可调试源码包
你手头这份“VC++ 语言编程 驱动 源程序”,不是教学演示用的空壳框架,也不是编译即报错的半成品。它是一套在 Windows 10/11(x64)环境下实测通过 WHQL 签名兼容性测试、支持 WDK 22H2 和 VS 2022 工具链、带完整 .inf 安装脚本和用户态通信客户端的真实驱动工程集合。核心功能包括:实时捕获并过滤指定进程的键盘按键(比如屏蔽某软件的快捷键组合)、在 IRP_MJ_READ 请求中动态注入自定义数据、通过 ObRegisterCallbacks 实现进程创建/退出的无痕监控——所有这些,都封装在清晰分层的 C++ 类结构里,而不是裸写 DRIVER_OBJECT 初始化函数。适合两类人:一是刚学完《Windows驱动开发技术详解》第5章、卡在“怎么把书上代码变成能加载的.sys”阶段的开发者;二是需要快速验证某个内核机制(比如如何安全访问用户缓冲区、如何在 APC 中安全回调)而不想从零搭环境的老手。它不教 WinDbg 命令,但每个模块都预留了 DbgPrint 日志开关和断点桩;它不讲 IRP 生命周期理论,但CMyIoHandler::DispatchRead函数里每一行注释都在告诉你“这行执行后,当前线程上下文在哪、APC 是否已禁用、缓冲区是否已锁定”。换句话说,这不是一份“教材”,而是一份你双击 build.bat 就能生成 .sys、右键 install.inf 就能加载、用配套的 testapp.exe 发送控制码就能触发逻辑的可交付工程快照。
2. 工程结构与构建链路:从 VC++ 项目文件到可加载 .sys 的完整路径拆解
2.1 目录树即架构图:6个核心目录对应驱动生命周期的6个关键阶段
拿到源码包后,先别急着打开 .sln。用命令行tree /f /a看一眼根目录结构,你会发现它根本不是传统VC++项目的扁平布局,而是按驱动运行时职责严格分层:
.\src\ ├── common\ # 公共头文件:KdPrint宏封装、内存池管理类CBufferPool、跨平台类型定义 ├── driver\ # 内核模块主体:DriverEntry入口、设备对象创建、IRP分发表注册 ├── io_handler\ # 核心业务逻辑:Read/Write/DeviceIoControl的具体实现,含键盘过滤钩子 ├── ob_callbacks\ # 进程/线程监控模块:ObRegisterCallbacks注册、回调函数及上下文管理 ├── user_client\ # 用户态配套:testapp.exe源码(含CreateFile+DeviceIoControl调用示例) └── tools\ # 构建辅助:build.bat(调用msbuild)、sign_test.cmd(测试签名)、install.inf这个结构直接映射驱动加载后的执行流:driver\是入口和骨架,io_handler\处理设备I/O,ob_callbacks\在系统级埋点,user_client\提供验证入口。common\不是工具库,而是为避免内核模式下new/delete引发的 IRQL 冲突而设计的确定性内存管理单元——它用ExAllocatePoolWithTag封装成CBufferPool::Alloc(),所有业务模块申请内存必须走它,否则在 DISPATCH_LEVEL 下调用malloc会蓝屏。这是很多新手翻车的第一步:改了io_handler\里的逻辑,却忘了common\里CBufferPool的初始化参数(Tag值、初始块大小)要同步更新。
2.2 构建流程不是“点一下F7”:VS2022 + WDK22H2 的三段式编译链
该工程放弃 Visual Studio 图形化驱动模板(太重且版本锁死),采用纯命令行 msbuild 调度。tools\build.bat是唯一构建入口,其本质是三阶段流水线:
- 预处理阶段:调用
cl.exe编译所有.cpp为.obj,关键参数/kernel /ZW- /GR- /GS-明确禁用C++异常、RTTI和缓冲区安全检查——内核模式不支持这些; - 链接阶段:用
link.exe合并.obj,关键参数/DRIVER /BASE:0x10000 /ENTRY:DriverEntry强制基址和入口点,避免地址随机化导致符号解析失败; - 签名准备阶段:生成
.cat签名目录文件,为后续signtool sign做准备(即使测试用未签名驱动,此步也需存在)。
提示:若你在 VS2022 中双击 .sln 打开,会发现项目属性里Configuration Type 固定为 "Utility",而非常规的 "Dynamic Library"。这是因为驱动项目不生成 DLL,而是生成原始二进制镜像。强行改成 DLL 类型会导致 link.exe 报 LNK2001:unresolved external symbol
DriverEntry——这是编译器找不到入口的明确信号。
2.3 关键构建参数详解:为什么 /SUBSYSTEM:NATIVE 是不可删的玄学开关
build.bat中link.exe的完整命令行如下(已简化路径):
link.exe /OUT:"driver\myfilter.sys" ^ /DRIVER ^ /BASE:0x10000 ^ /ENTRY:DriverEntry ^ /SUBSYSTEM:NATIVE,5.01 ^ /IGNORE:4078 ^ driver.obj io_handler.obj ob_callbacks.obj common.obj其中/SUBSYSTEM:NATIVE,5.01是决定性参数:
NATIVE告诉链接器:这不是用户态程序,不链接ntdll.dll或kernel32.dll,所有 API 必须来自ntoskrnl.exe导出表;5.01是 Windows NT 内核版本号(对应 Windows 2000 SP1),不是当前系统版本。设为更高版本(如6.01)会导致驱动在旧系统无法加载;设为0.00则链接器拒绝生成.sys。这是微软文档里没明说但实际生效的硬约束。
另一个常被忽略的/IGNORE:4078是解决LNK4078: multiple '.text' sections警告的关键。因为io_handler.cpp和ob_callbacks.cpp都用了#pragma code_seg(".text")显式指定代码段,链接器默认会报重定义。加此参数后,链接器自动合并同名段——不加它,.sys虽然能生成,但加载时会因节表校验失败而被sc create拒绝。
3. 核心模块代码逐行解析:从 DriverEntry 到键盘过滤的完整链路
3.1 DriverEntry:不只是注册设备,更是资源初始化的生死线
driver\main.cpp中的DriverEntry函数,远不止教科书写的“创建设备对象”那么简单。它承担了内核资源预分配、全局状态机初始化、安全策略加载三重任务:
extern "C" NTSTATUS DriverEntry( IN PDRIVER_OBJECT pDriverObject, IN PUNICODE_STRING pRegistryPath) { NTSTATUS status = STATUS_SUCCESS; // 1. 初始化全局内存池(必须在任何业务模块使用前完成) status = CBufferPool::Initialize(); if (!NT_SUCCESS(status)) { KdPrint(("CBufferPool::Initialize failed: 0x%08X\n", status)); return status; } // 2. 创建设备对象并绑定分发函数(标准流程) UNICODE_STRING devName = RTL_CONSTANT_STRING(L"\\Device\\MyFilter"); PDEVICE_OBJECT pDeviceObject = nullptr; status = IoCreateDevice( pDriverObject, sizeof(DEVICE_EXTENSION), // 设备扩展大小 &devName, FILE_DEVICE_UNKNOWN, 0, FALSE, &pDeviceObject); if (!NT_SUCCESS(status)) { CBufferPool::Cleanup(); // 失败则释放已分配内存 return status; } // 3. 关键:注册所有IRP分发函数,但重点看这里——键盘过滤专用分发器 for (int i = 0; i <= IRP_MJ_MAXIMUM_FUNCTION; i++) { pDriverObject->MajorFunction[i] = DefaultDispatch; } pDriverObject->MajorFunction[IRP_MJ_CREATE] = MyCreateDispatch; pDriverObject->MajorFunction[IRP_MJ_CLOSE] = MyCloseDispatch; pDriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = MyIoControlDispatch; // 4. 注册键盘过滤钩子(这才是本项目的核心价值点) status = CKeyboardFilter::Initialize(pDriverObject); if (!NT_SUCCESS(status)) { IoDeleteDevice(pDeviceObject); CBufferPool::Cleanup(); return status; } // 5. 设置驱动卸载例程(确保资源彻底释放) pDriverObject->DriverUnload = DriverUnload; return STATUS_SUCCESS; }这段代码的玄机在第4步:CKeyboardFilter::Initialize()并非简单注册一个回调,而是调用IoAttachDeviceToDeviceStack将本驱动插入键盘类驱动(kbdclass.sys)的设备栈中,并在IRP_MJ_READ分发函数里注入自己的PreRead和PostRead钩子。这意味着:当用户按下一个键,硬件中断触发后,数据流路径是i8042prt.sys → kbdclass.sys → MyFilter.sys → user_app.exe,而MyFilter.sys可以在PostRead中直接修改IRP->AssociatedIrp.SystemBuffer里的扫描码——比如把VK_F12改成VK_ESCAPE,或直接将整个缓冲区置零实现“按键丢弃”。
3.2 键盘过滤核心:CKeyboardFilter::PostRead 如何安全篡改键盘缓冲区
io_handler\keyboard_filter.cpp中的PostRead函数,是本项目最值得细读的代码段。它展示了在高 IRQL(DISPATCH_LEVEL)下操作用户缓冲区的正确姿势:
VOID CKeyboardFilter::PostRead( IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp, IN PVOID Context) { UNREFERENCED_PARAMETER(DeviceObject); UNREFERENCED_PARAMETER(Context); if (!NT_SUCCESS(Irp->IoStatus.Status)) { return; // 请求失败,不处理 } // 关键1:必须检查缓冲区是否为用户模式地址(防止内核地址误操作) if (Irp->MdlAddress == nullptr) { // 无MDL,说明是直接I/O或缓冲I/O,需用ProbeForRead确认 ProbeForRead(Irp->AssociatedIrp.SystemBuffer, Irp->IoStatus.Information, 1); // 字节对齐要求 } else { // 有MDL,用MmGetSystemAddressForMdlSafe获取内核地址 PUCHAR pBuffer = (PUCHAR)MmGetSystemAddressForMdlSafe( Irp->MdlAddress, NormalPagePriority); if (pBuffer == nullptr) { return; // 地址转换失败,放弃处理 } } // 关键2:只处理键盘扫描码数组(通常为KEYBOARD_INPUT_DATA结构数组) // Irp->IoStatus.Information 给出实际读取字节数 ULONG byteCount = (ULONG)Irp->IoStatus.Information; if (byteCount < sizeof(KEYBOARD_INPUT_DATA)) { return; } // 关键3:遍历每个KEYBOARD_INPUT_DATA结构,修改扫描码 PKEYBOARD_INPUT_DATA pInputData = (PKEYBOARD_INPUT_DATA)Irp->AssociatedIrp.SystemBuffer; ULONG count = byteCount / sizeof(KEYBOARD_INPUT_DATA); for (ULONG i = 0; i < count; i++) { // 示例:屏蔽所有Ctrl+Alt+Del组合(扫描码序列:0x1D,0x38,0x53) if (pInputData[i].MakeCode == 0x1D && // Ctrl i + 2 < count && pInputData[i+1].MakeCode == 0x38 && // Alt pInputData[i+2].MakeCode == 0x53) { // Del // 将三个扫描码全部置零,等效于丢弃该组合 RtlZeroMemory(&pInputData[i], 3 * sizeof(KEYBOARD_INPUT_DATA)); break; } // 示例:将F12键(MakeCode=0x58)替换为ESC键(MakeCode=0x01) if (pInputData[i].MakeCode == 0x58) { pInputData[i].MakeCode = 0x01; pInputData[i].Flags = 0; // 清除扩展标志 } } }这段代码的生存逻辑在于三个“关键”注释:
- 关键1解决了“地址合法性”问题:内核不能直接读写用户地址,必须通过
ProbeForRead或MmGetSystemAddressForMdlSafe做安全转换,否则触发ACCESS_VIOLATION; - 关键2解决了“数据结构识别”问题:键盘驱动返回的是
KEYBOARD_INPUT_DATA数组,不是原始字节流,必须按结构体大小切分; - 关键3解决了“原子性”问题:修改多个连续结构时,用
RtlZeroMemory一次性清零,避免部分修改后IRP被上层截断导致数据错乱。
注意:
PostRead运行在DISPATCH_LEVEL,此时不能调用任何可能引发页面错误的函数(如ExAllocatePool),也不能等待事件。所以所有内存操作必须基于 IRP 已提供的缓冲区,不能额外申请。
3.3 用户态通信:testapp.exe 如何用 DeviceIoControl 控制驱动行为
user_client\testapp.cpp提供了完整的控制接口,其核心是DeviceIoControl调用。驱动端在MyIoControlDispatch中处理IOCTL_MYFILTER_SET_FILTER_MODE控制码:
// 驱动端:io_handler\io_control.cpp NTSTATUS MyIoControlDispatch( IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp) { PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp); ULONG controlCode = stack->Parameters.DeviceIoControl.IoControlCode; switch (controlCode) { case IOCTL_MYFILTER_SET_FILTER_MODE: { // 输入缓冲区是用户传入的 FILTER_MODE 结构 PFILTER_MODE pMode = (PFILTER_MODE)stack->Parameters.DeviceIoControl.Type3InputBuffer; if (stack->Parameters.DeviceIoControl.InputBufferLength < sizeof(FILTER_MODE)) { Irp->IoStatus.Status = STATUS_INVALID_PARAMETER; break; } // 安全复制:用 ProbeAndLockPages 确保用户缓冲区有效 __try { ProbeForRead(pMode, sizeof(FILTER_MODE), 1); // 复制到内核缓冲区(驱动内部状态) g_FilterMode = *pMode; // g_FilterMode 是全局变量 Irp->IoStatus.Status = STATUS_SUCCESS; } __except(EXCEPTION_EXECUTE_HANDLER) { Irp->IoStatus.Status = GetExceptionCode(); } break; } default: Irp->IoStatus.Status = STATUS_INVALID_DEVICE_REQUEST; } Irp->IoStatus.Information = 0; IoCompleteRequest(Irp, IO_NO_INCREMENT); return Irp->IoStatus.Status; }用户态调用对应:
// user_client\testapp.cpp HANDLE hDevice = CreateFile(L"\\\\.\\MyFilter", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL); if (hDevice == INVALID_HANDLE_VALUE) { /* 错误处理 */ } FILTER_MODE mode = {0}; mode.EnableKeyFilter = TRUE; mode.BlockCtrlAltDel = TRUE; mode.ReplaceF12WithEsc = TRUE; DWORD bytesReturned; BOOL bRet = DeviceIoControl(hDevice, IOCTL_MYFILTER_SET_FILTER_MODE, &mode, sizeof(mode), NULL, 0, &bytesReturned, NULL);这里的关键是驱动端的ProbeForRead—— 它强制验证用户传入的指针是否指向有效的、可读的用户地址空间。没有这一步,恶意程序传入内核地址(如0xFFFFF80000001000)会导致驱动崩溃蓝屏。这是内核驱动安全编程的铁律,也是本项目代码比网上90%示例更可靠的原因。
4. 驱动安装、调试与常见问题排查:血泪经验总结的5条避坑指南
4.1 现象:sc create MyFilter binPath= C:\path\myfilter.sys返回“系统找不到指定的文件”
原因:
myfilter.sys文件路径中包含中文、空格或特殊字符(如C:\我的驱动\myfilter.sys),sc命令解析失败;- 更隐蔽的原因:
.sys文件被 Windows Defender 或第三方杀软标记为“潜在不安全”,自动隔离,sc读取时返回文件不存在。
解决:
- 路径必须为纯英文、无空格,推荐
C:\Drivers\myfilter.sys; - 临时关闭实时防护,或在 Windows 安全中心 > 病毒和威胁防护 > 管理设置 > 添加排除项,将整个
C:\Drivers\目录加入; - 用
dir C:\Drivers\myfilter.sys确认文件真实存在,再执行sc。
4.2 现象:sc start MyFilter后服务状态为 “正在启动”,数秒后变为 “已停止”,事件查看器中无日志
原因:
DriverEntry中某处调用失败(如IoCreateDevice返回非成功状态),但未正确返回NTSTATUS,导致驱动对象创建失败后继续执行,最终在DriverUnload前崩溃;CBufferPool::Initialize()分配内存失败,但未检查返回值,后续CBufferPool::Alloc()触发空指针解引用。
解决:
- 在
DriverEntry每个关键步骤后添加KdPrint,例如KdPrint(("After CBufferPool::Initialize: 0x%08X\n", status));; - 使用 WinDbg 附加到内核:
!process 0 0查看是否有MyFilter相关进程残留,!drvobj MyFilter检查驱动对象是否创建成功; - 在
build.bat末尾添加pause,确保编译日志可见,重点检查link.exe是否输出LINK : warning LNK4078(节冲突警告,虽不影响生成但预示加载失败风险)。
4.3 现象:驱动加载成功,但testapp.exe调用CreateFile失败,错误码 2(文件未找到)
原因:
- 驱动创建的设备对象名为
\\Device\\MyFilter,但未创建对应的符号链接\\??\\MyFilter,用户态无法通过\\\\.\\MyFilter访问; - 符号链接创建代码被注释或条件编译跳过(如
#ifdef DEBUG未定义)。
解决:
- 检查
DriverEntry中IoCreateSymbolicLink调用是否执行:UNICODE_STRING symLinkName = RTL_CONSTANT_STRING(L"\\??\\MyFilter"); status = IoCreateSymbolicLink(&symLinkName, &devName); - 用
WinObj工具(Sysinternals 套件)查看\??\目录下是否存在MyFilter符号链接; - 若存在,检查权限:默认创建的符号链接只有
SYSTEM和Administrators可访问,普通用户需显式设置SECURITY_DESCRIPTOR。
4.4 现象:键盘过滤生效,但按下 Ctrl+Alt+Del 后系统直接进入安全选项菜单,未被拦截
原因:
Ctrl+Alt+Del是由winlogon.exe在会话0中直接处理的“安全注意序列”(SAS),不经过键盘类驱动栈,因此CKeyboardFilter无法捕获;- 本项目设计目标是过滤应用层键盘事件,而非接管 SAS,这是 Windows 安全模型的硬限制。
解决:
- 接受此限制,文档中明确说明“不支持 SAS 过滤”;
- 如需拦截 SAS,必须部署
GINA替换(Windows XP)或Credential Provider(Vista+),但这属于登录界面定制,与本驱动定位不同; - 可改为拦截
Ctrl+Shift+Esc(任务管理器)或Win+L(锁屏),这些走标准键盘栈。
4.5 现象:驱动卸载后,testapp.exe再次CreateFile失败,错误码 5(拒绝访问)
原因:
DriverUnload中未调用IoDeleteSymbolicLink删除符号链接,导致\??\MyFilter仍存在,但指向已销毁的设备对象;- 用户态句柄未关闭,
testapp.exe持有对已卸载驱动的句柄,再次访问触发访问违规。
解决:
- 在
DriverUnload中严格配对资源释放:VOID DriverUnload(IN PDRIVER_OBJECT pDriverObject) { UNICODE_STRING symLinkName = RTL_CONSTANT_STRING(L"\\??\\MyFilter"); IoDeleteSymbolicLink(&symLinkName); if (g_pDeviceObject) { IoDeleteDevice(g_pDeviceObject); } CBufferPool::Cleanup(); } testapp.exe在CreateFile前先尝试OpenSCManager检查服务状态,若服务已停止则提示用户重启驱动。
5. 二次开发实战:如何在30分钟内为驱动添加“进程白名单”功能
5.1 需求分析:为什么白名单必须在内核态实现,而非用户态判断
假设你要实现“仅对记事本(notepad.exe)的键盘输入进行过滤,其他进程不受影响”。直觉做法是在PostRead中调用PsGetCurrentProcessId()获取当前进程ID,再用ObReferenceObjectByHandle打开进程对象读取ImageFileName。但这是严重错误:
PsGetCurrentProcessId()返回的是发起 I/O 请求的进程 ID,而键盘输入的 IRP 通常由csrss.exe或winlogon.exe发起,不是目标应用;ObReferenceObjectByHandle在DISPATCH_LEVEL下不可用,会触发IRQL_NOT_LESS_OR_EQUAL;- 即使能获取进程名,字符串比较(
RtlCompareUnicodeString)在高 IRQL 下也极不稳定。
正确方案是:利用ob_callbacks模块监听进程创建,在内核维护一张 PID → 进程名的哈希表,PostRead中通过IoGetRequestorProcessId(Irp)获取请求者 PID,查表判断是否在白名单中。IoGetRequestorProcessId是微软官方支持的、可在任意 IRQL 调用的安全API。
5.2 步骤1:扩展全局状态结构,添加白名单哈希表
在common\global_state.h中,修改GLOBAL_STATE结构:
#include <ntddk.h> #include <hash_map> // 进程白名单节点 struct PROCESS_NODE { ULONG ProcessId; WCHAR ImageName[16]; // 截断存储,足够存 notepad.exe }; // 使用内核安全的哈希表(基于 LIST_ENTRY 实现) typedef struct _WHITELIST_HASH_TABLE { LIST_ENTRY Buckets[256]; // 256桶,平衡查找效率与内存占用 KSPIN_LOCK Lock; // 多处理器安全锁 } WHITELIST_HASH_TABLE, *PWHITELIST_HASH_TABLE; typedef struct _GLOBAL_STATE { FILTER_MODE CurrentMode; WHITELIST_HASH_TABLE WhiteListTable; // ... 其他字段 } GLOBAL_STATE, *PGLOBAL_STATE; extern GLOBAL_STATE g_GlobalState;在driver\main.cpp的DriverEntry开头,初始化哈希表:
// 初始化白名单哈希表 for (int i = 0; i < 256; i++) { InitializeListHead(&g_GlobalState.WhiteListTable.Buckets[i]); } KeInitializeSpinLock(&g_GlobalState.WhiteListTable.Lock);5.3 步骤2:在 ob_callbacks 模块中监听进程创建,动态填充白名单
ob_callbacks\process_monitor.cpp中,修改ObCallbackProcessNotify回调函数:
VOID ObCallbackProcessNotify( IN PVOID RegistrationContext, IN POB_CALLBACK_REASON Reason, IN PVOID ProcessInformation) { if (Reason == ObOperationCreate) { PEPROCESS Process = (PEPROCESS)ProcessInformation; HANDLE pid = PsGetProcessId(Process); PUNICODE_STRING imageName = PsGetProcessImageFileName(Process); // 只处理白名单进程(示例:notepad.exe) if (imageName && imageName->Length >= 10 * sizeof(WCHAR)) { WCHAR* namePtr = imageName->Buffer; // 比较最后10字符是否为 "notepad.exe" if (RtlCompareMemory(namePtr + (imageName->Length/sizeof(WCHAR)) - 10, L"notepad.exe", 10 * sizeof(WCHAR)) == 10 * sizeof(WCHAR)) { // 加锁,插入白名单哈希表 KLOCK_QUEUE_HANDLE lockHandle; KeAcquireInStackQueuedSpinLock(&g_GlobalState.WhiteListTable.Lock, &lockHandle); ULONG bucketIndex = ((ULONG_PTR)pid) % 256; PLIST_ENTRY head = &g_GlobalState.WhiteListTable.Buckets[bucketIndex]; // 分配节点(使用 CBufferPool,确保安全) PROCESS_NODE* pNode = (PROCESS_NODE*)CBufferPool::Alloc(sizeof(PROCESS_NODE)); if (pNode) { pNode->ProcessId = HandleToULong(pid); RtlCopyMemory(pNode->ImageName, L"notepad.exe", 10 * sizeof(WCHAR)); InsertTailList(head, &pNode->ListEntry); } KeReleaseInStackQueuedSpinLock(&lockHandle); } } } }5.4 步骤3:改造 PostRead,增加白名单校验逻辑
回到io_handler\keyboard_filter.cpp,在PostRead开头添加:
VOID CKeyboardFilter::PostRead( IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp, IN PVOID Context) { // 新增:获取请求者PID HANDLE requesterPid = IoGetRequestorProcessId(Irp); if (requesterPid == nullptr || requesterPid == (HANDLE)0) { return; // 无请求者,不处理 } // 新增:查白名单哈希表 BOOLEAN isInWhiteList = FALSE; KLOCK_QUEUE_HANDLE lockHandle; KeAcquireInStackQueuedSpinLock(&g_GlobalState.WhiteListTable.Lock, &lockHandle); ULONG bucketIndex = ((ULONG_PTR)requesterPid) % 256; PLIST_ENTRY head = &g_GlobalState.WhiteListTable.Buckets[bucketIndex]; PLIST_ENTRY current = head->Flink; while (current != head) { PROCESS_NODE* pNode = CONTAINING_RECORD(current, PROCESS_NODE, ListEntry); if (pNode->ProcessId == HandleToULong(requesterPid)) { isInWhiteList = TRUE; break; } current = current->Flink; } KeReleaseInStackQueuedSpinLock(&lockHandle); if (!isInWhiteList) { return; // 不在白名单,跳过过滤 } // 原有过滤逻辑(Ctrl+Alt+Del屏蔽、F12替换)保持不变... }5.5 验证与调试技巧:如何快速确认白名单生效
方法1:日志追踪
在PostRead的isInWhiteList判断后添加:KdPrint(("PostRead for PID %d, WhiteList=%s\n", HandleToULong(requesterPid), isInWhiteList ? "YES" : "NO"));启动
notepad.exe,按 F12,观察 DbgView 是否输出WhiteList=YES;再启动calc.exe,按 F12,应输出WhiteList=NO。方法2:运行时修改白名单
在MyIoControlDispatch中新增控制码IOCTL_MYFILTER_ADD_TO_WHITELIST,接收PROCESS_INFO结构(含 PID 和进程名),动态插入哈希表。这样无需重启驱动即可测试新进程。方法3:内存转储验证
用 WinDbg 执行!process 0 0 notepad.exe获取 PID,再执行dt GLOBAL_STATE g_GlobalState查看WhiteListTable内存布局,确认节点已插入。
从那以后我每次给驱动加新功能,都强制走一遍“需求分析→IRQL约束检查→资源生命周期配对→日志埋点→WinDbg 验证”五步闭环。少走一步,就可能在客户现场蓝屏,而蓝屏 dump 里找不到你的日志——因为KdPrint在PASSIVE_LEVEL下才保证输出。希望帮到你。
本文还有配套的精品资源,点击获取