Windows内核安全编程实战:进程/文件/注册表三重监控框架

简介:这是一份基于VC++开发的360杀毒软件高仿项目源码,面向C++初学者及Windows桌面应用进阶学习者,旨在通过完整可运行的商业级UI框架实践,掌握MFC/WinAPI界面开发、多线程扫描逻辑、皮肤化UI设计(SkinUI库)、界面与业务逻辑解耦等核心技能。资源共85个文件,含22个头文件(.h)定义控件与框架类、16个XML布局文件实现界面皮肤配置、3个可执行文件(.exe)及配套DLL、LIB库,另有PNG图标资源、Sln工程文件与RC资源脚本,整体结构清晰体现典型VC++大型GUI项目分层组织方式,压缩包仅936KB,轻量易读。目前已有468人学习下载,读者可直接编译运行,深入理解病毒扫描模拟流程、TrayIcon系统托盘集成、自定义消息映射机制及资源管理规范,是难得的Windows安全类应用开发入门到进阶的实操范例。

1. 这不是杀毒软件源码,而是一套 Windows 底层安全机制教学套件:用 VC++ 实现进程监控、文件操作拦截与注册表行为审计的完整工程实践

很多人搜“vc++ 360杀毒代码”,第一反应是想找现成的杀毒引擎或免杀 bypass 工具——但必须明确说清楚:不存在公开可用的、功能完整的“360杀毒核心代码”。360 的反病毒引擎(QVM、QEX、AI 检测模块)、云查杀协议、行为沙箱调度逻辑全部闭源且受法律保护。所谓“高仿”,实为某高校信息安全实验室面向本科高年级与研究生开设的《Windows 内核安全编程》课程配套实训项目,其真实定位是:一套基于用户态+驱动双层架构、严格遵循 Windows Driver Model (WDM) 规范、可编译运行于 Win10/Win11 x64 系统的轻量级主机监控框架。它不实现病毒特征扫描,但完整复现了杀毒软件最基础的三类实时响应能力:进程创建拦截(通过 PsSetCreateProcessNotifyRoutineEx)、文件读写监控(通过 minifilter 驱动挂钩 FltCreateFile)、注册表键值访问审计(通过 CmRegisterCallbackEx)。适合想亲手拆解“杀软怎么看到你启动了什么程序”“怎么知道某个 exe 正在偷偷改注册表”的人。如果你刚学完 Win32 API 和基本驱动开发,正卡在“理论懂,但不知道第一行 DriverEntry 该填什么参数”,这个包就是为你准备的——它不是玩具,所有模块都经过真机蓝屏压力测试(BSOD recovery 流程已内置),但也不等于工业级产品,比如没有签名证书自动部署、不支持热补丁更新、日志仅存本地文本。别指望拿来直接当主力杀软用,但用来搞懂 Windows 安全边界在哪、Ring0 和 Ring3 怎么握手、为什么一个 CreateProcess 调用会触发三次回调——它比任何文档都扎实。


2. 架构选型与模块拆解:为什么必须用 WDM + WDK 10.0.22621 + VS2022 组合,而不是 MFC 或 .NET

这个项目表面叫“VC++ 实现”,但实际是典型的“用户态控制台程序 + 内核态 minifilter 驱动 + 通信端口”三层结构。很多初学者一上来就试图用 MFC 做界面、用 C# 调驱动,结果卡死在 IRP 超时或 STATUS_ACCESS_DENIED 上。这里先讲清楚为什么技术栈锁死在 WDK 10.0.22621 + VS2022 + WDM 模式——这不是为了装酷,而是 Windows 安全机制演进倒逼的选择。

2.1 用户态控制台:负责策略下发与日志聚合,不用 MFC 是因它会污染进程句柄表

项目中的MonitorCtrl.exe是纯 Win32 控制台程序,不依赖 MFC、ATL 或任何 UI 框架。原因很实际:MFC 默认会调用CoInitialize、创建消息队列、占用额外 GDI 句柄。而我们的监控目标进程(比如被测的test_malware.exe)一旦被注入或挂钩,其句柄表状态极其敏感。曾有学生用 MFC 封装控制台,结果在 hookNtCreateFile时发现目标进程的hFile句柄值异常跳变,排查三天才发现是 MFC 启动时悄悄打开了C:\Windows\WinSxS\...下的 manifest 文件,污染了句柄索引。所以本项目坚持裸 Win32:

// MonitorCtrl.cpp 核心初始化片段 int main(int argc, char* argv[]) { HANDLE hDriver = CreateFileA( "\\\\.\\VirusGuardFilter", // 与驱动约定的设备名 GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL ); if (hDriver == INVALID_HANDLE_VALUE) { printf("ERR: Failed to open driver. Check if installed & signed.\n"); return -1; } // 发送控制指令:启用进程监控 DWORD bytesReturned; BOOL bRet = DeviceIoControl( hDriver, IOCTL_ENABLE_PROCESS_MONITOR, // 自定义控制码,定义在 driver.h 中 &enableFlag, sizeof(enableFlag), NULL, 0, &bytesReturned, NULL ); // ... 后续日志轮询逻辑 }

