There are several filter drivers on Windows, ranging from Ndis packet filters to specific PnP device filters, filesystem filters, etc. This article mainly talks about the Mini-filter driver, which can be attached to the local disk drive, filter all disk devices in the system, and realize the function of write protection to a specific disk.
Introduction
The article requires essential Windows driver development and C/C++ knowledge. However, it may also interest people without Windows driver development experience. This article shows how to develop a simple file system Mini-filter driver.
The demo driver block writes for all the USB-type devices connected to the system. However, the user can modify the driver to block/allow the device to be connected to any bus.
File systems are targets for I/O operations to access files. Windows supports several file systems. The new technology file system (NTFS) is its native file system. There exist other types of file systems, such as Physical file systems (UDFS, CDFS, FAT), Network redirectors (RDR, NWRDR, WebDAV), and Special file systems (Npfs, Msfs). A Minifilter driver intercepts I/O operations sent between the I/O manager and the file system driver.
Background
Windows provide a legacy filter driver called the Filter manager implemented as a legacy filter driver. Each Mini-filter has its Altitude, which determines its relative position in the device stack. The filter manager receives the IRP, and then the filter manager calls upon the mini-filter it's managing, in descending order of the Altitude. The Altitude is an infinite-precision string interpreted as a decimal number. A filter driver with a low numerical altitude is loaded into the I/O stack below a filter driver with a higher numerical value.
Simple Mini-Filter Driver
Prerequisite
In Visual Studio 2019, locate the Create New Project and find the template to use Empty WDM Driver. You can download the passthrough sample from GitHub, remove non-relevant code, and start writing. For reference, follow the attached source code.
DriverEntry
The I/O Manager calls a driver's DriverEntry
routine when the driver is loaded. In NT, only one driver instance is loaded, regardless of the number of devices, the driver will control. Thus the DriverEntry
will be called first and one time. It is called at IRQL PASSIVE_LEVEL
and in the system process context.
extern "C"
NTSTATUS
DriverEntry (
__in PDRIVER_OBJECT DriverObject,
__in PUNICODE_STRING RegistryPath
)
{
NTSTATUS status;
UNREFERENCED_PARAMETER( RegistryPath );
KdPrint(("PocMiniFilter!DriverEntry: Entered\n") );
MiniFilterData.DriverObject = DriverObject;
status = FltRegisterFilter( DriverObject,
&FilterRegistration,
&MiniFilterData.Filter );
FLT_ASSERT( NT_SUCCESS( status ) );
if (NT_SUCCESS( status )) {
status = FltStartFiltering( MiniFilterData.Filter );
if (!NT_SUCCESS( status )) {
FltUnregisterFilter( MiniFilterData.Filter );
}
}
return status;
}
The Mini-filter driver must register as a mini-filter with the filter manager, specifying various settings, such as what operations it wishes to intercept. The driver sets up appropriate structures and then calls the FltRegisterFilter
routine to register. If victorious, the driver can do further initialization as needed and finally call FltStartFiltering
to start filtering operations.
Note: The driver doesn't need to set up dispatch routines independently because the driver is not directly in the I/O path; the filter manager is.
The FltRegisterFilter
has the following prototype:
NTSTATUS
FltRegisterFilter (
_In_ PDRIVER_OBJECT Driver,
_In_ CONST FLT_REGISTRATION *Registration,
_Outptr_ PFLT_FILTER *RetFilter
);
The required FLT_REGISTRATION
structure provides all the necessary information for registration. There is a lot of information encapsulated in this structure.
ContextRegistration
- An optional pointer to FLT_CONTEXT_REGISTRATION
structure array. it refers to some driver-defined data that can be attached to file system entities, such as files and volumes.
OperationRegistration
- The most important field. This is a pointer to an array of FLT_OPERATION_REGISTRATION
structures, each specifying the operation of interest and pre and post-callback the driver wishes to be called upon.
InstanceCallback
- This callback allows the driver to be notified when an instance is about to be attached to the new volume. The driver may return STATUS_SUCCESS
to attach to STATUS_FLT_DO_NOT_ATTACH
if the driver does not know which to link to this volume.
InstanceSetup Callback
InstanceSetup
routine is called whenever a new instance is created on a volume. This gives us a chance to decide if we need to attach to the volume or not. If this routine is not defined in the registration structure, automatic instances are constantly created.
This article mainly implements the blocking to filter the external media volumes having USB bus type. In the routine, first, get a handle and a file object pointer for the file system volume to which a given mini-filter instance is attached using FltOpenVolume
. A subsequent call to FltClose
must match every successful call to FltOpenVolume
. If a file object pointer is returned in the VolumeFileObject
parameter, the caller must release it when it is no longer needed by calling ObDereferenceObject
. The FltOpenVolume
, if succeeded, returns the file object pointer for the root directory of the volume that will be used in the call to FltDeviceIoControlFile
during the IOCTL_STORAGE_QUERY_PROPERTY
query to obtain the disk information of the current device through the storage adapter.
NTSTATUS
PocMiniFilterInstanceSetup(
__in PCFLT_RELATED_OBJECTS FltObjects,
__in FLT_INSTANCE_SETUP_FLAGS Flags,
__in DEVICE_TYPE VolumeDeviceType,
__in FLT_FILESYSTEM_TYPE VolumeFilesystemType )
{
UNREFERENCED_PARAMETER(Flags);
UNREFERENCED_PARAMETER(VolumeDeviceType);
PAGED_CODE();
NTSTATUS status = STATUS_UNSUCCESSFUL;
HANDLE FsVolumeHandle {};
PFILE_OBJECT FsFileObject = NULL;
STORAGE_PROPERTY_ID PropertyId = StorageAdapterProperty;
STORAGE_DESCRIPTOR_HEADER HeaderDescriptor;
PSTORAGE_PROPERTY_QUERY Query = NULL, buffer = NULL,
pQuery = NULL, OutputBuffer = NULL;
ULONG RetLength, OutputLength, Sizeneeded, SizeRequired;
PSTORAGE_ADAPTER_DESCRIPTOR pStorageDescriptor = NULL;
if (VolumeFilesystemType != FLT_FSTYPE_NTFS) {
KdPrint(("Unknown File system is not supported\n"));
status = STATUS_FLT_DO_NOT_ATTACH;
goto end;
}
else {
KdPrint(("InstanceSetup Attched to (Volume = %p, Instance = %p)\n",
FltObjects->Volume,
FltObjects->Instance));
status = FltOpenVolume(FltObjects->Instance, &FsVolumeHandle, &FsFileObject);
if (!NT_SUCCESS(status)) {
KdPrint(("FltOpenVolume failed with status = 0x%x\n", status));
goto end;
}
Sizeneeded = max (sizeof(STORAGE_DESCRIPTOR_HEADER),
sizeof(STORAGE_PROPERTY_QUERY));
OutputBuffer = (PSTORAGE_PROPERTY_QUERY)
ExAllocatePoolWithTag(NonPagedPool, Sizeneeded, 'VedR');
ASSERT(OutputBuffer != NULL);
RtlZeroMemory(OutputBuffer, Sizeneeded);
Query = (PSTORAGE_PROPERTY_QUERY)OutputBuffer;
Query->PropertyId = PropertyId;
Query->QueryType = PropertyStandardQuery;
status = FltDeviceIoControlFile(FltObjects->Instance,
FsFileObject,
IOCTL_STORAGE_QUERY_PROPERTY,
Query,
sizeof(STORAGE_PROPERTY_QUERY),
&HeaderDescriptor,
sizeof(STORAGE_DESCRIPTOR_HEADER),
&RetLength);
if (!NT_SUCCESS(status)) {
KdPrint(("FltDeviceIoControlFile failed with status = 0x%x\n", status));
goto end;
}
OutputLength = HeaderDescriptor.Size;
ASSERT(OutputLength >= sizeof(STORAGE_DESCRIPTOR_HEADER));
SizeRequired = max(OutputLength, sizeof(STORAGE_PROPERTY_QUERY));
buffer = (PSTORAGE_PROPERTY_QUERY)ExAllocatePoolWithTag
(NonPagedPool, SizeRequired, 'VedR');
ASSERT(buffer != NULL);
RtlZeroMemory(buffer, SizeRequired);
pQuery = (PSTORAGE_PROPERTY_QUERY) buffer;
pQuery->PropertyId = PropertyId;
pQuery->QueryType = PropertyStandardQuery;
status = FltDeviceIoControlFile(FltObjects->Instance,
FsFileObject,
IOCTL_STORAGE_QUERY_PROPERTY,
pQuery,
sizeof(STORAGE_PROPERTY_QUERY),
buffer,
OutputLength,
&RetLength);
if (!NT_SUCCESS(status)) {
KdPrint(("FltDeviceIoControlFile failed with status = 0x%x\n", status));
goto end;
}
pStorageDescriptor = (PSTORAGE_ADAPTER_DESCRIPTOR)buffer;
if (pStorageDescriptor->BusType == BusTypeUsb) {
MiniFilterData.IsTargetDisk = TRUE;
status = STATUS_FLT_DO_NOT_ATTACH;
}
}
end:
if (OutputBuffer != NULL) {
ExFreePoolWithTag(OutputBuffer, 'VedR');
OutputBuffer = NULL;
}
if (buffer != NULL) {
ExFreePoolWithTag(buffer, 'VedR');
buffer = NULL;
}
if (FsVolumeHandle) {
FltClose(FsVolumeHandle);
}
if (FsFileObject != NULL) {
ObDereferenceObject(FsFileObject);
FsFileObject = NULL;
}
return status;
}
The driver will not attach any instance to an external media USB device. Apart from this, there is some code related to manual detachment from an instance that can be stopped at teardown callback. Finally, a driver unload routine is necessary to deregister the driver, who also can be controlled by a flag.
NTSTATUS
PocMiniFilterUnload (
__in FLT_FILTER_UNLOAD_FLAGS Flags
)
{
UNREFERENCED_PARAMETER( Flags );
PAGED_CODE();
KdPrint(("PocMiniFilter!PtUnload: Entered\n") );
FltUnregisterFilter( MiniFilterData.Filter );
return STATUS_SUCCESS;
}
Installation
The proper way to install a file system mini-filter driver is by using an INF file. The INF files are used to install the hardware-based device drivers. But also can be used to install any driver on the windows system. A complete treatment of INF files is beyond the scope of this article.
;
; Copyright (c) Raahul
; PocMiniFilter.inf
[Version]
Signature = "$Windows NT$"
Class = "ActivityMonitor" ;This is determined by the work
;this filter driver does
ClassGuid = {b86dff51-a31e-4bac-b3cf-e8cfe75c9fc2} ;This value is determined by
;the Class
Provider = %ProviderString%
DriverVer = 06/16/2007,1.0.0.1
CatalogFile = PocMiniFilter.cat
[DestinationDirs]
DefaultDestDir = 12
MiniFilter.DriverFiles = 12 ;%windir%\system32\drivers
;;
;; Default install sections
;;
[DefaultInstall]
OptionDesc = %ServiceDescription%
CopyFiles = MiniFilter.DriverFiles
[DefaultInstall.Services]
AddService = %ServiceName%,,MiniFilter.Service
;;
;; Default uninstall sections
;;
[DefaultUninstall]
DelFiles = MiniFilter.DriverFiles
[DefaultUninstall.Services]
DelService = %ServiceName%,0x200 ;Ensure service is stopped before deleting
;
; Services Section
;
[MiniFilter.Service]
DisplayName = %ServiceName%
Description = %ServiceDescription%
ServiceBinary = %12%\%DriverName%.sys ;%windir%\system32\drivers\
Dependencies = "FltMgr"
ServiceType = 2 ;SERVICE_FILE_SYSTEM_DRIVER
StartType = 3 ;SERVICE_DEMAND_START
ErrorControl = 1 ;SERVICE_ERROR_NORMAL
LoadOrderGroup = "FSFilter Activity Monitor"
AddReg = MiniFilter.AddRegistry
;
; Registry Modifications
;
[MiniFilter.AddRegistry]
HKR,,"SupportedFeatures",0x00010001,0x3
HKR,"Instances","DefaultInstance",0x00000000,%DefaultInstance%
HKR,"Instances\"%Instance1.Name%,"Altitude",0x00000000,%Instance1.Altitude%
HKR,"Instances\"%Instance1.Name%,"Flags",0x00010001,%Instance1.Flags%
;
; Copy Files
;
[MiniFilter.DriverFiles]
%DriverName%.sys
[SourceDisksFiles]
PocMiniFilter.sys = 1,,
[SourceDisksNames]
1 = %DiskId1%,,,
;;
;; String Section
;;
[Strings]
ProviderString = "Raahul"
ServiceDescription = "PocMiniFilter Mini-Filter Driver"
ServiceName = "PocMiniFilter"
DriverName = "PocMiniFilter"
DiskId1 = "PocMiniFilter Device Installation Disk"
;Instances specific information.
DefaultInstance = "PocMiniFilter Instance"
Instance1.Name = "PocMiniFilter Instance"
Instance1.Altitude = "370030"
Instance1.Flags = 0x0 ; Allow all attachments
Let's start examining the critical aspect of the INF file.
[Version]: section is mandatory in an INF.
The signature directive must be set to "$Windows NT$
." the call
and classguid
directives are mandatory and specify the class to which this driver belongs. The complete list of classes can be stored in the registry under the key "Computer\HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Class," where a GUID
uniquely identifies each type.
DefaultInstall Section: Indicates what operations should execute as part of "running" this INF. The default install section can also be used to install a file system mini-filter driver.
For more information on how to modify the INF, please look at https://docs.microsoft.com/en-us/windows-hardware/drivers/ifs/creating-an-inf-file-for-a-file-system-driver.
Installing the Driver
Once the INF file is adequately modified and the driver code compiled, it is ready to be installed. The simplest install method is to copy the driver package (SYS, INF, and CAT files) to the target system, then right-click on the INF file explorer and select INSTALL. This will run the INF.
NOTE: If in INF, the start type of driver is 0, then reboot the system, and the driver gets loaded at boot time. For this sample, the start type of driver is Demand start, so no reboot is required.
At this point, the POC mini-filter driver is installed and can be loaded with the fltmc
command line tool (use elevated command prompt) and run.
C:\> fltmc load pocminifilter
As soon as the driver is loaded into the system, the InstanceSetup
callback will identify the device bus type and act accordingly.
To unload the driver from the system, use the following command:
c:\> fltmc unload pocminifilter.sys
Test Environment Preparation
You can temporarily disable the digital driver signature check to start a Windows driver test. In Windows 11, you can do this in the following way:
- bcdedit /set nointegritychecks on
- bcdedit /set debug on
- bcdedit /set testsigning on
- Reboot the machine and,
- Hold the Shift button and choose the Restart option in the main Windows menu.
- Select Troubleshoot -> Advanced Options -> Startup Settings -> Restart
- In Startup Settings, push F7 to choose the Disable driver signature enforcement option.
That's It & RFC
That's it! Check it out and post any questions or comments about anything in the article. I hope you find it valuable. Please feel free to point out the mistakes made. I'll try to rectify them in future versions.
Points of Interest
In one of my projects, the task was to block certain media types from attaching filter driver instances.
History
- 12th September, 2022: First publication