📜 ⬆️ ⬇️

Development of MiniFilter driver

I had a chance at work to face the task of controlling access and redirecting requests to the file system within certain processes. It was necessary to implement a simple, easily configurable solution.

I decided to develop a MiniFilter driver that can be configured using a text file.

Consider what MiniFilter represents in general:
')
Filtering is carried out through the so-called Filter Manager, which comes with the Windows operating system, is activated only when loading mini filters. Filter Manager connects directly to the file system stack. Mini filters are registered for processing data on I / O operations using the Filter Manager function, thus obtaining indirect access to the file system. After registration and start-up, the mini filter receives a set of data on I / O operations that were specified during configuration and, if necessary, can make changes to this data, thus affecting the operation of the file system.


The following diagram shows a simplified view of how the Filter Manager functions.

More detailed theoretical information can be obtained on the MSDN website using the link at the end of the article. Not bad enough all sorted out.

We will move towards development and consider some basic structures that need to be filled.

General global data.
typedef struct _MINIFILTER { PDRIVER_OBJECT pDriverObject; PFLT_FILTER pFilter; } MINIFILTER, *PMINIFILTER; MINIFILTER fileManager; 


In this structure, we will store the link to the object of our driver and the link to the filter instance. I want to note that PFLT_FILTER uniquely identifies the mini filter and remains constant for the entire duration of the driver. Used when activating or stopping the filtering process.