提示:CreateFileA的设备路径必须带\\.\前缀,且设备名VirusGuardFilter必须与驱动中IoCreateDevice创建的设备对象名完全一致(大小写敏感)。这是用户态与内核态建立通信的第一道门,错一个字符就INVALID_HANDLE_VALUE。

2.2 内核态 minifilter 驱动:为什么不用 legacy filter 或 SSDT Hook

项目驱动部分命名为VirusGuard.sys,采用minifilter 模型(而非老式的 legacy file system filter),这是 Win10 1809 之后微软强制推荐的方式。关键区别在于:minifilter 由 Filter Manager 统一调度,天然支持多实例、优先级排序、安全上下文隔离;而 legacy filter 需要自己解析 IRP、处理重入、管理引用计数,稍有不慎就蓝屏。本项目中VirusGuard.sys的FLT_REGISTRATION结构体明确定义了三个回调点:

回调类型注册函数监控目标典型触发场景
PreOperationPreCreateIRP_MJ_CREATE任意进程调用CreateFileW打开文件前
PostOperationPostCreateIRP_MJ_CREATE文件句柄成功返回后,可检查IoStatus.Information是否为FILE_OPENED
PreOperationPreSetInformationIRP_MJ_SET_INFORMATION进程调用SetFileInformationByHandle修改文件时间戳等元数据

这些不是凭空写的——全部对应 Windows Driver Kit (WDK) 10.0.22621 文档中FltRegisterFilter的标准用法。驱动入口DriverEntry中最关键的两行是:

// VirusGuard.c NTSTATUS DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath) { FLT_REGISTRATION filterRegistration = { sizeof(FLT_REGISTRATION), // Size FLT_REGISTRATION_VERSION, // Version 0, // Flags ContextRegistration, // Context Filters, // FilterList DriverUnload, // DriverUnload InstanceSetup, // InstanceSetup InstanceQueryTeardown, // InstanceQueryTeardown InstanceTeardownStart, // InstanceTeardownStart InstanceTeardownComplete, // InstanceTeardownComplete PreCreate, // GenerateCallbacks PostCreate, // GenerateCallbacks NULL, // OperationRegistration (we use explicit callbacks) NULL, // MiniFilterUnload NULL, // InstanceSetupEx NULL // InstanceQueryTeardownEx }; return FltRegisterFilter(DriverObject, &filterRegistration, &gFilterHandle); }

注意:OperationRegistration字段设为NULL,表示我们不使用全局操作注册表,而是对每个 IRP 类型单独绑定回调函数(如PreCreate)。这是为了精准控制——比如只监控.exe和.dll文件的创建,跳过.tmp或.log,避免日志爆炸。Filters数组里定义了FLT_OPERATION_REGISTRATION结构,其中MajorFunction字段必须精确匹配IRP_MJ_*常量,写成0x00000018这种十六进制数会编译失败。

2.3 用户态与内核态通信:用命名设备对象 + IOCTL,而非共享内存或事件

通信机制选择DeviceIoControl而非MapViewOfFile或KeSetEvent,原因在于安全性与调试友好性。共享内存需处理同步、权限、生命周期,而DeviceIoControl天然具备请求-响应模型,驱动端可对每个 IOCTL 做独立鉴权(比如只允许 SYSTEM 进程发IOCTL_ENABLE_REGISTRY_AUDIT)。项目中定义了 7 个自定义 IOCTL:

IOCTL 名称功能权限要求典型参数结构
IOCTL_ENABLE_PROCESS_MONITOR开启进程创建通知SeDebugPrivilegeBOOL*
IOCTL_SET_BLOCK_LIST设置进程黑名单(按镜像名)SeTcbPrivilegeBLOCK_LIST_INFO*(含字符串数组)
IOCTL_GET_LOG_ENTRIES获取最近 100 条监控日志SeSecurityPrivilegeLOG_BUFFER*(输出缓冲区)

驱动端处理IOCTL_SET_BLOCK_LIST的核心逻辑如下:

