第三十章:综合实战项目——开发一个完整的内核监控驱动
终于到了最后一章。说实话,写到这里我自己都有点感慨。前面二十九章我们一步步拆解了进程、文件、网络、注册表这四个监控方向,现在要把它们全部整合到一起,做成一个真正能跑的内核监控驱动。
这个项目我去年在公司内部做过一个类似的版本,当时是为了监控内部员工的可疑操作。嗯,踩了不少坑,今天我把这些经验都揉进这个项目里。
项目整体架构
先看整体设计。这个驱动不是简单地把四个模块拼在一起,而是需要一套统一的管理机制。我个人习惯把架构分成三层:
- 回调注册层:负责向系统注册各类监控回调
- 事件处理层:统一处理回调数据,过滤、格式化
- 通信输出层:通过 IOCTL 与用户态通信,输出监控日志
核心思路:所有监控事件走同一个环形缓冲区,用户态程序定期读取。这样既避免了频繁上下文切换,又不会丢事件。
数据结构设计
所有监控事件共用同一个结构体。为什么?因为用户态程序处理起来方便,不需要为每种事件写不同的解析代码。
// 统一事件结构
typedef struct _MONITOR_EVENT {
UINT32 EventType; // 0=进程 1=文件 2=网络 3=注册表
UINT32 ProcessId; // 触发事件的进程ID
UINT64 Timestamp; // 高精度时间戳
UINT32 DataSize; // 事件数据长度
UINT8 Data[256]; // 事件具体数据(变长)
} MONITOR_EVENT, *PMONITOR_EVENT;
// 环形缓冲区
#define RING_BUFFER_SIZE 4096
typedef struct _RING_BUFFER {
volatile LONG Head;
volatile LONG Tail;
KSPIN_LOCK Lock;
MONITOR_EVENT Events[RING_BUFFER_SIZE];
} RING_BUFFER, *PRING_BUFFER;
个人经验:环形缓冲区的大小要根据实际场景调整。我之前在一个高频交易系统上做过监控,4096 个槽位根本不够,后来改成了 65536。但普通办公场景 4096 绰绰有余。
进程监控模块
进程监控是最基础的。我用 PsSetCreateProcessNotifyRoutine 注册回调,记录进程创建和退出。
VOID ProcessNotifyCallback(
HANDLE ParentId,
HANDLE ProcessId,
BOOLEAN Create
) {
MONITOR_EVENT event = {0};
event.EventType = 0;
event.ProcessId = (UINT32)ProcessId;
KeQuerySystemTime(&event.Timestamp);
if (Create) {
// 获取进程文件名
PEPROCESS process;
PsLookupProcessByProcessId(ProcessId, &process);
// ... 获取文件名写入 event.Data
ObDereferenceObject(process);
}
PushToRingBuffer(&event);
}
注意:在进程回调中获取文件名要小心。新创建的进程可能还没完全初始化,强行获取会导致蓝屏。我建议加一个 100ms 的延迟再获取,或者干脆只记录 PID,让用户态程序自己去查。
文件监控模块
文件监控用微过滤器驱动(Minifilter)。这里我只监控 IRP_MJ_CREATE 和 IRP_MJ_WRITE,因为这两个操作最能反映文件访问行为。
FLT_PREOP_CALLBACK_STATUS
FilePreCreateCallback(
PFLT_CALLBACK_DATA Data,
PCFLT_RELATED_OBJECTS FltObjects,
PVOID *CompletionContext
) {
MONITOR_EVENT event = {0};
event.EventType = 1;
event.ProcessId = (UINT32)PsGetCurrentProcessId();
KeQuerySystemTime(&event.Timestamp);
// 获取文件路径
FltGetFileNameInformation(
Data,
FLT_FILE_NAME_NORMALIZED,
&nameInfo
);
// 复制路径到 event.Data
PushToRingBuffer(&event);
return FLT_PREOP_SUCCESS_WITH_CALLBACK;
}
网络监控模块
网络监控我用 WFP(Windows Filtering Platform)。说白了我只监控出站连接,因为内部员工的数据泄露风险主要在外发。
VOID NetworkClassifyCallback(
const FWPS_INCOMING_VALUES *inFixedValues,
const FWPS_INCOMING_METADATA_VALUES *inMetaValues,
void *layerData,
const void *classifyContext,
const FWPS_FILTER *filter,
UINT64 flowContext,
FWPS_CLASSIFY_OUT *classifyOut
) {
MONITOR_EVENT event = {0};
event.EventType = 2;
event.ProcessId = (UINT32)inMetaValues->processId;
KeQuerySystemTime(&event.Timestamp);
// 提取远程IP和端口
// 写入 event.Data
PushToRingBuffer(&event);
}
避坑指南:WFP 的回调运行在 DPC 级别,不能调用任何分页内存操作。我曾经在这里犯过错,在回调里用了 ExAllocatePoolWithTag,结果系统直接蓝屏。记住:DPC 里只能用 NonPagedPool。
注册表监控模块
注册表监控用 CmRegisterCallbackEx。我主要监控关键路径的修改,比如 HKLM\SYSTEM\CurrentControlSet\Services 下的驱动启动项。
NTSTATUS RegistryCallback(
PVOID CallbackContext,
PVOID Argument1,
PVOID Argument2
) {
REG_NOTIFY_CLASS notifyClass = (REG_NOTIFY_CLASS)Argument1;
if (notifyClass == RegNtPreSetValueKey) {
PREG_SET_VALUE_KEY_INFORMATION info =
(PREG_SET_VALUE_KEY_INFORMATION)Argument2;
MONITOR_EVENT event = {0};
event.EventType = 3;
event.ProcessId = (UINT32)PsGetCurrentProcessId();
KeQuerySystemTime(&event.Timestamp);
// 记录键路径和值
PushToRingBuffer(&event);
}
return STATUS_SUCCESS;
}
IOCTL 通信接口
用户态程序通过 DeviceIoControl 读取事件。我设计了两个主要 IOCTL 码:
| IOCTL 码 | 功能 | 输入 | 输出 |
|---|---|---|---|
| IOCTL_GET_EVENT | 读取一个事件 | 无 | MONITOR_EVENT |
| IOCTL_CLEAR_BUFFER | 清空缓冲区 | 无 | 无 |
| IOCTL_GET_STATS | 获取统计信息 | 无 | 统计结构体 |
NTSTATUS DispatchIoctl(
PDEVICE_OBJECT DeviceObject,
PIRP Irp
) {
PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp);
ULONG ioctlCode = stack->Parameters.DeviceIoControl.IoControlCode;
switch (ioctlCode) {
case IOCTL_GET_EVENT:
// 从环形缓冲区取出一个事件
PopFromRingBuffer(&event);
// 复制到用户输出缓冲区
break;
case IOCTL_CLEAR_BUFFER:
// 重置环形缓冲区头尾指针
break;
}
Irp->IoStatus.Status = STATUS_SUCCESS;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return STATUS_SUCCESS;
}
驱动入口与卸载
驱动入口要做的事情很多:注册所有回调、创建设备对象、初始化环形缓冲区。卸载时则要按相反顺序清理。
NTSTATUS DriverEntry(
PDRIVER_OBJECT DriverObject,
PUNICODE_STRING RegistryPath
) {
// 1. 初始化环形缓冲区
RingBufferInit(&g_RingBuffer);
// 2. 创建设备对象和符号链接
IoCreateDevice(/* ... */);
IoCreateSymbolicLink(/* ... */);
// 3. 注册进程监控
PsSetCreateProcessNotifyRoutine(ProcessNotifyCallback, FALSE);
// 4. 注册文件监控(Minifilter)
FltRegisterFilter(DriverObject, &Registration, &g_FilterHandle);
// 5. 注册网络监控(WFP)
FwpsCalloutRegister(/* ... */);
// 6. 注册注册表监控
CmRegisterCallbackEx(RegistryCallback, /* ... */);
DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DispatchIoctl;
DriverObject->DriverUnload = DriverUnload;
return STATUS_SUCCESS;
}
VOID DriverUnload(PDRIVER_OBJECT DriverObject) {
// 按注册的逆序卸载
CmUnRegisterCallback(g_RegCookie);
FwpsCalloutUnregisterById(/* ... */);
FltUnregisterFilter(g_FilterHandle);
PsSetCreateProcessNotifyRoutine(ProcessNotifyCallback, TRUE);
IoDeleteSymbolicLink(&g_SymLink);
IoDeleteDevice(DriverObject->DeviceObject);
}
重要提醒:卸载顺序不能乱。我曾经先卸载了进程回调,再卸载 Minifilter,结果 Minifilter 卸载时触发了进程回调,导致系统崩溃。记住:先注册的后卸载,后注册的先卸载。
用户态测试程序
写一个简单的控制台程序来验证驱动是否正常工作:
int main() {
HANDLE hDevice = CreateFile(
L"\\\\.\\MonitorDriver",
GENERIC_READ | GENERIC_WRITE,
0, NULL, OPEN_EXISTING, 0, NULL
);
MONITOR_EVENT event;
DWORD bytesReturned;
while (TRUE) {
BOOL result = DeviceIoControl(
hDevice,
IOCTL_GET_EVENT,
NULL, 0,
&event, sizeof(event),
&bytesReturned,
NULL
);
if (result) {
PrintEvent(&event);
}
Sleep(100); // 避免忙等
}
CloseHandle(hDevice);
return 0;
}
这个综合项目涵盖了 Windows 内核监控的四大核心领域。你想想看,把这些模块整合到一起,就是一个企业级监控软件的雏形。我在实际项目中,还加了事件过滤规则和告警阈值,但核心框架就是上面这些。
嗯,到这里《C语言Windows驱动开发完全实战》的三十章内容就全部结束了。从最简单的 Hello World 驱动,到现在的综合监控驱动,这条路走下来不容易。希望你能把这些代码真正跑起来,遇到问题多调试,多分析蓝屏日志——相信我,每个蓝屏都是一次学习机会。
最后说一句:驱动开发没有捷径,但也没有想象中那么难。保持耐心,保持好奇,你一定能写出稳定可靠的内核程序。
公众号:蓝海资料掘金营,微信deep3321