返回
顶部

references:

前面讲到的windows文件系统过滤驱动代码比较多,而且容易出错,所以微软推出了minifilter框架,来让开发者更专注于业务逻辑,一些重复性的繁杂的代码都整合到了框架中

我们在WPP修改过的FsFilter项目代码上进行修改

另外需要注意的是,MiniFilter项目需要使用C++进行编写,因此文件名需要更改为.cpp

代码编写

之前用的全局变量是g_fsFilterDriverObject,现在我们需要改成MiniFilter句柄

PFLT_FILTER g_minifilterHandle = NULL;

然后需要在DriverEntry函数中注册MiniFilter

NTSTATUS status = FltRegisterFilter(DriverObject, &g_filterRegistration, &g_minifilterHandle);
if (!NT_SUCCESS(status)) 
    return status; 

然后启动MiniFilter来拦截IO请求

status = FltStartFiltering(g_minifilterHandle);
if (!NT_SUCCESS(status)) 
    FltUnregisterFilter(g_minifilterHandle); 

上面调用FltRegisterFilter注册MiniFilter的时候使用了一个未定义的变量g_filterRegistration这是另一个全局变量,是一个FLT_REGISTRATION结构体

CONST FLT_REGISTRATION g_filterRegistration =
{
    sizeof(FLT_REGISTRATION),      //  Size
    FLT_REGISTRATION_VERSION,      //  Version
    0,                             //  Flags
    NULL,                          //  Context registration
    g_callbacks,                   //  Operation callbacks
    InstanceFilterUnloadCallback,  //  FilterUnload
    InstanceSetupCallback,         //  InstanceSetup
    InstanceQueryTeardownCallback, //  InstanceQueryTeardown
    NULL,                          //  InstanceTeardownStart
    NULL,                          //  InstanceTeardownComplete
    NULL,                          //  GenerateFileName
    NULL,                          //  GenerateDestinationFileName
    NULL                           //  NormalizeNameComponent
};

g_callbacks字段为FLT_OPERATION_REGISTRATION结构体,该结构体描述了针对捕获的IO操作的pre-operation和post-operation

我们这里只写一个pre-operation

CONST FLT_OPERATION_REGISTRATION g_callbacks[] =
{
    {
        IRP_MJ_CREATE,
        0,
        PreOperationCreate,
        0
    },

    { IRP_MJ_OPERATION_END }
};

新建一个文件FsMinifilter.cpp

该文件中将会包含回调函数的定义

MiniFilter中的pre和post opertaion的概念就相当于我们之前写的传统fsfilter中的dispatch和completion例程

我们需要针对IRP_MJ_CREATE操作定义一个pre-operation来输出文件名

FLT_PREOP_CALLBACK_STATUS FLTAPI PreOperationCreate(
    _Inout_ PFLT_CALLBACK_DATA Data,
    _In_ PCFLT_RELATED_OBJECTS FltObjects,
    _Flt_CompletionContext_Outptr_ PVOID* CompletionContext
)
{
    //
    // Pre-create callback to get file info during creation or opening
    //

    DbgPrint("%wZ\n", &Data->Iopb->TargetFileObject->FileName);

    return FLT_PREOP_SUCCESS_NO_CALLBACK;
}

下一步需要实现InstanceFilterUnloadCallback函数,这个会在卸载MiniFilter的时候用到,我们在这个函数中调用FltUnregisterFilter函数来注销MiniFilter

还有另外两个函数InstanceSetupCallback和InstanceQuertTeardownCallback需要定义,这两个函数会在我们的minifilter连接磁盘分区或者和磁盘分区断开连接的时候被用到

这两个函数只是个stub,里面没有实际的代码,只有一个返回语句

NTSTATUS FLTAPI InstanceSetupCallback(
    _In_ PCFLT_RELATED_OBJECTS  FltObjects,
    _In_ FLT_INSTANCE_SETUP_FLAGS  Flags,
    _In_ DEVICE_TYPE  VolumeDeviceType,
    _In_ FLT_FILESYSTEM_TYPE  VolumeFilesystemType)
{
    //
    // This is called to see if a filter would like to attach an instance to the given volume.
    //

    return STATUS_SUCCESS;
}

NTSTATUS FLTAPI InstanceQueryTeardownCallback(
    _In_ PCFLT_RELATED_OBJECTS FltObjects,
    _In_ FLT_INSTANCE_QUERY_TEARDOWN_FLAGS Flags
)
{
    //
    // This is called to see if the filter wants to detach from the given volume.
    //

    return STATUS_SUCCESS;
}

演示

项目地址

可以看到明显代码要少了很多很多,我们只需要实现一下回调函数即可