Register filter
 CONST FLT_REGISTRATION FilterRegistration = { sizeof( FLT_REGISTRATION ), // Size FLT_REGISTRATION_VERSION, // Version 0, // Flags NULL, // Context Callbacks, // Operation callbacks FilterUnload, // FilterUnload FilterLoad, // InstanceSetup NULL, // InstanceQueryTeardown NULL, // InstanceTeardownStart NULL, // InstanceTeardownComplete NULL, // GenerateFileName NULL // NormalizeNameComponent }; 


Here it is necessary to dwell on several fields:
  1. Callbacks - a link to a structure that defines what and with what functions we are going to process.
  2. FilterUnload is the function that will be called when the filter is disabled.
  3. FilterLoad is a function that will be called when the filter is initialized.


Next, consider the structure of Callbacks:
 const FLT_OPERATION_REGISTRATION Callbacks[] = { { IRP_MJ_CREATE, 0, PreFileOperationCallback, PostFileOperationCallback }, { IRP_MJ_OPERATION_END } }; 


Here we indicate that we will intercept the CreateFile operation, also indicate the functions that will be called, respectively, before and after the operation on the file.

Next, I provide the code for the functions that are called when the filter is initialized and disabled.
 NTSTATUS FilterLoad (IN PCFLT_RELATED_OBJECTS FltObjects, IN FLT_INSTANCE_SETUP_FLAGS Flags, IN DEVICE_TYPE VolumeDeviceType, IN FLT_FILESYSTEM_TYPE VolumeFilesystemType) { if (VolumeDeviceType == FILE_DEVICE_NETWORK_FILE_SYSTEM) { return STATUS_FLT_DO_NOT_ATTACH; } return STATUS_SUCCESS; } NTSTATUS FilterUnload ( IN FLT_FILTER_UNLOAD_FLAGS Flags ) { return STATUS_SUCCESS; } 


I think the code does not need additional comments, since everything is fairly standard. I note only that our driver will not work for the network.

Now let's look at the driver initialization function:
 NTSTATUS DriverEntry( IN PDRIVER_OBJECT theDriverObject, IN PUNICODE_STRING theRegistryPath ) { int i; NTSTATUS status; PCHAR ConfigInfo; UNICODE_STRING test; DbgPrint("MiniFilter: Started."); // Register a dispatch function for (i = 0; i < IRP_MJ_MAXIMUM_FUNCTION; i++) { theDriverObject->MajorFunction[i] = OnStubDispatch; } theDriverObject->DriverUnload = OnUnload; fileManager.pDriverObject = theDriverObject; status = FltRegisterFilter(theDriverObject, &FilterRegistration, &fileManager.pFilter); if (!NT_SUCCESS(status)) { DbgPrint("MiniFilter: Driver not started. ERROR FltRegisterFilter - %08x\n", status); return status; } ConfigInfo = ReadConfigurationFile(); if(ConfigInfo != NULL && NT_SUCCESS(ParseConfigurationFile(ConfigInfo))) { ExFreePool(ConfigInfo); DbgPrint("MiniFilter: Configuration finished."); }else { if(ConfigInfo != NULL)ExFreePool(ConfigInfo); FltUnregisterFilter( fileManager.pFilter ); DbgPrint("MiniFilter: Driver configuration was failed. Driver not started."); return STATUS_DEVICE_CONFIGURATION_ERROR; } status = FltStartFiltering( fileManager.pFilter ); if (!NT_SUCCESS( status )) { FltUnregisterFilter( fileManager.pFilter ); FreeConfigInfo(); DbgPrint("MiniFilter: Driver not started. ERROR FltStartFiltering - %08x\n", status); return status; } DbgPrint("MiniFilter: Filter was started and configured."); return STATUS_SUCCESS; } 

Registration of the mini filter is done by calling the FltRegisterFilter function, to which we pass the FilterRegistration structure received at the input of theDriverObject, described earlier, and a link to the variable where the created instance of the filter fileManager.pFilter will be placed. To start the filtering process, you need to call the FltStartFiltering function (fileManager.pFilter).

Also note that the loading of the configuration file and its processing is performed by the following calls ConfigInfo = ReadConfigurationFile (); and ParseConfigurationFile (ConfigInfo), respectively.

Data from the configuration file is converted to the next set of structures.
 typedef struct FILE_REDIRECT_RULE { UNICODE_STRING From; UNICODE_STRING To; struct FILE_REDIRECT_RULE *NextRule; }FileRedirectRule, *PFileRedirectRule; struct PROCESS_CONFIGURATION_RULE { UNICODE_STRING ProcessName; struct FILE_REDIRECT_RULE *Rule; }; typedef struct CONFIGURATION_MAP { struct PROCESS_CONFIGURATION_RULE ProcessRule; struct REDIRECT_MAP *NextItem; }ConfigurationMap ,*PConfigurationMap; 


The head structure is the CONFIGURATION_MAP, which stores a link to the description of the ProcessRule process, as well as a pointer to the next element. In turn, PROCESS_CONFIGURATION_RULE stores a link to the process name and directly to the structure of I / O redirection rules, which, like REDIRECT_MAP, is a linked list.

Consider the driver unloading function, it is quite simple:
 VOID OnUnload( IN PDRIVER_OBJECT DriverObject ) { FltUnregisterFilter(fileManager.pFilter); FreeConfigInfo(); DbgPrint("MiniFilter: Unloaded"); } 


Here we just remove the filter registration and release all our configuration structures.

Now let's turn to the most interesting part, namely the function that redirects I / O operations. Since we have a fairly simple driver, we will do this directly in the PreFileOperationCallback.
 FLT_PREOP_CALLBACK_STATUS PreFileOperationCallback ( __inout PFLT_CALLBACK_DATA Data, __in PCFLT_RELATED_OBJECTS FltObjects, __deref_out_opt PVOID *CompletionContext ) { NTSTATUS status; PFILE_OBJECT FileObject; PFileRedirectRule redirectRuleItem; PFLT_FILE_NAME_INFORMATION pFileNameInformation; PConfigurationMap rule; UNICODE_STRING fullPath; UNICODE_STRING processName; PWCHAR Volume; FLT_PREOP_CALLBACK_STATUS returnStatus = FLT_PREOP_SUCCESS_NO_CALLBACK; if(FLT_IS_FS_FILTER_OPERATION(Data)) { return FLT_PREOP_SUCCESS_NO_CALLBACK; } 


We define the main variables, and also check whether we have already received something filtered, and if so, then this operation should be skipped, otherwise we can get a recursion of calls, which may entail a BSOD.

 if (FltObjects->FileObject != NULL && Data != NULL) { FileObject = Data->Iopb->TargetFileObject; if(FileObject != NULL && Data->Iopb->MajorFunction == IRP_MJ_CREATE) { 


Here we refer to the data structures obtained from FilterManager. PFLT_CALLBACK_DATA structure - stores data on the current I / O operation, the FilterManager is guided by the fields of this structure when accessing the file system. Accordingly, if we want to change the behavior of Windows when accessing files or directories, we need to reflect this in PFLT_CALLBACK_DATA. More specifically, we are interested in the Data-> Iopb-> TargetFileObject field, using it we can get the path to the file in the current section and later change it if necessary, thus changing the behavior of the OS. PCFLT_RELATED_OBJECTS - contains objects associated with this input / output operation, such as a link to a file, a section, and so on. Let us verify that the elements of the structure we need are filled. Also check that the function in the context of which we are running is really MJ_CREATE.

 processName.Length = 0; processName.MaximumLength = NTSTRSAFE_UNICODE_STRING_MAX_CCH * sizeof(WCHAR); processName.Buffer = ExAllocatePoolWithTag(NonPagedPool, processName.MaximumLength,CURRENT_PROCESS_TAG); RtlZeroMemory(processName.Buffer, processName.MaximumLength); status = GetProcessImageName(&processName); 


In this section of code, we allocate memory for the path and process name. I can not imagine what size the string will be, so we select the maximum possible WCHAR string. I will not consider the source code for GetProcessImageName, I will only say that it returns the full path to the file in the following form: \ Device \ HarddiskVolume4 \ Windows \ notepad.exe. Ie section, well, actually, the path to the file.
  if(NT_SUCCESS(status)) { if(LoggingEnabled()== 1) { DbgPrint("MiniFilter: Process: %ws", processName.Buffer); } } else { return FLT_PREOP_SUCCESS_NO_CALLBACK; } rule = FindRuleByProcessName(&processName,GetRedirectionMap()); 


The FindRuleByProcessName function, if successful, returns the first element of the linked list containing the redirection rules for the current process, otherwise NULL.

 ExFreePool(processName.Buffer); if(rule != NULL){ if(LoggingEnabled() == 1) { DbgPrint("MiniFilter: File name %ws", FileObject->FileName.Buffer); } redirectRuleItem = rule->ProcessRule.Rule; 

We release unnecessary memory and check that we have received some object, but not NULL. redirectRuleItem = rule-> ProcessRule.Rule - call to the first rule for this process.
 while(redirectRuleItem) { if(RtlCompareUnicodeString(&FileObject->FileName ,&redirectRuleItem->From, FALSE) == 0) { status = FltGetFileNameInformation( Data, FLT_FILE_NAME_NORMALIZED | FLT_FILE_NAME_QUERY_ALWAYS_ALLOW_CACHE_LOOKUP, &pFileNameInformation ); 

We start the pass by all the rules for this process, compare the link to the current file with what we have in the configuration. If it coincides, we try to get additional information about the file, for example, to which section it belongs. To do this, use the FltGetFileNameInformation function.
 if(NT_SUCCESS(status)) { fullPath.Length = 0; fullPath.MaximumLength = NTSTRSAFE_UNICODE_STRING_MAX_CCH * sizeof(WCHAR); fullPath.Buffer = ExAllocatePoolWithTag(NonPagedPool, fullPath.MaximumLength, FULL_PATH_TAG); RtlZeroMemory(fullPath.Buffer, fullPath.MaximumLength); Volume = wcssplt(pFileNameInformation->Volume.Buffer, redirectRuleItem->From.Buffer ); RtlAppendUnicodeToString(&fullPath, Volume); RtlAppendUnicodeToString(&fullPath, redirectRuleItem->To.Buffer); ExFreePool(Volume); ExFreePool(FileObject->FileName.Buffer); 


If everything is ok, we try to select a section, after which we form the final line. Final Path = Current Section + Where to send an I / O request.
  FileObject->FileName.Length = fullPath.Length; FileObject->FileName.MaximumLength = fullPath.MaximumLength; FileObject->FileName.Buffer = fullPath.Buffer; Data->Iopb->TargetFileObject->RelatedFileObject = NULL; Data->IoStatus.Information = IO_REPARSE; Data->IoStatus.Status = STATUS_REPARSE; DbgPrint("MiniFilter: Redirect done %ws", fullPath.Buffer); return FLT_PREOP_COMPLETE; 

Next, we configure the system structures so that the File Manager processes the request once again, but only now in a different way. For this, it is important to set the following values ​​for the Data-> IoStatus.Information = IO_REPARSE and Data-> IoStatus.Status = STATUS_REPARSE; fields, as well as specify the new path to the FileObject-> FileName.Buffer = fullPath.Buffer ;. As a result of the function, we return FLT_PROP_COMPLETE.

  } } redirectRuleItem = redirectRuleItem->NextRule; } } } } return FLT_PREOP_SUCCESS_NO_CALLBACK; } 

Do not forget to go to the next item in the list of redirects. FLT_PREOP_SUCCESS_NO_CALLBACK is returned if you do nothing with the current Filter Manger operation.

At the moment, redefinition of I / O works only within one section, as soon as I debug the version with support for several sections, I’ll post.

It is necessary to install a mini filter using a specially designed inf file, an example which you will find in the source code for this article.

The configuration file is as follows:
 #minifilter config start { #logging : off #process : \Device\HarddiskVolume4\Windows\notepad.exe { #rule : redirect { #from : \test.txt #to : \data\test.txt } #rule : redirect { #from : \ioman.log #to : \IRCCL.ini } } } 

The file must be located in the root of drive C, the name must be: minifilter.conf.

So we have the ability to redirect file I / O requests, however, in addition to, say, implementing a mechanism to deny access to a file is quite simple. It is necessary to select the file, access to which should be denied and specify the following value for the field of the system structure Data-> IoStatus.Status = STATUS_ACCESS_DENIED ;. Remember to return FLT_PROP_COMPLETE as the result of a function.

To start or stop the service I use KMD Manager. For the analysis of memory leaks PoolTag. As for debugging, you can use DbgView, however, for Windows Vista and higher, debugging messages need to be activated, for this you need to create a DWORD key in the registry using the following path HKEY_LOCAL_MACHINE \ SYSTEM \ CurrentControlSet \ Control \ Session Manager \ Debug Print Filter with the name DEFAULT and value eight.

To run the driver in the 64-bit version of Windows 7, you will need to disable driver signature verification, to do this, restart the computer, press F8 at system start and select Disable Driver Signature Enforcement, or use the Driver Signature Enforcement Overrider (DSEO) utility. This utility will allow you to activate the test debug mode of the drivers and sign the necessary driver with a fake certificate, which ultimately will allow using it without any problems.

Regardless of whether logging is enabled or not, after starting the service in DbgView you should observe something similar.


And so our driver will look like in DeviceTree


I can add that the code is still quite raw and needs some work, but in general it functions normally. Actually, if you have a BSOD, I'm not guilty). Tested only on Windows 7 X86 and Windows 7 IA64.

Link to source codes and utilities: publish.rar

What to read:
  1. MSDN Documentation
  2. File Systems and Filters Blog


Ps. I want to note that I am not a professional in system programming, so this article does not pretend to be complete. By the nature of my business I am engaged in development under Microsoft Dynamics CRM (.net, asp.net, etc.).

I will be glad to your comments.

Source: https://habr.com/ru/post/176739/


All Articles