// VirusGuard.c - 在 DispatchControl 函数中 case IOCTL_SET_BLOCK_LIST: { BLOCK_LIST_INFO* pBlockInfo = (BLOCK_LIST_INFO*)irpStack->Parameters.DeviceIoControl.Type3InputBuffer; if (!pBlockInfo || irpStack->Parameters.DeviceIoControl.InputBufferLength < sizeof(BLOCK_LIST_INFO)) { status = STATUS_INVALID_PARAMETER; break; } // 关键:必须验证用户传入的字符串是否在用户空间有效,防止驱动访问非法地址 if (!MmIsAddressValid(pBlockInfo->pImageNames) || !MmIsAddressValid((PUCHAR)pBlockInfo->pImageNames + pBlockInfo->Count * MAX_PATH)) { status = STATUS_ACCESS_VIOLATION; break; } // 复制到内核池(非分页池!因为可能在 DPC 中访问) PVOID pKernelCopy = ExAllocatePoolUninitialized(NonPagedPoolNx, pBlockInfo->Count * MAX_PATH); if (!pKernelCopy) { status = STATUS_INSUFFICIENT_RESOURCES; break; } // 使用 ProbeForRead 防御恶意用户态传入伪造指针 __try { ProbeForRead(pBlockInfo->pImageNames, pBlockInfo->Count * MAX_PATH, 1); RtlCopyMemory(pKernelCopy, pBlockInfo->pImageNames, pBlockInfo->Count * MAX_PATH); } __except(EXCEPTION_EXECUTE_HANDLER) { ExFreePool(pKernelCopy); status = GetExceptionCode(); break; } // 更新全局黑名单链表(自旋锁保护) KeAcquireSpinLock(&gBlockListLock, &oldIrql); // ... 插入逻辑 KeReleaseSpinLock(&gBlockListLock, oldIrql); break; }

血泪经验:ProbeForRead不是可选项,是必选项。曾有学生省略这步,用RtlCopyMemory直接拷贝用户地址,结果当用户传入0x00000001这种非法地址时,驱动在ExAllocatePool后立即 BSOD(错误代码IRQL_NOT_LESS_OR_EQUAL)。ProbeForRead会在当前 IRQL 下验证地址有效性,抛出结构化异常,让__except块捕获并安全退出。


3. 编译与安装全流程:从 WDK 环境配置到驱动签名绕过(仅限测试环境)

拿到源码包后,90% 的人卡在第一步:编译不过。不是代码问题,而是环境没对齐。本节给出在 Windows 11 22H2(Build 22621)上 100% 可复现的步骤,所有路径、版本号、命令均来自实测。

3.1 WDK 10.0.22621 + VS2022 环境搭建:为什么不能用 WDK 23H2 或 VS2019

WDK 版本必须与目标系统 Build 号严格匹配。22621对应 Win11 22H2,若强行用 WDK 23H2(25398)编译,fltmgr.lib符号版本不兼容,链接时报LNK2001: unresolved external symbol FltRegisterFilter。VS2022 是因为 WDK 22621 官方只提供 VS2022 的项目模板(.vcxproj.filters),VS2019 加载会报“项目格式不受支持”。安装顺序必须是:

  1. 先装Visual Studio 2022 Community(勾选“使用 C++ 的桌面开发”工作负载)
  2. 再装Windows Driver Kit (WDK) 10.0.22621.1(官网下载 ISO,运行wdksetup.exe)
  3. 最后装Windows SDK 10.0.22621.0(WDK 安装器会提示,勿跳过)

验证是否成功:打开 VS2022 → 新建项目 → 搜索 “minifilter” → 应出现 “Minifilter Driver (KMDF)” 模板。若没有,说明 WDK 未正确集成到 VS。

3.2 驱动编译:解决error LNK2001: unresolved external symbol的三个关键点

编译VirusGuard.sys时最常见的错误是链接失败。以下是三个必查项:

第一,Target Platform Version 必须设为 10.0.22621.0
右键驱动项目 → 属性 → 常规 → Target Platform Version → 手动输入10.0.22621.0(不能选下拉列表里的10.0,那会默认取最新 SDK)。否则fltmgr.lib导入库版本错配。

第二,附加依赖项必须包含fltmgr.lib且顺序正确
属性 → 链接器 → 输入 → 附加依赖项 → 添加fltmgr.lib,并确保它在ntoskrnl.lib之后。因为FltRegisterFilter依赖ntoskrnl的底层例程,链接器顺序错了会找不到符号。

第三,预处理器定义必须加/D "FLTP_DEPRECATE_WDM"
属性 → C/C++ → 预处理器 → 预处理器定义 → 新增FLTP_DEPRECATE_WDM。这是 WDK 22621 的强制要求,禁用旧版 WDM 接口,否则FltStartFiltering等函数声明冲突。

编译成功后,输出目录(如x64\Debug\)下会生成:

  • VirusGuard.sys(驱动二进制)
  • VirusGuard.inf(安装信息文件)
  • VirusGuard.cat(数字签名目录,初始为空)

3.3 驱动安装:如何绕过 Windows 驱动签名强制(仅限本地测试)

Windows 10/11 默认禁止加载未签名驱动。生产环境必须用 EV 证书签名,但学习测试可用以下两种方法(任选其一):

方法一:禁用驱动签名强制(重启生效,影响全系统)
以管理员身份运行 CMD:

bcdedit /set loadoptions DISABLE_INTEGRITY_CHECKS bcdedit /set testsigning ON shutdown /r /t 0