扩展

这里着重介绍minifilter的context概念,windowd的很多机制中都有各种context

在minifilter这里,context是filter自定义的一些数据,这些数据附着在某些内核对象中

在使用Context之前,需要先调用FltAllocateContext函数来获取一个未初始化的Context对象

函数签名如下:

NTSTATUS FLTAPI FltAllocateContext(
  [in]  PFLT_FILTER      Filter,
  [in]  FLT_CONTEXT_TYPE ContextType,
  [in]  SIZE_T           ContextSize,
  [in]  POOL_TYPE        PoolType,
  [out] PFLT_CONTEXT     *ReturnedContext
);

a1是filter注册函数FltRegisterFilter的返回值,同时它也可以通过filter注册的callback函数的FLT_RELATED_OBJECTS参数获取

image-20240518112817314

a5是传出参数,里面会保存未初始化的Context对象

分配之后,我们就可以在里面存储任何我们想要存储的数据,我们必须把这个Context attach到一个对象上,这个可以通过调用FltSetFileContext函数完成,不同的Context类型拥有对应的Set函数,只需要把File替换为对应的Context类型名即可

函数签名如下:

NTSTATUS FLTAPI FltSetFileContext(
  [in]  PFLT_INSTANCE             Instance,
  [in]  PFILE_OBJECT              FileObject,
  [in]  FLT_SET_CONTEXT_OPERATION Operation,
  [in]  PFLT_CONTEXT              NewContext,
  [out] PFLT_CONTEXT              *OldContext
);

对于所有的类型,a1都是必须要提供的参数,对于FILE类型的Context,我们需要提供一个FILE_OBJECT来将Context附着在上面

NewContext就是我们前面Allocate的Context对象,OldContext是一个可选参数,可以用来取回该FILE_OBJECT上之前附着的Context对象

Context有自己的引用计数,因此在用完之后,需要调用FltReleaseContext函数来进行释放

对于这种需要对引用计数加以关注的对象,使用RAII是最佳实践

后面如果需要使用Context,使用FltGetXxxContext函数即可,这里的Xxx就是Context类型

在minifilter中发起IO请求

我们不能直接在filter的callback中使用诸如ZwCreateFile这样的内核函数,因为我们当前就在IO处理的栈中,再次调用这种函数很容易发生问题

filter manager提供了一些函数来帮助filter进行IO操作,这些函数的名称和ZeCreatefile很像,只是前缀变成了Flt

这个函数的函数签名太长,我就不贴了,可以自己看官方文档

不过大多数情况下,我们选择使用FltCreateFileEx,该函数中有一个传出参数,会保存返回的FILE_OBJECT对象,然后我们可以调用FltReadFile和FltWriteFile等函数对该FILE_OBJECT进行操作,对于这种情况,在操作完成之后,需要使用ObDereferenceObject函数来降低引用计数

下面我们来实现一个文件备份驱动以加深理解

File Backup Driver

我们将要开发的这个驱动拥有自动备份文件的功能,每当这个文件被打开进行写入操作的时候,我们就会先备份一下原始文件,这样可以在将来有需要的时候恢复到上一个状态

我们将以NTFS Stream的形式将备份文件存储在原始文件中,如果需要恢复文件的上一个状态,我们使用Context替换掉当前的Stream以完成备份的恢复

我们定义的Context对象如下:

typedef struct _FileContext {
    Mutex Lock;
    LARGE_INTEGER BackupTime;
    BOOLEAN Written;
}FileContext;

其中第一个字段是一个互斥量,我们使用他来进行同步,因为一个文件可能会被多个线程在几乎同一时刻进行写入,我们需要互斥量来保证只有一个备份生成

我们这里使用的是标准互斥量而不是FastMutex,是因为我们的驱动需要进行IO操作,而IO操作只在IRQL为0(PASSIVE_LEVEL)下才能进行,FastMutex会将IRQL升高到1(APC_LEVEL)从而导致死锁

关于死锁的更详细解释是,IO操作通过向请求线程发送特殊内核APC来完成,而APC在APC_LEVEL下无法执行,一个正在等待FAST_MUTEX的线程的IRQL就是APC_LEVEL,那么IO操作就永远无法完成,死锁就形成了

Write字段是为了保证我们只在用户第一次调用Write函数时对文件进行备份

image-20240519161229347

RAII

在我们备份文件的代码中,我们需要进行上锁,其中用到了RAII特性,一个Autolocker类

#pragma once
template<typename TLock>
class Locker {
public:
    Locker(TLock* lock) :m_lock(lock) {
        m_lock->Lock();
    }
    ~Locker() {
        m_lock->Unlock();
    }
private:
    TLock* m_lock;
};

