Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / Objective-C

Storage Device Restriction Using a Minifilter Driver Approach

4.86/5 (5 votes)
11 Sep 2022CPOL6 min read 7.2K   211  
The fully working sample code explains everything necessary to create a Minifilter driver to block devices connected through an interface.
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.

What is a File System Mini-filter Driver?

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.

Image 1

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.

C++
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;

    //
    //  Register with FltMgr to tell it our callback routines
    //

    status = FltRegisterFilter( DriverObject,
                                &FilterRegistration,
                                &MiniFilterData.Filter );

    FLT_ASSERT( NT_SUCCESS( status ) );

    if (NT_SUCCESS( status )) {

        //
        //  Start filtering i/o
        //

        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:

C++
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.

C++
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));

        // Involve an internal IOCTL to Disk filter and get the 
        // desired target disk information
        // If it is of interest, then attach to it 
        // and start filtering else do not attach.
        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;
        }
        
        // Found the enough size now query property with updated size.
        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.

C++
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:

  1. bcdedit /set nointegritychecks on
  2. bcdedit /set debug on
  3. bcdedit /set testsigning on
  4. Reboot the machine and,
  5. Hold the Shift button and choose the Restart option in the main Windows menu.
  6. Select Troubleshoot -> Advanced Options -> Startup Settings -> Restart
  7. 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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)