Minifilter注重功能实现,不注重更深层的IRP之类的操控
编写Minifilter的第一件事是向过滤器宣告我们的微过滤器的存在。这里所谓的微过滤器是符合过滤器标准的过滤组件,它其实是一组回调函数,这组回调函数向过滤管理器注册之后,在合适的时机(比如,要求的文件操作发生时)过滤管理器就会以合适的方式来调用某个回调函数。
如果我们编写这个回调函数中的内容,就可以对文件系统加以过滤了。这比花很多精力去绑定各种设备要简单得多,因为复杂的任务都在过滤管理器里面做了。
就基本很明白了,既然是一套回调函数,也就是过滤器,那就要先注册,然后开启,然后等等
一个是用FltRegisterFilter注册一个微过滤器;另一个是用函数FltStartFiltering来开始过滤。
NTSTATUS
FLTAPI
FltRegisterFilter (
__in PDRIVER_OBJECT Driver,
__in CONST FLT_REGISTRATION *Registration,
__deref_out PFLT_FILTER *RetFilter
);
第1个参数是本驱动的驱动对象,是在入口函数DriverEntry中作为参数传入的。
第2个参数就是一个宣告注册信息的结构,这个结构内含描述这个过滤器的全部信息。在这里,称为“微过滤器注册结构”。
第3个参数(RetFilter)是一个返回参数。返回注册成功的微过滤句柄。微过滤器句柄非常常用,一般都保存在全局变量中以备后用,在下面调用函数FltStartFiltering就需要这个句柄作为参数。显而易见,调用FltRegisterFilter本身并不复杂,问题在于要填写一个合法的FLT_REGISTRATION结构。这个结构在下一小节中介绍。
NTSTATUS
FLTAPI
FltStartFiltering (
__in PFLT_FILTER Filter
);
非常简单,此函数只有一个参数,就是调用FltRegisterFilter时返回的微过滤器句柄。一般情况下,这个函数的调用会成功;如果失败,除了放弃过滤,几乎别无选择。
微过滤器的数据结构
注册微过滤器时,我们填写了一个名为微过滤器注册结构(FLT_REGISTRATION)的数据结构,定义如下:
typedef struct _FLT_REGISTRATION {
USHORT Size; //结构的大小
USHORT Version; //结构的版本
FLT_REGISTRATION_FLAGS Flags; //微过滤器的标志位
CONST FLT_CONTEXT_REGISTRATION *ContextRegistration;
CONST FLT_OPERATION_REGISTRATION *OperationRegistration; //操作回调函数,这是重点里面的重点
PFLT_FILTER_UNLOAD_CALLBACK FilterUnloadCallback; //卸载回调函数
//实例安装回调
PFLT_INSTANCE_SETUP_CALLBACK InstanceSetupCallback;
PFLT_INSTANCE_QUERY_TEARDOWN_CALLBACK InstanceQueryTeardownCallback;
PFLT_INSTANCE_TEARDOWN_CALLBACK InstanceTeardownStartCallback;
PFLT_INSTANCE_TEARDOWN_CALLBACK InstanceTeardownCompleteCallback;
PFLT_GENERATE_FILE_NAME GenerateFileNameCallback; //生成文件名回调
PFLT_NORMALIZE_NAME_COMPONENT NormalizeNameComponentCallback; //格式化名字组件回调
PFLT_NORMALIZE_CONTEXT_CLEANUP NormalizeContextCleanupCallback; //格式化上下文清理回调
} FLT_REGISTRATION, *PFLT_REGISTRATION;
第1个域Size表示FLT_REGISTRTION结构的大小,当然大小就是sizeof(FLT_REGISTRATION)。微软习惯在Windows内核的数据结构前面加上大小,以便容易排错。
第2个域Version是FLT_REGISTRATION结构的版本号。对于这个域,读者不需要多加考虑,直接按照惯例填写FLT_REGISTRATION_VERSION即可。
第3个域Flags是标志位,标志是否要收到这一类的操作。但是有趣的是,这个域只有两种设置方法:一种设置为NULL,不起任何作用;另一种则设置为FLT_REGISTRATION_DO_NOT_SUPPORT_SERVICE_STOP,代表当停止服务时Minifilter不会响应且不会调用到FilterUnloadCallback,即使FilterUnloadCallback并不是NULL。
第4个域Context Registration:上下文注册,注册处理上下文的函数。
第5个域OperationRegistration:操作回调函数集注册。这是最重要的一个域,我们将要过滤的文件操作回调函数写在其中,可以定义所有功能代码对应的回调函数,举例如下:
const FLT_OPERATION_REGISTRATION Callbacks[] = {
{ IRP_MJ_CREATE,0,NPPreCreate,XxxPostCreate },
//填写要过滤的定义集合
{ IRP_MJ_OPERATION_END }
};
有关FLT_OPERATION_REGISTRATION这个结构,后面会做更详细的解说。
第6个域FilterUnloadCallback:驱动卸载回调函数。在这个驱动被停止时,这个函数被调用,代表要释放程序内的资源以结束过滤行为。这个域可以设置为NULL。
第7个域InstanceSetupCallback:实例安装回调函数,当一个卷实例要加载时会通知此回调处理。这个域可以设置为NULL。
第8个域InstanceQueryTeardownCallback:控制实例销毁函数,这个回调只有在一个手工解除绑定的请求时被调用。这个域可以设置成NULL。
第9个域InstanceTeardownStartCallback:实例解绑定函数,当调用时代表已经决定要解除绑定,这个域可以设置为NULL。
第10个域InstanceTeardownCompleteCallback:实例解绑定完成函数,当确定时调用解除绑定后的完成函数,这个域可以设置为NULL。
还有一些域因为使用不多,本书略去,有兴趣的读者可以自己参考相关文档。笔者习惯将它们设置成NULL。
数据结构实例:
const FLT_REGISTRATION FilterRegistration = {
sizeof( FLT_REGISTRATION ), // Size
FLT_REGISTRATION_VERSION, // Version
0, // Flags
NULL, // Context
Callbacks, // Operation callbacks
NPUnload, // MiniFilterUnload
NPInstanceSetup, // InstanceSetup
NPInstanceQueryTeardown, // InstanceQueryTeardown
NPInstanceTeardownStart, // InstanceTeardownStart
NPInstanceTeardownComplete, // InstanceTeardownComplete
NULL, // GenerateFileName
NULL, // GenerateDestinationFileName
NULL // NormalizeNameComponent
};
其中,最重要的就是CallBacks。这是一个回调函数数组,在其中可以处理所有的请求。但是处理方式可以和以前请求过滤时有所不同,以前处理的是IRP,其实有两种处理:一种是在请求完成之前就进行处理;另一种是用事件等待请求完成之后,或者在完成函数进行处理。前一种适合要拦截请求本身的情况,后一种适合要拦截请求之后返回结果的情况。在Minifilter中,这两种过滤被截然地分在两个回调函数中,一个称作预操作回调(Pre-Operation Function),另一个称为后操作回调(Post-Operation Function),下面是一个例子:
const FLT_OPERATION_REGISTRATION Callbacks[] =
{
{
IRP_MJ_CREATE,
0,
NPPreCreate, // 生成预操作回调函数
NPPostCreate // 生成后操作回调函数
},
{
IRP_MJ_WRITE,
FLT_OPERATION_REGISTRATION_SKIP_CACHED_IO,
NPPreWrite,
NPPostWrite
},
{IRP_MJ_OPERATION_END}
};
Callbacks数组内存储的数据结构为 FLT_OPERATION_REGISTRATION 的数组,用意是把需要做过滤的请求一个一个声明出来,每个都包括了预操作回调函数与后操作回调函数,宣告过后通过注册就能使IRP包顺利地通过这边指定的函数来做处理了。当有多个微过滤器时,IRP会通过每一个微过滤器的预操作函数与后操作函数,除非IRP传递到中途被直接返回不再传递下去。
可以看到,这个数组的每个元素由4个部分组成。第1个域是请求的主功能号,这是我们熟知的。第2个域是一个标志位,有3种写法:第1种是写0,这个标志仅仅对读/写回调有用,所以对生成请求的处理直接写0即可;第2种是写FLT_OPERATION_REGISTRATION_SKIP_CACHED_IO,表示不过滤缓冲读/写请求;
第3种是写FLT_OPERATION_REGISTRATION_SKIP_PAGING_IO,表示不过滤分页读写请求。接下来的两个域就是预操作回调函数和后操作回调函数。
请注意最后一个元素必须是{IRP_MJ_OPERATION_END},否则过滤器无法知道到底有多少个元素。
我们已经看到了上面有若干个回调函数,其中有一些回调函数在操作函数集Callbacks中,还有一些回调函数就直接在微过滤器注册结构中。下面的任务就是逐个实现这些函数。
卸载回调函数 FltUnregisterFilter
NTSTATUS
NPUnload (
__in FLT_FILTER_UNLOAD_FLAGS Flags
)
{
UNREFERENCED_PARAMETER( Flags );
PAGED_CODE();
PT_DBG_PRINT( PTDBG_TRACE_ROUTINES,("NPminifilter!NPUnload: Entered ") );
FltCloseCommunicationPort( gServerPort );
FltUnregisterFilter( gFilterHandle );
return STATUS_SUCCESS;
}
这个函数的主要工作是释放资源,FltUnregisterFilter与FltRegisterFilter互相对应,FltUnregisterFilter是用来释放已注册的微过滤器在Windows内核内部所使用的资源。
预操作数与后操作数的讲解
预操作回调函数
我们针对IRP_MJ_CREATE这个主功能号来设置预操作函数与后操作函数,当系统接收到标识为IRP_MJ_CREATE也就是试图生成或者打开文件时,自然就会调用预操作函数与后操作函数。
NPPreCreate就是我们设置的预回调函数。这个函数有3个参数,其中第一个参数是一个PFLT_CALLBACK_DATA的指针,PFLT_CALLBACK_DATA称为回调数据包,这个数据包内含有这个请求相关的全部信息。正是因为有了这个参数,所以不再直接读取IRP的信息了。这个函数的参数中不再有IRP指针。
FLT_PREOP_CALLBACK_STATUS
NPPreCreate (
__inout PFLT_CALLBACK_DATA Data,
__in PCFLT_RELATED_OBJECTS FltObjects,
__deref_out_opt PVOID *CompletionContext
)
{
//缓冲区,用来获得文件名
char FileName[260] = "X:";
NTSTATUS status;
PFLT_FILE_NAME_INFORMATION nameInfo;
//未使用的参数,用宏掩盖使之不发生编译警告
UNREFERENCED_PARAMETER( FltObjects );
UNREFERENCED_PARAMETER( CompletionContext );
//检测可分页代码
PAGED_CODE();
__try
{
//获取文件名信息,获取文件名和解析文件名等几个函数在本节内稍后的内容中介绍
status = FltGetFileNameInformation( Data,
FLT_FILE_NAME_NORMALIZED| FLT_FILE_NAME_QUERY_DEFAULT,
&nameInfo );
if (NT_SUCCESS( status ))
{
//判断是否阻挡
if (gCommand == ENUM_BLOCK)
{
//如果成功了,解析文件名信息,然后比较其中是否含有NOTEPAD.EXE这个子字符串
FltParseFileNameInformation( nameInfo );
//将字符串转换为CHAR大写以利于比对字符串
if (NPUnicodeStringToChar(&nameInfo->Name, FileName))
{
if (strstr(FileName, "NOTEPAD.EXE") > 0)
{
//填写拒绝
Data->IoStatus.Status = STATUS_ACCESS_DENIED;
Data->IoStatus.Information = 0;
FltReleaseFileNameInformation( nameInfo );
//返回请求已经结束,也就是说不用再下传了
return FLT_PREOP_COMPLETE;
}
}
}
//释放名字资源
FltReleaseFileNameInformation( nameInfo );
}
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
DbgPrint("NPPreCreate EXCEPTION_EXECUTE_HANDLER
");
}
return FLT_PREOP_SUCCESS_WITH_CALLBACK;
}
这是一个很简单的预操作函数,它的主要作用就是尽可能地解析目前的文件名称,然后判断这个名称是否符合我们需要的条件。我们要做的目的是限制名为"notepad.exe"的文件被使用,任何此文件的操作比如说读取、删除、覆盖、重命名、执行等,必定都会先调用到打开请求。因此,我们在这边做个简单的判断,试图去分辨出目前系统操作的文件是否就正符合我们所寻找的条件。
上面用到一个自定义函数NPUnicodeStringToChar。该函数将UNICODE_STRING转换为全大写的CHAR数组,一边搜索子字符串“NOTEPAD.EXE”。其中使用了内核API函数RtlUpperChar转换大小写,请读者试试自己实现这个函数。
下面是回调数据包的定义:
typedef struct _FLT_CALLBACK_DATA {
FLT_CALLBACK_DATA_FLAGS Flags;
PETHREAD CONST Thread;
PFLT_IO_PARAMETER_BLOCK CONST Iopb;
IO_STATUS_BLOCK IoStatus;
struct _FLT_TAG_DATA_BUFFER *TagData;
union {
struct {
LIST_ENTRY QueueLinks;
PVOID QueueContext[2];
};
PVOID FilterContext[4];
};
KPROCESSOR_MODE RequestorMode;
} FLT_CALLBACK_DATA, *PFLT_CALLBACK_DATA;
回调数据包结构代表了一个I/O操作。过滤管理器与微过滤驱动都使用这个结构来初始化与处理I/O操作,内含许多嵌套结构定义,这些定义可以在WDK标准头文档fltkernel.h中找到更多的数据。这个结构可以说是Minifilter的基础。以前在sfilter中,我们从IRP指针及IRP的当前栈空间指针中得到许多信息,比如写请求的长度等,现在我们如何让得到这些信息呢?
请注意Iopb域,这是一个PFLT_IO_PARAMETER_BLOCK指针,这个数据结构的定义如下:
typedef struct _FLT_IO_PARAMETER_BLOCK {
ULONG IrpFlags;
UCHAR MajorFunction;
UCHAR MinorFunction;
UCHAR OperationFlags;
UCHAR Reserved;
PFILE_OBJECT TargetFileObject;
PFLT_INSTANCE TargetInstance;
FLT_PARAMETERS Parameters;
} FLT_IO_PARAMETER_BLOCK, *PFLT_IO_PARAMETER_BLOCK;
在这里读者就可以找到以前熟悉的许多信息了,包括主功能号,次功能号和文件对象指针等。此外,其中还有一个结构为FLT_PARAMETERS的参数域,这个数据结构是一个联合体,应用得域根据不同的主功能号而不同,数据结构如下:
typedef union _FLT_PARAMETERS {
省略…
//
// IRP_MJ_WRITE
//
struct {
ULONG Length; //Length of transfer
ULONG POINTER_ALIGNMENT Key;
LARGE_INTEGER ByteOffset; //Offset to write to
PVOID WriteBuffer; //Not in IO_STACK_LOCATION parameters list
PMDL MdlAddress; //Mdl address for the buffer (maybe NULL)
} Write;
省略…
} FLT_PARAMETERS, *PFLT_PARAMETERS;
从这里就很容易找到写请求包括的写入位置、长度和缓冲区等相关参数。
这里再介绍一下解析文件路径所需要调用的函数。第一个函数是FltGetFileNameInformation,原型如下:
NTSTATUS
FLTAPI
FltGetFileNameInformation (
__in PFLT_CALLBACK_DATA CallbackData,
__in FLT_FILE_NAME_OPTIONS NameOptions,
__deref_out PFLT_FILE_NAME_INFORMATION *FileNameInformation
);
这个函数可以取得一个文件或目录的文件名信息的结构,第二个函数名为FltParseFileNameInformation,原型如下:
NTSTATUS
FLTAPI
FltParseFileNameInformation (
__inout PFLT_FILE_NAME_INFORMATION FileNameInformation
);
通过FltParseFileNameInformation这个函数可以的到一个含有路径名称与文件名称的字符串,我们再用字符串替换与对比便可以轻易的找出路径内是否有NOTEPAD.EXE等字符串。在决定否决这个请求之后,我们采用常见的与填写IRP的IoStatus域完全一样的方法否决这次请求,相关代码如下:
Data->IoStatus.Status = STATUS_ACCESS_DENIED;
Data->IoStatus.Information = 0;
FltReleaseFileNameInformation( nameInfo );
return FLT_PREOP_COMPLETE;
这段程序代码主要是要告诉过滤器,这个请求要即刻返回失败。即代表了这个IRP不会往下处理。
后操作回调函数
当IRP完成返回时就会通知后操作回调函数,例如,若不要让文件新建成功,可以通过FltCancelFileOpen这个操作,这是因为我们在预操作函数内就已经过滤该行为且设定返回值的动作了,并不需要在这里重做一次。下面这个后处理回调函数对程序功能本身并没有意义,仅仅作为后处理回调函数写法的说明在这里展现给读者。
FLT_POSTOP_CALLBACK_STATUS
NPPostCreate (
__inout PFLT_CALLBACK_DATA Data,
__in PCFLT_RELATED_OBJECTS FltObjects,
__in_opt PVOID CompletionContext,
__in FLT_POST_OPERATION_FLAGS Flags
)
{
FLT_POSTOP_CALLBACK_STATUS returnStatus = FLT_POSTOP_FINISHED_PROCESSING;
PFLT_FILE_NAME_INFORMATION nameInfo;
NTSTATUS status;
UNREFERENCED_PARAMETER( CompletionContext );
UNREFERENCED_PARAMETER( Flags );
//
// If this create was failing anyway, don't bother scanning now.
//
if (!NT_SUCCESS( Data->IoStatus.Status ) ||
(STATUS_REPARSE == Data->IoStatus.Status)) {
return FLT_POSTOP_FINISHED_PROCESSING;
}
//从回调数据包里面获得名字信息
status = FltGetFileNameInformation( Data,
FLT_FILE_NAME_NORMALIZED | FLT_FILE_NAME_QUERY_DEFAULT,
&nameInfo );
if (!NT_SUCCESS( status )) {
return FLT_POSTOP_FINISHED_PROCESSING;
}
return returnStatus;
}
返回FLT_POSTOP_FINISHED_PROCESSING代表Minifilter已经完成对I/O的所有处理,并返回控制给过滤管理器。
通信方式
考虑到内核态与用户态之间的互动,以前的做法是使用用户态的API函数DeviceIOControl结合在内核模块中的处理控制请求来实现双方数据的传递。但是在Minifilter中却不同,Minifilter有内建支持API提供给开发者来使用,这里就先针对这些API来作介绍。
这个方法有个称呼叫“通信端口”(Communication Port),顾名思义,就是先定义一个通道名称,通过双边已经定义好的通信端口来做数据上的沟通,使用上很像socket或管道(pipe)之类的通信程序设计。
PSECURITY_DESCRIPTOR sd;
OBJECT_ATTRIBUTES oa;
status = FltBuildDefaultSecurityDescriptor( &sd, FLT_PORT_ALL_ACCESS );
if (!NT_SUCCESS( status )) {
goto final;
}
RtlInitUnicodeString( &uniString, MINISPY_PORT_NAME );
InitializeObjectAttributes( &oa,
&uniString,
OBJ_KERNEL_HANDLE | OBJ_CASE_INSENSITIVE,
NULL,
sd );
status = FltCreateCommunicationPort( gFilterHandle,
&gServerPort,
&oa,
NULL,
NPMiniConnect,
NPMiniDisconnect,
NPMiniMessage,
1 );
也就是利用FltBuildDefaultSecurityDescriptor先初始化安全描述符,再初始化对象,再以某个端口名进行通信,再RING3相应的函数对应相应的例如NPMiniConnect这样的函数进行通信
FltBuildDefaultSecurityDescriptor以FLT_PORT_ALL_ACCESS权限来产生一个安全性的叙述子,MINISPY_PORT_NAME是刚刚所讲的通信端口定义的名称,通过InitializeObjectAttributes来初始化对象属性(OBJECT_ATTRIBUTES),接下来便是注册这个通信端口以及所需要使用到的函数。
这里必须提供3个回调函数,类似于以前我们为了实现通信所写的控制请求的分发函数。这3个回调函数分别是NPMiniConnect、NPMiniDisconnect、NPMiniMessage。
NPMiniConnect是用户态与内核态建立连接时内核会调用到的函数。
NPMiniDisconnect是用户态与内核态连接结束时内核会调用到的函数。
NPMiniMessage是用户态与内核态传送数据时内核会调用到的函数。
用户态不再使用CreateFile和DeviceIoControl这系列的API,Minifilter有专门的API提供给用户态程序使用。
相关的API主要有两个:FilterConnectCommunicationPort和 FilterSendMessage,
FilterConnectCommunicationPort可以调用到我们提供的NPMiniConnect函数,
FilterSendMessage调用到相对应的NPMiniMessage。
一对一关系很容易理解。至于参数都是PVOID的指针,开发时两边程序通过自定义的数据结构,传入指针即可将数据传入或者取出。
HRESULT
WINAPI
FilterConnectCommunicationPort(
IN LPCWSTR lpPortName,
IN DWORD dwOptions,
IN LPVOID lpContext OPTIONAL,
IN DWORD dwSizeOfContext,
IN LPSECURITY_ATTRIBUTES lpSecurityAttributes OPTIONAL,
OUT HANDLE *hPort
);
各参数说明如下:
lpPortName:宽字符字符串,比如L"NPPort";
dwOptions:目前没有使用,设为0;
lpContext:通过此参数可以传入上下文数据给Minifilter的connect routine;
dwSizeOfContext:上下文的数据大小,单位为byte;
lpSecurityAttributes:通过此API,只要传入已定义的Port名称,就可以得到句柄;
此外,WDK定义的FilterSendMessage原型如下:
HRESULT
WINAPI
FilterSendMessage(
__in HANDLE hPort,
__in_bcount LPVOID lpInBuffer,
__in DWORD dwInBufferSize,
__out_bcount_part_opt LPVOID lpOutBuffer,
__in DWORD dwOutBufferSize,
__out LPDWORD lpBytesReturned
);
各参数说明如下:
hPort:连接端口名称,宽字符字符串;
lpInBuffer:输入缓冲区,将定义好的结构用指针传入;
dwInBufferSize:输入缓冲区的大小;
lpOutBuffer:输出缓冲区,既可以传入数据也可以取得返回的数据;
dwOutBufferSize:输出缓冲区的大小;
lpBytesReturned:当FilterSendMessage调用成功时会返回一个标志lpOutBuffer大小的值。
说了这么多具体怎么做呢,我们可以做一个dll作为Ring3和内核的通信,RING3调用就行了,但是必须得有FltUser.h fltLib.lib fltMgr.lib 记住的一点是初始化的完成在DLL_PROCESS_ATTACH中
以下是示例
示例:
/ dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "stdafx.h"
#include <FltUser.h>
#pragma comment(lib, "fltLib.lib")
typedef enum _USER_COMMAND_
{
USER_PASS = 0,
USER_BLOCK
}USER_COMMAND;
#define MINI_FILTER_PORT_NAME L"\MiniFilterPort"
HANDLE __PortHandle = INVALID_HANDLE_VALUE;
int InitialCommunicationPort(void);
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
{
InitialCommunicationPort();
break;
}
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
{
if (__PortHandle != NULL)
{
CloseHandle(__PortHandle);
__PortHandle = NULL;
}
break;
}
}
return TRUE;
}
int InitialCommunicationPort(void)
{
//MiniFilter 的通信机制 --->??
DWORD Status = FilterConnectCommunicationPort(
MINI_FILTER_PORT_NAME, //监听套接字
0,
NULL,
0,
NULL,
&__PortHandle);
if (Status != S_OK) {
return Status;
}
return 0;
}
int MiniFilterDeviceIoControl(USER_COMMAND UserCommand)
{
DWORD ReturnLength = 0;
DWORD Status = 0;
//同步还是异步 ?? ---> ??
Status = FilterSendMessage(
__PortHandle,
&UserCommand,
sizeof(USER_COMMAND),
NULL,
NULL,
&ReturnLength);
if (Status != S_OK)
{
return Status;
}
return 0;
}