我们使用do while循环划出来一个作用域(直接用花括号也可以划出来一块作用域)然后在该作用域中实例化一个Autolocker类,此时构造函数被调用,上锁操作随之完成,在出了作用域之后,Autolocker类会自动析构从而完成解锁操作

这里面海涌到了模板,C++真是博大精深,不理解语言特性连代码都看不懂

这里虽然我写的是指针,但是推荐使用引用写法,因为引用可以在编译阶段规避掉空指针引用的问题,有空指针的话编译是无法通过的

文件复制

NTFS文件系统支持Stream,我们可以在原始文件路径后面跟一个:Backup来创建出一个Stream,然后把源文件内容写入这个Stream中

使用FltReadFile和FltWriteFile函数进行源文件的读取和目标文件的写入

最后就是我们当前文件的大小可能会小于上一个版本的备份文件的大小,所以我们还需要设置当前版本备份文件的文件结束指针的位置

FILE_END_OF_FILE_INFORMATION info;
        info.EndOfFile = fileSize;
        status = FltSetInformationFile(FltObjects->Instance, targetFile, &info, sizeof(info),
            FileEndOfFileInformation);

OnPostCleanup

我们的Context是附着在一个文件上的,如果我们不作任何处理的话,只有当这个文件被删除的时候,我们的Context才会被清理掉,但是这个文件有可能永远都不会被删除,因此我们需要在文件关闭的时候清理掉Context

IRP_MJ_CLOSE和IRP_MJ_CLEANUP,两者的区别在于后者意味着对应的FILE_OBJECT已经不再被需要了,该文件的所有句柄都已经关闭,但是FILE_OBJECT的引用计数还没有归零,这是我们清理Context的最佳时机;而前者意味着最后一个句柄被关闭,但是由于缓存的存在,有时并不总是这样

效果演示

项目地址

改进

可以使用SectionObject来进行文件的备份,SectionObject允许我们像操作内存一样操作文件内容,而且无需操心内存的分配,这些工作以及写回到文件的操作都是由内存管理器来自动完成的

Section还支持在进程间以及内核模式和用户模式之间共享内存

下面使用SecionObject改进前面编写的Backup驱动的代码,主要的改动发生在BackupFile函数中

这是改进后的代码,可以看到没有给buffer分配内存