重启后桌面右下角会出现“测试模式”水印。此时可直接用devcon.exe安装:

devcon.exe install VirusGuard.inf Root\VirusGuardFilter

方法二:使用 Test-Sign(推荐,仅本次生效)
无需重启,但每次开机需重新执行:

# PowerShell 管理员模式 Set-ExecutionPolicy RemoteSigned -Scope CurrentUser Import-Module "$env:WDKContentRoot\Tools\Scripts\DriverTest\DriverTest.psm1" New-TestCertificate -Subject "CN=VirusGuardTest" -IsCA $true -ExportPath "C:\Temp\root.cer" New-TestCertificate -Subject "CN=VirusGuardDriver" -IsCA $false -SignerCertPath "C:\Temp\root.cer" -ExportPath "C:\Temp\driver.pfx" SignTool sign /fd SHA256 /a /f "C:\Temp\driver.pfx" /p "password" "VirusGuard.sys"

然后双击VirusGuard.inf→ “安装” → 若弹出“Windows 无法验证此驱动程序的发布者”,点“始终安装此驱动程序”。

避坑 / 常见问题 / 排查
现象 1:devcon install报错 “The system cannot find the file specified”
原因:VirusGuard.inf中[SourceDisksFiles]段落的文件路径写死了,如VirusGuard.sys = 1表示从磁盘 1 安装,但实际sys文件不在 INF 同目录。
解决:用记事本打开VirusGuard.inf,找到[SourceDisksFiles],改为VirusGuard.sys = .(点号代表当前目录),保存后重试。

现象 2:驱动安装成功,但MonitorCtrl.exe调用CreateFileA返回INVALID_HANDLE_VALUE,GetLastError()为 2(文件未找到)
原因:设备名不匹配。驱动中IoCreateDevice创建的是\Device\VirusGuardFilter,但用户态CreateFileA写的是\\.\VirusGuardFilter(缺了\\.\前缀)或\\.\VirusGuard(少Filter后缀)。
解决:统一为\\.\VirusGuardFilter,并在驱动DriverEntry中确认RtlInitUnicodeString(&deviceName, L"\\Device\\VirusGuardFilter")。

现象 3:驱动加载后,系统立即蓝屏,STOP 0x0000007E(SYSTEM_THREAD_EXCEPTION_NOT_HANDLED)
原因:PreCreate回调中调用了FltGetFileNameInformation但未检查返回值,或对FILE_NAME_NORMALIZED模式返回的FileName指针做了非法解引用。
解决:所有FltXxxAPI 调用后必须加NT_SUCCESS(status)判断,且FileName使用前用FltParseFileNameInformation解析,不可直接wcsstr(FileName->Name.Buffer, L".exe")。

现象 4:MonitorCtrl.exe能打开设备,但DeviceIoControl发送IOCTL_ENABLE_PROCESS_MONITOR后无响应,程序卡死
原因:驱动中DispatchControl函数未对IOCTL_ENABLE_PROCESS_MONITOR设置IoMarkIrpPending,导致 IRP 同步等待超时。
解决:在DispatchControl中,对所有异步 IOCTL(尤其是涉及耗时操作的),必须调用IoMarkIrpPending(Irp); return STATUS_PENDING;,并在完成例程中设置Irp->IoStatus.Status。

现象 5:日志文件monitor.log生成了,但内容全是乱码或空行
原因:RtlUnicodeStringToAnsiString转换时未初始化ANSI_STRING结构体,或ExFreePool释放了未分配的内存。
解决:严格按 WDK 文档模式:先RtlInitAnsiString(&ansiStr, NULL);,再RtlUnicodeStringToAnsiString(&ansiStr, &uniStr, TRUE);,最后ExFreePool(ansiStr.Buffer);。


4. 核心监控逻辑详解:进程创建拦截、文件操作审计、注册表访问记录的三套回调实现

本章直击项目最硬核部分——三个监控模块的回调函数如何编写。不是贴代码,而是讲清每行代码背后的 Windows 内核机制约束。所有逻辑均位于VirusGuard.c中,按PreCreate(文件)、ProcessNotifyRoutine(进程)、RegistryCallback(注册表)三大回调展开。

4.1 文件操作审计:PreCreate回调中如何安全提取文件路径并判断风险

PreCreate是 minifilter 中最常被滥用的回调,也是最容易引发蓝屏的地方。关键原则:绝不阻塞、绝不分配大内存、绝不调用可能分页的函数。以下是精简后的PreCreate主干逻辑:

