第二十一章:文件系统过滤:Minifilter框架、文件操作监控、文件加密与保护

文件系统过滤,说白了就是在文件读写路径上插一脚。你想想看,每次应用程序读写文件,都要经过I/O管理器,再往下走就是文件系统驱动。如果我们能在中间加一层,就能看到所有文件操作,甚至能改数据、拦操作。这就是Minifilter干的事。

我最早接触文件过滤驱动,还是用传统的legacy filter。那玩意儿写起来真叫一个痛苦——你得自己处理IRP的挂载、转发、完成,稍不留神就蓝屏。后来微软推出了Minifilter框架,嗯,这简直就是救星。它把那些脏活累活都封装好了,我们只需要写几个回调函数就行。

21.1 Minifilter框架的核心概念

Minifilter是Windows文件系统过滤的新一代框架。它基于Filter Manager (FltMgr.sys) 工作。Filter Manager负责管理所有Minifilter的加载、卸载、通信,以及IRP的派发。

我个人习惯把Minifilter想象成一个"插件系统"。Filter Manager是主程序,我们写的驱动就是插件。插件只需要注册感兴趣的操作,比如只监控IRP_MJ_CREATE,其他操作Filter Manager会自动帮我们处理。

关键点:Minifilter的加载顺序由Altitude决定。Altitude是一个数字,数字越小越靠近底层文件系统。比如加密驱动通常用低Altitude(30xxx),监控驱动用高Altitude(36xxx)。

这里有个我踩过的坑:Altitude不是随便写的。微软给每个功能类别分配了区间,乱写会导致驱动加载失败。我曾经图省事随便写了个数字,结果驱动死活加载不上,查了半天才发现是Altitude冲突。

Minifilter框架架构图 用户模式 (User Mode) 应用程序 (notepad.exe, cmd.exe, ...) 内核模式 (Kernel Mode) I/O Manager (IopXxxControlFile) Filter Manager (FltMgr.sys) Minifilter A (监控) Minifilter B (加密) Minifilter C (防病毒) 文件系统 (NTFS.sys / FastFat.sys)

21.2 注册与回调机制

写一个Minifilter驱动,核心就是注册回调。Filter Manager提供了两种回调:

  • Pre-operation回调:在IRP到达文件系统之前触发。你可以在这里拦截、修改、甚至拒绝操作。
  • Post-operation回调:在文件系统处理完IRP之后触发。适合做监控、日志记录。

注册回调的代码大概长这样:

// 定义回调函数
FLT_PREOP_CALLBACK_STATUS
MyPreCreateCallback(
    _Inout_ PFLT_CALLBACK_DATA Data,
    _In_ PCFLT_RELATED_OBJECTS FltObjects,
    _Flt_CompletionContext_Outptr_ PVOID *CompletionContext
)
{
    UNREFERENCED_PARAMETER(CompletionContext);
    
    // 获取文件名
    PFLT_FILE_NAME_INFORMATION nameInfo;
    FltGetFileNameInformation(Data, FLT_FILE_NAME_NORMALIZED, &nameInfo);
    
    // 打印文件名
    DbgPrint("文件创建操作: %wZ\n", &nameInfo->Name);
    
    FltReleaseFileNameInformation(nameInfo);
    return FLT_PREOP_SUCCESS_WITH_CALLBACK;
}

// 注册回调
CONST FLT_OPERATION_REGISTRATION Callbacks[] = {
    { IRP_MJ_CREATE, 0, MyPreCreateCallback, MyPostCreateCallback },
    { IRP_MJ_READ,   0, MyPreReadCallback,   NULL },
    { IRP_MJ_WRITE,  0, MyPreWriteCallback,  NULL },
    { IRP_MJ_CLEANUP,0, NULL,                MyPostCleanupCallback },
    { IRP_MJ_OPERATION_END }
};

小技巧:Pre回调返回FLT_PREOP_SUCCESS_WITH_CALLBACK,表示我还想收到Post回调。如果返回FLT_PREOP_SUCCESS,Post回调就不会被调用。我一般监控操作都用WITH_CALLBACK,这样前后都能处理。

21.3 文件操作监控实战

文件监控是最常见的Minifilter应用场景。你想监控谁在什么时候改了哪个文件,写个Minifilter就搞定了。

我记得有个项目,客户说服务器上的配置文件总被人改,但不知道是谁干的。我写了个Minifilter,监控IRP_MJ_WRITE操作,把进程名、文件名、时间戳全记下来。结果发现是运维脚本有个bug,半夜自动跑的时候写错了路径。

监控的核心逻辑:

FLT_PREOP_CALLBACK_STATUS
MyPreWriteCallback(
    _Inout_ PFLT_CALLBACK_DATA Data,
    _In_ PCFLT_RELATED_OBJECTS FltObjects,
    _Flt_CompletionContext_Outptr_ PVOID *CompletionContext
)
{
    // 获取进程ID和进程名
    HANDLE pid = PsGetCurrentProcessId();
    PEPROCESS process = PsGetCurrentProcess();
    
    // 获取文件名
    PFLT_FILE_NAME_INFORMATION nameInfo;
    NTSTATUS status = FltGetFileNameInformation(
        Data, 
        FLT_FILE_NAME_NORMALIZED | FLT_FILE_NAME_QUERY_DEFAULT,
        &nameInfo
    );
    
    if (NT_SUCCESS(status)) {
        // 解析文件名,判断是否需要监控
        if (IsTargetFile(nameInfo->Name)) {
            // 记录日志
            LogFileOperation(pid, nameInfo->Name, "WRITE");
        }
        FltReleaseFileNameInformation(nameInfo);
    }
    
    return FLT_PREOP_SUCCESS_WITH_CALLBACK;
}