// 改写为使用SectionObject
NTSTATUS BackupFile(UNICODE_STRING path, PCFLT_RELATED_OBJECTS FltObjects) {
    KdPrintEx((FOR_DEBUG, TRACE_LEVEL_ERROR, "Entering BackupFile function call\n"));
    // 在内核里面是没有CopyFile这种现成的API给我们使用的
    LARGE_INTEGER fileSize;
    auto status = FsRtlGetFileSize(FltObjects->FileObject, &fileSize);
    if (!NT_SUCCESS(status)) {
        KdPrintEx((FOR_DEBUG, TRACE_LEVEL_ERROR, "Retrieve file size failed [0x%08X]\n", status));
        return status;
    }
    if (fileSize.QuadPart == 0) {
        KdPrintEx((FOR_DEBUG, TRACE_LEVEL_ERROR, "File empty [0x%08X]\n", status));
        return FLT_PREOP_SUCCESS_NO_CALLBACK;
    }
    // 我们的基本想法就是分别打开两个stream(srouce),一个是原始stream(target),另一个是用来备份的stream
    // 打开source
    PFILE_OBJECT sourceFile = NULL;
    PFILE_OBJECT targetFile = NULL;
    HANDLE hSourceFile = NULL;
    HANDLE hTargetFile = NULL;
    HANDLE hSection = NULL;
    IO_STATUS_BLOCK statusBlock;
    PVOID buffer = nullptr;
    do {
        OBJECT_ATTRIBUTES sourceFileAttr;
        InitializeObjectAttributes(&sourceFileAttr, &path, OBJ_KERNEL_HANDLE | OBJ_CASE_INSENSITIVE, NULL, NULL);
        // 使用FltCreateFileEx打开源文件
        // 你妈的这参数也太多了点
        // 我们用户模式下用于打开文件的API通常都会要求提供一个文件路径
        // 而内核模式种对应的API则要求提供一个OBJECT_ATTRIBUTES结构体,里面会包含要打开的文件路径
        // 这里面比较重要的参数有以下几个
        /*
        这两个参数提供了必要的信息以告知IO管理器设备栈中的下一个设备的位置,这样才能正常向目标设备
        发送IO请求
        FltObjects->Filter,
        FltObjects->Instance,
        hSourceFile  传出参数,用于接收该函数返回的句柄
        sourceFile   也是传出参数,用于接受该函数返回的FILE_OBJECT结构体,后面提供给FltReadFile函数使用
        FILE_OPEN    这个标志位就是表示文件一定要是已经存在的
        IO_IGNORE_SHARE_ACCESS_CHECK   这个标志位很重要,因为目标文件已经处于打开状态,且此时我们
        并不知道其是否设置了share相关标志位,所以我们请求IO管理器忽略share的检查
        */
        status = FltCreateFileEx(
            FltObjects->Filter,
            FltObjects->Instance,
            &hSourceFile,
            &sourceFile,
            GENERIC_READ | SYNCHRONIZE,
            &sourceFileAttr,
            &statusBlock,
            NULL, FILE_ATTRIBUTE_NORMAL,
            FILE_SHARE_READ | FILE_SHARE_WRITE,
            FILE_OPEN, FILE_SYNCHRONOUS_IO_NONALERT | FILE_SEQUENTIAL_ONLY,
            NULL, 0, IO_IGNORE_SHARE_ACCESS_CHECK
        );
        if (!NT_SUCCESS(status)) {
            KdPrintEx((FOR_DEBUG, TRACE_LEVEL_ERROR, "File open failed [0x%08X]\n", status));
            break;
        }
        // 然后我们需要在同一个文件中创建另一个Stream
        UNICODE_STRING targetFileName;
        const WCHAR backupStream[] = L":backup";
        targetFileName.MaximumLength = path.Length + sizeof(backupStream);
        targetFileName.Buffer = (WCHAR*)ExAllocatePoolWithTag(PagedPool, targetFileName.MaximumLength, DRIVER_TAG);
        if (NULL == targetFileName.Buffer) {
            KdPrintEx((FOR_DEBUG, TRACE_LEVEL_ERROR, "Allocate memory from paged pool failed\n"));
            status = STATUS_NO_MEMORY;
            break;
        }
        RtlCopyUnicodeString(&targetFileName, &path);
        RtlAppendUnicodeToString(&targetFileName, backupStream);
        KdPrintEx((FOR_DEBUG, TRACE_LEVEL_ERROR, "Backup file name: %wZ\n", &targetFileName));
        OBJECT_ATTRIBUTES targetFileAttr;
        InitializeObjectAttributes(&targetFileAttr, &targetFileName, OBJ_KERNEL_HANDLE | OBJ_CASE_INSENSITIVE, NULL, NULL);
        status = FltCreateFileEx(
            FltObjects->Filter,
            FltObjects->Instance,
            &hTargetFile,
            &targetFile,
            GENERIC_WRITE | SYNCHRONIZE,
            &targetFileAttr,
            &statusBlock,
            NULL, FILE_ATTRIBUTE_NORMAL,
            0,
            FILE_OVERWRITE_IF, // 覆盖已经存在的数据
            FILE_SYNCHRONOUS_IO_NONALERT | FILE_SEQUENTIAL_ONLY,
            NULL, 0, 0
        );
        if (!NT_SUCCESS(status)) {
            KdPrintEx((FOR_DEBUG, TRACE_LEVEL_ERROR, "Create backup stream failed [0x%08X]\n", status));
            break;
        }
        // 释放targetFileName.Buffer内存
        ExFreePoolWithTag(targetFileName.Buffer, DRIVER_TAG);
        // 创建SectionObject   这个Section用来映射源文件
        OBJECT_ATTRIBUTES sectionAttr = RTL_CONSTANT_OBJECT_ATTRIBUTES(nullptr , OBJ_KERNEL_HANDLE);
        status = ZwCreateSection(&hSection, SECTION_MAP_READ | SECTION_QUERY,
            &sectionAttr, NULL,
            PAGE_READONLY, SEC_COMMIT, hSourceFile);
        if (!NT_SUCCESS(status)) {
            KdPrintEx((FOR_DEBUG, TRACE_LEVEL_ERROR, "ZwCreateSection failed [0x%08X]\n", status));
            break;
        }
        // 这些操作都完成之后,我们就可以进行srouce到target的拷贝操作了
        // 使用循环分配小块内存,直到整个文件拷贝完成
        ULONG size = 1 << 20; // 1MB 1*1024*1024
        // 我们无需分配内存,只需要提供一个指针变量作为map函数的传出参数即可
        // buffer = ExAllocatePoolWithTag(PagedPool, size, DRIVER_TAG);
        // if (nullptr == buffer) {
        //  KdPrintEx((FOR_DEBUG, TRACE_LEVEL_ERROR, "Buffer allocate failed"));
        //  status = STATUS_NO_MEMORY;
        //  break;
        // }
        ULONGLONG remainBytes = fileSize.QuadPart;
        ULONG out = 0;
        ULONG readOffset = 0;
        LARGE_INTEGER sectionOffset;
        sectionOffset.QuadPart = 0;
        SIZE_T viewBytes = 0;
        ULONG bytesWritten = 0;
        // 从source中读取数据然后写入到target中
        while (remainBytes) {
            viewBytes = min(size, remainBytes);
            ZwMapViewOfSection(hSection, NtCurrentProcess(), &buffer, 0, 0,&sectionOffset, 
                &viewBytes, ViewUnmap, 0, PAGE_READONLY);

            if (!NT_SUCCESS(status)) {
                KdPrintEx((FOR_DEBUG, TRACE_LEVEL_ERROR, "ZwMapViewOfSection failed [0x%08X]\n", status));
                break;
            }
            // 写入目标stream
            status = FltWriteFile(
                FltObjects->Instance,
                targetFile,
                0,// 在进行同步IO的时候是没有必要指定offset的,因为IO管理器会自动追踪偏移量
                viewBytes,
                buffer,
                0,
                &bytesWritten,
                NULL, NULL);
            if (!NT_SUCCESS(status)) {
                KdPrintEx((FOR_DEBUG, TRACE_LEVEL_ERROR, "Write bytes to target failed [0x%08X]\n", status));
                break;
            }

            status=ZwUnmapViewOfSection(NtCurrentProcess(), buffer);
            if (!NT_SUCCESS(status)) {
                KdPrintEx((FOR_DEBUG, TRACE_LEVEL_ERROR, "ZwUnmapViewOfSection failed [0x%08X]\n", status));
                break;
            }
            sectionOffset.QuadPart += bytesWritten;
            remainBytes -= bytesWritten;
        }
        // 还要考虑到一种情况,就是上一个版本的备份文件比这次备份的大,因此我们需要设置文件的结束位置
        FILE_END_OF_FILE_INFORMATION info;
        info.EndOfFile = fileSize;
        status = FltSetInformationFile(FltObjects->Instance, targetFile, &info, sizeof(info),
            FileEndOfFileInformation);
        if (!NT_SUCCESS(status)) {
            KdPrintEx((FOR_DEBUG, TRACE_LEVEL_ERROR, "Set file end pointer failed [0x%08X]\n", status));
            break;
        }
    } while (false);


    status = FsRtlGetFileSize(targetFile, &fileSize);
    if (!NT_SUCCESS(status)) {
        KdPrintEx((FOR_DEBUG, TRACE_LEVEL_ERROR, "Failed to get backup file size [0x%08X]\n", status));
    }
    else {

        KdPrintEx((FOR_DEBUG, TRACE_LEVEL_ERROR, "Backup file size: [0x%08X]\n", fileSize.QuadPart));
    }
    KdPrintEx((FOR_DEBUG,TRACE_LEVEL_ERROR,"[sourceFile] This is the source file object address [0x%p]\n", sourceFile));
    KdPrintEx((FOR_DEBUG,TRACE_LEVEL_ERROR,"[targetFile] This is the backup file object address [0x%p]\n", targetFile));

    if (hTargetFile)  // 虽然这两个句柄没啥用,但还是要带上的,不然运行到FlrCreateFileEx的时候会BSOD
        FltClose(hTargetFile);
    if (hSourceFile)
        FltClose(hSourceFile);
    if (hSection)
        ZwClose(hSection);
    if (sourceFile)
        ObDereferenceObject(sourceFile);
    if (targetFile)
        g_backupFileObject = targetFile;
    KdPrintEx((FOR_DEBUG, TRACE_LEVEL_ERROR, "[g_backupFileObject] This is the backup file object address [0x%p]\n", g_backupFileObject));
    KdPrintEx((FOR_DEBUG, TRACE_LEVEL_ERROR, "File backup succeed\n"));
    return status;
}

项目地址

和用户模式进行通信

IoControl可以用于User/Kernel之间的通信,但是这种方式只能由User主动发起,Kernel只能被动接收消息

FilterManager提供了一个通信机制,可以让Kernel直接向User发送消息,这个东西被称作FilterCimmunicationPort,通过FltCreateCommunicationPort函数可以创建一个这样的端口出来

然后通过FltSendMessage来通过该端口发送消息,User也可以使用这套通信机制向Kernel发送消息,这个通信端口是双向的,只不过在UserMode中使用的API名称是FilterSendMessage

下面使用通信端口对前面的BackupDriver进一步改进

项目地址

演示