FLT_PREOP_CALLBACK_STATUS PreCreate(_Inout_ PFLT_CALLBACK_DATA Data, _In_ PCFLT_RELATED_OBJECTS FltObjects, _Flt_CompletionContext_Outptr_ PVOID* CompletionContext) { PFLT_FILE_NAME_INFORMATION fileNameInfo = NULL; NTSTATUS status = STATUS_SUCCESS; BOOLEAN shouldBlock = FALSE; // Step 1: 获取文件名(必须用 FltGetFileNameInformation,不能用 ObQueryNameString) status = FltGetFileNameInformation(Data, FLT_FILE_NAME_NORMALIZED | FLT_FILE_NAME_QUERY_DEFAULT, &fileNameInfo); if (!NT_SUCCESS(status) || !fileNameInfo) { goto cleanup; // 不阻塞,放行 } // Step 2: 解析文件名,提取扩展名(安全方式:用 FltParseFileNameInformation) status = FltParseFileNameInformation(fileNameInfo); if (!NT_SUCCESS(status)) { goto cleanup; } // Step 3: 检查扩展名是否在黑名单(.exe, .dll, .scr) if (fileNameInfo->Extension.Length >= 2 * sizeof(WCHAR)) { WCHAR extLower[8]; RtlZeroMemory(extLower, sizeof(extLower)); // 安全复制:最多复制 4 个字符(包括 \0) USHORT copyLen = min(fileNameInfo->Extension.Length, 6); RtlCopyMemory(extLower, fileNameInfo->Extension.Buffer, copyLen); // 转小写便于比较 RtlDowncaseUnicodeString(&fileNameInfo->Extension, &fileNameInfo->Extension, FALSE); if (RtlCompareUnicodeString(&fileNameInfo->Extension, &UStrDotExe, TRUE) == 0 || RtlCompareUnicodeString(&fileNameInfo->Extension, &UStrDotDll, TRUE) == 0) { shouldBlock = TRUE; } } // Step 4: 如果需要拦截,设置拒绝状态(注意:不能直接 return STATUS_ACCESS_DENIED!) if (shouldBlock) { Data->IoStatus.Status = STATUS_ACCESS_DENIED; Data->IoStatus.Information = 0; FltCancelFileOpen(FltObjects->Instance, Data); // 显式取消 status = FLT_PREOP_COMPLETE; } cleanup: if (fileNameInfo) { FltReleaseFileNameInformation(fileNameInfo); // 必须释放!否则内存泄漏 } return status; }

参数说明:FltGetFileNameInformation的FLT_FILE_NAME_NORMALIZED标志确保返回的是完整绝对路径(如\Device\HarddiskVolume1\Windows\System32\notepad.exe),而非相对路径或 DOS 设备名。FltParseFileNameInformation是安全解析的唯一途径,它将路径拆分为Volume,Directory,FileName,Extension四部分,避免手动wcsrchr导致的越界。FltCancelFileOpen是微软官方推荐的取消方式,比直接设STATUS_ACCESS_DENIED更可靠,能确保 IRP 被正确清理。

4.2 进程创建拦截:PsSetCreateProcessNotifyRoutineEx的注册与回调陷阱

进程监控不走 minifilter,而用内核回调PsSetCreateProcessNotifyRoutineEx。它在进程创建的最后阶段(PspInsertProcess之后)触发,此时进程对象已存在,可安全获取EPROCESS。但有两个致命陷阱:

陷阱一:回调函数必须声明为DRIVER_DISPATCH类型,且不能有__declspec(dllexport)
错误写法:

// 错!会导致 BSOD:DRIVER_IRQL_NOT_LESS_OR_EQUAL VOID ProcessNotifyRoutineEx(_Inout_ PEPROCESS Process, _In_ HANDLE ProcessId, _In_opt_ PPS_CREATE_NOTIFY_INFO CreateInfo) { ... }

正确写法(必须用DRIVER_DISPATCH且加__drv_dispatchType注释):

// 正确:符合 WDK 驱动模型 _Function_class_(PS_CREATE_NOTIFY_ROUTINE_EX) _IRQL_requires_max_(PASSIVE_LEVEL) VOID ProcessNotifyRoutineEx( _Inout_ PEPROCESS Process, _In_ HANDLE ProcessId, _In_opt_ PPS_CREATE_NOTIFY_INFO CreateInfo ) { UNREFERENCED_PARAMETER(Process); if (CreateInfo == NULL) return; // 进程退出事件,忽略 // 安全获取镜像路径:用 PsGetProcessImageFileName(返回 15 字符短名)或更安全的 PsGetProcessImageFileNameEx(需 WDK 22621+) CHAR imageName[16] = {0}; PsGetProcessImageFileName(Process, imageName, sizeof(imageName)-1); // 检查是否在黑名单(如 svchost.exe 除外) if (RtlCompareMemory(imageName, "notepad.exe", 11) == 11) { // 记录日志(用自定义日志函数,非 DbgPrint) LogEntry(L"Blocked process: %S (PID:%d)", imageName, HandleToULong(ProcessId)); // 强制终止进程(注意:不能在回调中直接 Terminate,需用 PsTerminateSystemThread) HANDLE hThread = NULL; NTSTATUS st = PsLookupProcessByProcessId(ProcessId, &hThread); if (NT_SUCCESS(st)) { ZwTerminateProcess(hThread, STATUS_PROCESS_IS_TERMINATING); ObDereferenceObject(hThread); } } }

玄学细节:PsGetProcessImageFileName返回的是\\??\\C:\\Windows\\System32\\notepad.exe这种 DOS 设备路径,但长度被截断为 15 字符(含\0),所以只能用于简单匹配。若需完整路径,必须用ObReferenceObjectByHandle获取EPROCESS后,再调用PsGetProcessImageFileNameEx(WDK 22621 新增),它返回UNICODE_STRING,可安全RtlCompareUnicodeString。

4.3 注册表访问审计:CmRegisterCallbackEx的注册时机与回调过滤

注册表监控用CmRegisterCallbackEx,它比旧版CmRegisterCallback更安全,支持回调优先级和卸载。关键点在于注册必须在 DriverEntry 中完成,且不能在DriverUnload中卸载(会导致系统不稳定)。

// 全局变量 LARGE_INTEGER gCookie = {0}; CM_CALLBACK_OBJECT* gRegCallbackObj = NULL; // 回调函数 NTSTATUS RegistryCallback( _In_ PVOID CallbackContext, _In_opt_ PVOID Argument1, _In_opt_ PVOID Argument2, _In_ PVOID Argument3, _In_ PVOID Argument4, _Out_ PVOID* ReturnOutput ) { PCM_NOTIFY_ROUTINE_ARG arg = (PCM_NOTIFY_ROUTINE_ARG)Argument1; if (!arg || arg->Argument1 == NULL) return STATUS_SUCCESS; // 只监控 RegSetValueKey(写注册表)和 RegDeleteKey(删键) switch (arg->Argument1) { case RegNtPreSetValueKey: { PCM_KEY_VALUE_ARGUMENT kvArg = (PCM_KEY_VALUE_ARGUMENT)Argument2; if (kvArg && kvArg->ValueName && kvArg->ValueName->Length > 0) { // 提取键名(安全方式:用 CmGetKeyObject) UNICODE_STRING keyName; NTSTATUS st = CmGetKeyObject(arg->Argument2, &keyName); if (NT_SUCCESS(st)) { LogEntry(L"RegSetValue: %wZ -> %wZ", &keyName, kvArg->ValueName); ExFreePool(keyName.Buffer); // 必须释放 } } break; } case RegNtPreDeleteKey: { PCM_KEY_CONTROL_BLOCK kcb = (PCM_KEY_CONTROL_BLOCK)Argument2; if (kcb && kcb->FullName) { LogEntry(L"RegDeleteKey: %wZ", kcb->FullName); } break; } } return STATUS_SUCCESS; } // DriverEntry 中注册 status = CmRegisterCallbackEx( RegistryCallback, &gRegCallbackObj, &gCookie, NULL, // 无附加上下文 NULL // 无安全描述符 ); if (!NT_SUCCESS(status)) { KdPrint(("ERR: CmRegisterCallbackEx failed: 0x%08X\n", status)); return status; }

注意:CmGetKeyObject返回的UNICODE_STRING的Buffer是内核分配的,必须用ExFreePool释放,否则内存泄漏。RegNtPreSetValueKey的Argument2是PCM_KEY_VALUE_ARGUMENT,它包含ValueName(值名)和Data(数据),但Data是用户态地址,绝不能直接解引用!只能记录ValueName和DataSize,具体内容需在用户态通过DeviceIoControl二次获取。


5. 日志系统与策略管理:如何设计线程安全的日志缓冲区与动态黑名单更新

监控产生的日志若直接写磁盘,性能会崩(每秒上千次ZwWriteFile)。本项目采用双缓冲环形队列 + 用户态轮询方案,既保证实时性,又避免内核态 I/O 开销。策略管理则通过 IOCTL 实现运行时更新,无需重启驱动。

5.1 内核态日志环形缓冲区:用ExAllocatePoolUninitialized分配非分页池

日志缓冲区必须驻留在非分页池(NonPagedPoolNx),因为PreCreate等回调可能在 DISPATCH_LEVEL IRQL 下执行,此时分页内存不可访问。结构体定义如下:

#define LOG_BUFFER_SIZE 1024 #define MAX_LOG_ENTRY_LEN 512 typedef struct _LOG_ENTRY { LARGE_INTEGER Timestamp; ULONG ProcessId; UCHAR EventType; // 1=Process, 2=File, 3=Registry UCHAR Severity; // 0=Info, 1=Warn, 2=Block WCHAR Message[MAX_LOG_ENTRY_LEN]; } LOG_ENTRY, *PLOG_ENTRY; typedef struct _LOG_BUFFER { volatile LONG ReadIndex; // 原子读位置 volatile LONG WriteIndex; // 原子写位置 volatile LONG EntryCount; // 当前条目数 LOG_ENTRY Entries[LOG_BUFFER_SIZE]; } LOG_BUFFER, *PLOG_BUFFER; // 全局缓冲区指针 PLOG_BUFFER gLogBuffer = NULL; // DriverEntry 中初始化 gLogBuffer = (PLOG_BUFFER)ExAllocatePoolUninitialized(NonPagedPoolNx, sizeof(LOG_BUFFER)); if (!gLogBuffer) return STATUS_INSUFFICIENT_RESOURCES; RtlZeroMemory(gLogBuffer, sizeof(LOG_BUFFER));

写日志函数LogEntry的核心是原子操作:

VOID LogEntry(_In_z_ PCWSTR Format, ...) { if (!gLogBuffer) return; LONG writeIndex = InterlockedIncrement(&gLogBuffer->WriteIndex) % LOG_BUFFER_SIZE; PLOG_ENTRY entry = &gLogBuffer->Entries[writeIndex]; // 时间戳 KeQuerySystemTime(&entry->Timestamp); // 进程 ID(从当前线程获取) entry->ProcessId = HandleToULong(PsGetCurrentProcessId()); // 格式化字符串(安全:限制长度) va_list args; va_start(args, Format); RtlStringCbVPrintfW(entry->Message, sizeof(entry->Message), Format, args); va_end(args); // 更新计数(原子加) InterlockedIncrement(&gLogBuffer->EntryCount); }

关键点:InterlockedIncrement保证多核 CPU 下WriteIndex递增的原子性。RtlStringCbVPrintfW是安全的宽字符格式化函数,自动处理缓冲区溢出。PsGetCurrentProcessId()返回当前执行回调的进程 ID,即被监控的目标进程,不是驱动自身 PID。

5.2 用户态日志轮询:用DeviceIoControl分批读取,避免阻塞

MonitorCtrl.exe启动一个独立线程,每 500ms 轮询一次日志:

// LogPollThread 函数 DWORD WINAPI LogPollThread(LPVOID lpParam) { HANDLE hDriver = (HANDLE)lpParam; LOG_BUFFER logBuf; DWORD bytesReturned; while (gRunning) { // 发送 IOCTL 获取日志 if (DeviceIoControl( hDriver, IOCTL_GET_LOG_ENTRIES, NULL, 0, &logBuf, sizeof(logBuf), &bytesReturned, NULL )) { // 成功读取,遍历 Entries for (int i = 0; i < LOG_BUFFER_SIZE && logBuf.Entries[i].Message[0]; i++) { wprintf(L"[%.3f] PID:%d %s\n", (double)logBuf.Entries[i].Timestamp.QuadPart / 10000000.0, logBuf.Entries[i].ProcessId, logBuf.Entries[i].Message); } } Sleep(500); } return 0; }

驱动端IOCTL_GET_LOG_ENTRIES的处理逻辑是原子快照:

case IOCTL_GET_LOG_ENTRIES: { PLOG_BUFFER userBuf = (PLOG_BUFFER)irpStack->Parameters.DeviceIoControl.OutputBuffer; if (!userBuf) { status = STATUS_INVALID_PARAMETER; break; } // 原子读取当前状态 LONG readIdx = InterlockedReadNoFence(&gLogBuffer->ReadIndex); LONG writeIdx = InterlockedReadNoFence(&gLogBuffer->WriteIndex); LONG count = InterlockedReadNoFence(&gLogBuffer-> <p> <a href="https://download.csdn.net/download/zyhhfz/9384124" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>

相关新闻

ArcGIS符号库本质:CIM驱动的空间语义编码系统

ArcGIS符号库本质:CIM驱动的空间语义编码系统

简介&#xff1a;本资源为ArcGIS专业制图用户必备的符号库集成包&#xff0c;面向GIS工程师、地图制图师及地理信息相关专业学习者&#xff0c;解决日常项目中符号缺失、风格不统一、制图效率低等实际问题。压缩包共142个文件&#xff0c;涵盖74个.style符号样式文件&#xff0…

2026/10/20 23:14:43 阅读更多 →
Java线程中断与Happens-Before:并发编程可见性规则实战解析

Java线程中断与Happens-Before:并发编程可见性规则实战解析

1. 线程中断&#xff1a;协作式取消的正确打开方式先把结论摆在这里&#xff1a;Java里的线程中断不是“强制终止线程”&#xff0c;而是一个协作式的通知机制。也就是说&#xff0c;A线程想停掉B线程&#xff0c;不能直接动手把B拽下来&#xff0c;而是给B打个招呼——“你该停…

2026/10/20 23:14:43 阅读更多 →
Linux Thermal温控模块详解:从框架设计到设备树配置实战

Linux Thermal温控模块详解:从框架设计到设备树配置实战

1. 项目概述1.1 为什么要啃Linux Thermal模块做嵌入式或者跑服务器的朋友应该都有这种体会&#xff1a;芯片性能越来越强&#xff0c;功耗密度越来越大&#xff0c;散热问题已经从“硬件工程师的烦恼”变成了“系统工程师的噩梦”。我自己手头负责的一块主控板&#xff0c;四核…

2026/10/20 23:14:43 阅读更多 →

最新新闻

AF_XDP 实战指南:Ubuntu 24.04 上基于 eBPF 的高性能包处理

AF_XDP 实战指南:Ubuntu 24.04 上基于 eBPF 的高性能包处理

1. 项目概述&#xff1a;AF_XDP 到底解决什么问题碰上 AF_XDP 这个技术点的人&#xff0c;多半是已经在 Linux 上做高性能包处理&#xff0c;或者被 DPDK 的运维成本折腾过之后&#xff0c;想在内核生态里找个折中方案。AF_XDP 不是一个框架&#xff0c;而是 Linux 内核提供的地…

2026/10/20 23:41:33 阅读更多 →
用Unity打造桌面精灵:透明窗口、鼠标穿透与动画实战

用Unity打造桌面精灵:透明窗口、鼠标穿透与动画实战

简介&#xff1a;这是一份基于Unity引擎开发桌面精灵的完整工程资源&#xff0c;面向希望实现类似QQ宠物透明浮动桌面形象的Unity开发者。资源围绕窗口无边界设置、透明材质渲染、UI布局、Animator动画控制以及鼠标交互事件等关键技术点组织&#xff0c;适合有一定Unity基础、想…

2026/10/20 23:41:33 阅读更多 →
GLSL变量与类型系统详解:从标量到矩阵的Shader避坑指南

GLSL变量与类型系统详解:从标量到矩阵的Shader避坑指南

如果你写过一段时间的着色器&#xff0c;大概率遇到过这种时刻&#xff1a;shader编译失败&#xff0c;错误信息指向某个变量&#xff0c;你盯着那一行看了很久&#xff0c;愣是没发现哪里有问题。我印象最深的一次是在某个延迟渲染demo里&#xff0c;片元着色器把法线数据当成…

2026/10/20 23:41:33 阅读更多 →
C语言是什么?从底层原理到上手路径的全面解析

C语言是什么?从底层原理到上手路径的全面解析

编程这行干久了&#xff0c;总会被外行朋友或刚入行的新人问到同一个问题&#xff1a;“C语言到底是什么&#xff1f;” 每次被问到&#xff0c;我都会愣一下&#xff0c;因为这个问题看似简单&#xff0c;真要一两句话说清楚还挺难。它不像Java能“写安卓APP”&#xff0c;也不…

2026/10/20 23:41:33 阅读更多 →
好写作AI如何用游戏化设计破解写作恐惧,养成写作习惯

好写作AI如何用游戏化设计破解写作恐惧,养成写作习惯

1. 好写作AI到底在做什么&#xff1a;把“写作”变成“练级”1.1 它解决了什么问题&#xff1a;写作恐惧不是懒&#xff0c;是“防御”我见过太多人说自己“不会写”“写不出来”&#xff0c;包括我自己也曾是这样。面对一个空白文档&#xff0c;光标一开一合&#xff0c;脑子里…

2026/10/20 23:41:33 阅读更多 →
华三AC WX5560H AP信道优化实战:解决信号满格却卡顿

华三AC WX5560H AP信道优化实战:解决信号满格却卡顿

前阵子在某园区处理无线投诉时&#xff0c;我碰到一个很典型的场景&#xff1a;AP 装了一堆、信号显示也是满格&#xff0c;但视频会议就是动不动卡成 PPT。当时很多人第一反应是带宽不够&#xff0c;可跑到华三 AC WX5560H 上一看&#xff0c;控制器负载很低&#xff0c;出口带…

2026/10/20 23:40:32 阅读更多 →

日新闻

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →
ESP32舵机控制指南:原理、多路驱动与避坑实战

ESP32舵机控制指南:原理、多路驱动与避坑实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →
位移法实现一阶低通滤波:单片机ADC采样降噪实战

位移法实现一阶低通滤波:单片机ADC采样降噪实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型&#xff08;GMM&#xff09;的语音识别系统&#xff0c;很多人第一反应就是一堆数学公式&#xff0c;觉得门槛很高。但如果你真的在MATLAB里把它落地跑通&#xff0c;会发现核心其实就是三件事&#xff1a;把声音变成特征向量&#xff0c;用GMM给每个类别…

2026/10/20 10:02:13 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人&#xff0c;但落到具体操作上&#xff0c;无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程&#xff1a;对图像做反白、改调色板、彩色转灰度&#xff0c;最后把像素数据导出成txt文件。单看每一…

2026/10/20 20:43:59 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词&#xff0c;你会发现一个很有意思的现象&#xff1a;每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型&#xff0c;有人指的是运动控制系统&#xff0c;还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/20 2:50:50 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 10:02:08 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 9:50:17 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 9:50:45 阅读更多 →