Minifilters
Filter Manager architecture, FLT_REGISTRATION, pre- and post-operation callbacks, altitude ordering, FltSendMessage for kernel-to-user communication, and file system monitoring for detection
A ransomware sample starts encrypting files across the filesystem. Windows Defender and a third-party EDR both intercept every file write through their minifilter drivers at the file system layer, scan the data being written, and block or quarantine the operation before it reaches the disk. Understanding minifilter mechanics explains how AV scan-on-write works, why ransomware that writes via direct NTFS volume I/O instead of file system APIs can bypass some filters, and how EDR products communicate suspicious file-write events to their user-mode analysis components.
Filter Manager Architecture
File System I/O Path with Minifilters:
User mode:
CreateFile("victim.docx") → WriteFile(...) → ReadFile(...)
Kernel I/O stack (top to bottom):
I/O Manager generates IRP_MJ_WRITE IRP
Filter Manager (FltMgr.sys) ─── highest-level file system driver
|
+─ Minifilter altitude 420000: Backup filter (pre-op → post-op)
|
+─ Minifilter altitude 385000: EDR scanner (pre-op → post-op)
| |
| +─ Pre-op: read buffer, hash content, block if malicious
| +─ Post-op: log result, send event to user-mode service
|
+─ Minifilter altitude 320000: AV scan-on-write (pre-op → post-op)
|
+─ Minifilter altitude 140000: Encryption filter (post-op)
|
v
NTFS / FAT32 / exFAT file system driver (base driver)
|
v
Storage stack → disk
The Filter Manager (fltmgr.sys) is a Windows-supplied infrastructure driver that manages the minifilter stack. Minifilters no longer need to manage legacy filter driver complexity (chaining, pass-through, etc.) — they register with the Filter Manager and the Filter Manager handles IRP forwarding based on altitude order.
Altitude Ranges
| Altitude Range | Filter Category | Examples |
|---|---|---|
| 420,000 – 429,999 | FSFilter Backup | Backup software, shadow copies |
| 400,000 – 409,999 | FSFilter Copy Protection | DRM, copy protection |
| 360,000 – 389,999 | FSFilter Undelete | Recycle bin, undelete tools |
| 320,000 – 329,999 | FSFilter Anti-Virus | Windows Defender, third-party AV |
| 260,000 – 269,999 | FSFilter Replication | File replication services |
| 200,000 – 209,999 | FSFilter Continuous Backup | VSS, continuous data protection |
| 180,000 – 189,999 | FSFilter Content Screener | DLP, data leak prevention |
| 140,000 – 149,999 | FSFilter Encryption | EFS, BitLocker, third-party encryption |
| 100,000 – 109,999 | FSFilter Compression | Filesystem compression |
FLT_REGISTRATION — Minifilter Registration
// Operation callbacks array
const FLT_OPERATION_REGISTRATION Callbacks[] = {
{
.MajorFunction = IRP_MJ_CREATE,
.PreOperation = PreCreateCallback,
.PostOperation = PostCreateCallback
},
{
.MajorFunction = IRP_MJ_WRITE,
.PreOperation = PreWriteCallback,
.PostOperation = NULL
},
{
.MajorFunction = IRP_MJ_READ,
.PreOperation = PreReadCallback,
.PostOperation = NULL
},
{ IRP_MJ_OPERATION_END } // must be last
};
// Registration structure passed to FltRegisterFilter
const FLT_REGISTRATION FilterRegistration = {
.Size = sizeof(FLT_REGISTRATION),
.Version = FLT_REGISTRATION_VERSION,
.Flags = 0,
.ContextRegistration = NULL,
.OperationRegistration = Callbacks,
.FilterUnloadCallback = FilterUnload,
.InstanceSetupCallback = InstanceSetup,
.InstanceQueryTeardown = InstanceQueryTeardown,
.InstanceTeardownStart = NULL,
.InstanceTeardownComplete = NULL,
};
PFLT_FILTER g_Filter;
NTSTATUS DriverEntry(PDRIVER_OBJECT DrvObj, PUNICODE_STRING RegPath) {
NTSTATUS status;
// Register with Filter Manager
status = FltRegisterFilter(DrvObj, &FilterRegistration, &g_Filter);
if (!NT_SUCCESS(status)) return status;
// Start filtering (begin receiving IRP callbacks)
status = FltStartFiltering(g_Filter);
if (!NT_SUCCESS(status)) {
FltUnregisterFilter(g_Filter);
return status;
}
return STATUS_SUCCESS;
}
Pre and Post Operation Callbacks
Pre-operation callbacks can inspect or modify the I/O before it proceeds. They return one of three key values:
| Return Value | Meaning |
|---|---|
| FLT_PREOP_SUCCESS_WITH_CALLBACK | Allow, and also call my post-operation callback when done |
| FLT_PREOP_SUCCESS_NO_CALLBACK | Allow, skip post-operation callback |
| FLT_PREOP_COMPLETE | Complete the IRP now with the status in Data->IoStatus; do NOT pass to lower drivers or base file system |
| FLT_PREOP_PENDING | Pend the IRP (async processing); call FltCompletePendedPreOperation later |
FLT_PREOP_CALLBACK_STATUS PreWriteCallback(
PFLT_CALLBACK_DATA Data,
PCFLT_RELATED_OBJECTS FltObjects,
PVOID *CompletionContext)
{
// Get process context: who is writing?
PEPROCESS proc = IoThreadToProcess(Data->Thread);
HANDLE pid = PsGetProcessId(proc);
// Get write buffer
PVOID writeBuffer = NULL;
ULONG writeLength = Data->Iopb->Parameters.Write.Length;
if (Data->Iopb->Parameters.Write.MdlAddress) {
writeBuffer = MmGetSystemAddressForMdlSafe(
Data->Iopb->Parameters.Write.MdlAddress,
NormalPagePriority);
} else {
writeBuffer = Data->Iopb->Parameters.Write.WriteBuffer;
}
// Simple entropy check for ransomware detection
if (writeBuffer && writeLength > 4096) {
double entropy = ComputeEntropy(writeBuffer, writeLength);
if (entropy > 7.8) {
// High entropy write — suspicious (possible encryption)
SendAlertToUserMode(FltObjects->FileObject, pid, entropy);
// Optional block: uncomment to deny the write
// Data->IoStatus.Status = STATUS_ACCESS_DENIED;
// Data->IoStatus.Information = 0;
// return FLT_PREOP_COMPLETE;
}
}
return FLT_PREOP_SUCCESS_NO_CALLBACK;
}
Kernel-to-User-Mode Communication via FltSendMessage
Minifilters communicate with their user-mode companion service through Filter Manager communication ports. The kernel side creates a server port; the user-mode service connects as a client. The kernel driver sends events; the user-mode component can send commands back:
// Kernel side: create communication port
PFLT_PORT g_ServerPort;
PFLT_PORT g_ClientPort;
NTSTATUS ConnectNotify(PFLT_PORT ClientPort, PVOID ServerPortCookie,
PVOID ConnectionContext, ULONG SizeOfContext,
PVOID *ConnectionPortCookie) {
g_ClientPort = ClientPort;
return STATUS_SUCCESS;
}
// Create the server port during DriverEntry:
UNICODE_STRING portName = RTL_CONSTANT_STRING(L"\ScannerPort");
OBJECT_ATTRIBUTES oa;
InitializeObjectAttributes(&oa, &portName, OBJ_KERNEL_HANDLE | OBJ_CASE_INSENSITIVE,
NULL, NULL);
FltCreateCommunicationPort(g_Filter, &g_ServerPort, &oa,
NULL, ConnectNotify, DisconnectNotify, MessageNotify, 1);
// Send event to user mode from pre-write callback:
typedef struct _SCAN_EVENT {
HANDLE Pid;
ULONG WriteLength;
double Entropy;
WCHAR FileName[256];
} SCAN_EVENT;
VOID SendAlertToUserMode(PFILE_OBJECT FileObj, HANDLE Pid, double Entropy) {
if (!g_ClientPort) return;
SCAN_EVENT evt = { .Pid = Pid, .Entropy = Entropy };
ULONG replyLen = 0;
FltSendMessage(g_Filter, &g_ClientPort, &evt, sizeof(evt),
NULL, &replyLen, NULL);
}
Minifilter Bypass Techniques
| Bypass Technique | How It Works | Detection / Mitigation |
|---|---|---|
| Direct NTFS volume write | Open volume (\\.\C:) and write raw sectors via DeviceIoControl(FSCTL_*). Bypasses file system path, Filter Manager never sees file-level IRPs. | Volume-level writes to NTFS metadata sectors are suspicious; Sysmon file events miss these; requires volume-level monitoring |
| Kernel driver DKOM | Remove minifilter from FLT_FILTER linked list in kernel. Filter Manager skips it. | Requires ring-0 access; detectable by comparing fltmc output to kernel enumeration |
| FltUnregisterFilter via BYOVD | Call FltUnregisterFilter on the target filter's PFLT_FILTER handle by finding it in kernel memory and calling the export | FltUnregisterFilter triggers driver unload event; monitor for unexpected filter deregistration |
| Altitude gap exploitation | Load a driver at altitude lower than the AV scanner (e.g., 100,000) to pre-encrypt data before the AV sees it | Monitor new filter driver registrations via Event ID 7045 and fltmc |
| Rename-then-write | Some older AV minifilters only scanned on specific extension matches; rename the file to .tmp before writing encrypted content, rename back after | Modern AV scans content regardless of extension; Sysmon EventID 11 (FileCreate) and 23 (FileDelete) chains catch rename-write-rename |
Q & A
How does a minifilter pre-operation callback differ from a legacy filter driver for handling async I/O?
Legacy filter drivers (pre-Filter Manager) had to manually handle IRP_MJ_WRITE, set up completion routines, and carefully manage the IRP stack across asynchronous completion — a significant source of bugs and system instability. The Filter Manager model (minifilters) abstracts this via the pre/post callback split and the FLT_PREOP_PENDING return value. When a minifilter's pre-operation callback needs to do asynchronous work (e.g., send the write buffer to user mode for scanning and wait for the verdict), it returns FLT_PREOP_PENDING, which causes the Filter Manager to hold the I/O operation without completing it and without blocking any kernel thread. The minifilter spawns a system worker thread to do the scan, and when the verdict arrives, calls FltCompletePendedPreOperation() with either FLT_PREOP_SUCCESS_NO_CALLBACK (allow) or sets Data->IoStatus.Status and calls with FLT_PREOP_COMPLETE (deny). The original I/O operation then resumes or fails — transparent to the user-mode caller. This model prevents a minifilter from blocking the calling thread while waiting for asynchronous verdicts, which is critical for AV products doing cloud lookups or machine-learning model inference. The risk for ransomware detection is timeout: if the verdict is slow and the filter has a timeout configured, the default behavior may be to allow the write after the timeout expires. Production AV products handle this by doing local fast-path analysis synchronously (entropy check, header magic, extension check) and only escalating uncertain cases to async cloud lookup.
Can a minifilter read the contents of a file being written by ransomware?
Yes, but the mechanics depend on whether the write uses memory-mapped I/O or standard WriteFile. For standard WriteFile: the pre-write callback receives the write buffer either as a direct pointer (Data->Iopb->Parameters.Write.WriteBuffer) or as an MDL (Data->Iopb->Parameters.Write.MdlAddress). If the buffer address is in user-mode virtual memory, the minifilter must use MmGetSystemAddressForMdlSafe to get a kernel-accessible pointer to the locked pages, or it must probe the user-mode address with ProbeForRead inside a try/except block. For memory-mapped file writes: the mechanism is different. When a process writes via a memory-mapped view (MapViewOfFile + direct memory writes), the actual I/O appears as paging operations (IRP_MJ_WRITE with paging I/O flag set in the IRP flags). Minifilters can intercept these by checking for FO_WRITE_THROUGH or the IRP_PAGING_IO flag in Data->Iopb->IrpFlags. Some ransomware families specifically use memory-mapped I/O to avoid triggering minifilter scan-on-write for non-paging I/O — they correctly observe that many AV minifilters skip paging I/O for performance reasons. A comprehensive ransomware detection minifilter must handle both paths. Additionally, for read-modify-write patterns (read file, encrypt in memory, write back), the entropy analysis should compare per-file entropy over time rather than evaluating single writes in isolation, since a single block of "already-encrypted" data being re-written looks the same as new ransomware encryption.