注意:FltGetFileNameInformation可能会引起递归调用!因为获取文件名本身可能触发文件操作。我建议在Pre回调中设置一个递归检测标志,或者使用FLT_FILE_NAME_QUERY_DEFAULT来避免死循环。

21.4 文件加密与保护实现

文件加密是Minifilter的另一个硬核应用。原理很简单:写文件时加密数据,读文件时解密数据。但实现起来有不少坑。

加密的时机在Pre-Write回调里。文件系统要写数据了,我们先把数据加密,再放行。解密的时机在Post-Read回调里。文件系统读完了数据,我们拿到数据解密,再返回给上层。

代码骨架:

// 写加密
FLT_PREOP_CALLBACK_STATUS
MyPreWriteEncrypt(
    _Inout_ PFLT_CALLBACK_DATA Data,
    _In_ PCFLT_RELATED_OBJECTS FltObjects,
    _Flt_CompletionContext_Outptr_ PVOID *CompletionContext
)
{
    PFLT_IO_PARAMETER_BLOCK iopb = Data->Iopb;
    
    // 只加密特定目录的文件
    if (!IsProtectedDirectory(FltObjects->FileObject)) {
        return FLT_PREOP_SUCCESS_NO_CALLBACK;
    }
    
    // 获取写入缓冲区
    PMDL mdl = iopb->Parameters.Write.MdlAddress;
    PVOID buffer = MmGetSystemAddressForMdlSafe(mdl, NormalPagePriority);
    ULONG length = iopb->Parameters.Write.Length;
    
    if (buffer && length > 0) {
        // 加密数据(使用AES-256)
        EncryptBuffer(buffer, length, GetFileKey(FltObjects->FileObject));
    }
    
    return FLT_PREOP_SUCCESS_NO_CALLBACK;
}

// 读解密
FLT_POSTOP_CALLBACK_STATUS
MyPostReadDecrypt(
    _Inout_ PFLT_CALLBACK_DATA Data,
    _In_ PCFLT_RELATED_OBJECTS FltObjects,
    _Flt_CompletionContext_ PVOID CompletionContext,
    _In_ FLT_POST_OPERATION_FLAGS Flags
)
{
    if (!NT_SUCCESS(Data->IoStatus.Status)) {
        return FLT_POSTOP_FINISHED_PROCESSING;
    }
    
    PFLT_IO_PARAMETER_BLOCK iopb = Data->Iopb;
    PMDL mdl = iopb->Parameters.Read.MdlAddress;
    PVOID buffer = MmGetSystemAddressForMdlSafe(mdl, NormalPagePriority);
    ULONG length = iopb->Parameters.Read.Length;
    
    if (buffer && length > 0) {
        // 解密数据
        DecryptBuffer(buffer, length, GetFileKey(FltObjects->FileObject));
    }
    
    return FLT_POSTOP_FINISHED_PROCESSING;
}

关键设计决策:加密密钥怎么管理?我见过两种方案:

  • 文件级密钥:每个文件一个随机密钥,密钥存在文件流中。安全性高,但实现复杂。
  • 目录级密钥:整个受保护目录共用一个密钥。实现简单,适合演示。

我个人推荐生产环境用文件级密钥,配合DPAPI保护主密钥。

21.5 避坑指南

写Minifilter这些年,我踩过的坑能写一本书。这里挑几个最要命的:

  1. 递归问题:Minifilter里做文件操作,比如写日志文件,会再次触发你的回调。必须用线程本地存储或全局标志来防止递归。
  2. 分页池与非分页池:Pre回调运行在APC级别,不能使用分页内存。我习惯在DriverEntry里预分配好所有缓冲区。
  3. 文件重命名:监控IRP_MJ_SET_INFORMATION时,要处理FileRenameInformation情况。很多人只监控了CREATE和WRITE,漏掉了重命名。
  4. 缓存问题:Windows有缓存管理器。你加密了数据,但缓存里可能是明文。需要配合CcFlushCache使用。

曾经我犯过一个错误:在Post-Read回调里解密数据后,忘了更新Mdl的标记。结果上层拿到的是解密后的数据,但缓存管理器以为还是加密状态。下次读的时候,缓存命中,直接返回了加密数据。应用程序就崩溃了。解决方案是在解密后调用MmBuildMdlForNonPagedPool刷新Mdl。

21.6 性能优化建议

文件过滤驱动对性能极其敏感。你想想看,每次读写都要经过你的回调,稍微慢一点,整个系统都能感觉到。

优化点 说明 建议
回调粒度 只注册需要的IRP 不要注册IRP_MJ_OPERATION_END以外的所有操作
文件名获取 FltGetFileNameInformation开销大 使用缓存,或只在需要时获取
加密算法 AES-256软件实现较慢 使用硬件加速(AES-NI),或只加密文件头部
日志写入 每次操作都写日志 批量写入,或使用环形缓冲区

我有个项目,刚开始没注意性能,结果部署到文件服务器上,用户反馈打开文件慢了好几秒。后来优化了文件名获取逻辑,把加密从全文件加密改成只加密前4KB,性能就上来了。

经验之谈:调试Minifilter时,用WinDbg的!fltkd扩展。它能显示所有加载的Minifilter、Altitude、回调注册情况。我每次调试新驱动,第一件事就是!fltkd.filters看看有没有冲突。

文件系统过滤是个深坑,但掌握了Minifilter框架,你就能做出很多实用的工具。从简单的文件监控,到企业级的透明加密系统,都能用这套技术实现。嗯,今天就先聊到这,代码里的细节,你得多动手试试才能真掌握。


公众号:蓝海资料掘金营,微信deep3321