第三十章:综合实战项目——开发一个完整的内核监控驱动

终于到了最后一章。说实话,写到这里我自己都有点感慨。前面二十九章我们一步步拆解了进程、文件、网络、注册表这四个监控方向,现在要把它们全部整合到一起,做成一个真正能跑的内核监控驱动。

这个项目我去年在公司内部做过一个类似的版本,当时是为了监控内部员工的可疑操作。嗯,踩了不少坑,今天我把这些经验都揉进这个项目里。

项目整体架构

先看整体设计。这个驱动不是简单地把四个模块拼在一起,而是需要一套统一的管理机制。我个人习惯把架构分成三层:

  • 回调注册层:负责向系统注册各类监控回调
  • 事件处理层:统一处理回调数据,过滤、格式化
  • 通信输出层:通过 IOCTL 与用户态通信,输出监控日志

核心思路:所有监控事件走同一个环形缓冲区,用户态程序定期读取。这样既避免了频繁上下文切换,又不会丢事件。

内核监控驱动整体架构 回调注册层 PsSetCreateProcessNotifyRoutine FltRegisterFilter CmRegisterCallbackEx FwpsCalloutRegister PsSetCreateThreadNotifyRoutine 事件处理层 事件过滤 → 格式化 → 入队 环形缓冲区 (Ring Buffer) 通信输出层 IOCTL 接口 事件读取 驱动卸载清理 用户态监控程序 (Console/WPF)

数据结构设计

所有监控事件共用同一个结构体。为什么?因为用户态程序处理起来方便,不需要为每种事件写不同的解析代码。

// 统一事件结构
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