Hey everyone! Hope you’re all doing well. As always, I was looking for an interesting Windows internals topic to blog about. I google-d around, asked ChatGPT for ideas, but most of the suggestions were pretty generic.. Things like system calls, PFNs, and PTEs, which already have great write-ups from other researchers. Finally, I came across Mini-Filter Drivers. They seemed intriguing, and while there are a few posts on the topic (James Forshaw has a great write-up on finding bugs in them ❤), I wanted to understand what they are, How they work, How to write such drivers, How they are different from the legacy drivers etc etc., And here it is!! Hope it would be useful!
Basically, Mini-Filter Drivers are one of the components of modern Windows file system architecture. They provide a way for developers to monitor and modify file system operations without needing to interact directly with lower-level file system drivers. Mini-filters operate within the file system filter driver framework, allowing them to intercept and process I/O operations in a structured manner.
They sit between the user-mode application and the file system. They leverage the Filter Manager (a Microsoft-provided kernel-mode component that simplifies interaction with the file system stack). This architecture allows mini-filters to attach dynamically to volumes and intercept I/O requests at various levels. This way it ensures deterministic load order, controlled request routing, and isolation between filters.
You have to first understand what File System Filter Drivers are, before getting to know about Mini-Filter Drivers so go read about them then come back to this post.. JK, I’ll give a short introduction of them. Microsoft quotes File System Filter Drivers as:
“A file system filter driver can filter I/O operations for one or more file systems or file system volumes. Depending on the nature of the driver, filter can mean log, observe, modify, or even prevent. Typical applications for file system filter drivers include antivirus utilities, encryption programs, and hierarchical storage management systems.”
TL;DR, Filter driver can inspect and modify almost any IO request sent to a file system. A simplified request flow would look something like this,
When a user-mode application requests to interact with a file (e.g., opening, reading, or writing), the request transitions into kernel mode, where the I/O Manager processes it and generates an I/O Request Packet (IRP). This IRP is then passed through a stack of drivers before reaching the file system driver. As shown in the diagram, the flow follows:
**ntfs.sys**
) – The final destination of the IRP, where the file system driver handles the operation and interacts with the storage device.Filter drivers, like those labeled as “Legacy Filter Driver A” and “Legacy Filter Driver B,” register for specific I/O requests and can choose to:
Since IRPs do not automatically propagate down the stack, a filter driver must explicitly pass the IRP along if further processing is required. Otherwise, it can complete the IRP itself, preventing lower drivers (including the file system driver) from seeing the request. If a filter driver wants to inspect or modify the response, it should register a completion routine. This design allows filter drivers to monitor and control file system operations, making them useful for applications like antivirus software, encryption systems, and monitoring tools.
Legacy filter drivers have several drawbacks that impact system stability, performance, and compatibility. Managing their position in the device stack is complex, and conflicts often arise when multiple filter drivers attempt to process the same I/O request. Poorly implemented filter drivers can cause system crashes, and inefficient handling of IRPs may introduce unnecessary latency. These issues make it difficult to maintain a reliable and efficient filtering mechanism, especially when different software components interact with the same file system requests.
To address these limitations, Microsoft introduced the filter manager model. The filter manager (fltmgr.sys) is a driver that ships with Windows and provides a standardized framework for handling file system filtering operations. Instead of requiring developers to manually manage filter driver stacking, the filter manager abstracts this process and ensures that requests are handled in a controlled manner.
This is when Mini-Filter Drivers comes into play.. With this approach, developers can write minifilters, which are more lightweight and easier to implement compared to legacy filter drivers. The filter manager intercepts requests destined for the file system and distributes them to the minifilters registered in the system (Refer to the below mentioned image for reference). Unlike legacy filter drivers, minifilters operate within a structured, sorted stack managed by the filter manager, reducing conflicts and improving system stability.
In this model, when a user-mode application interacts with a file, the request is first handled by the I/O Manager, which is responsible for processing all I/O requests in Windows. Instead of directly passing the request to legacy filter drivers, as in the older model, the I/O Manager forwards the request to the Filter Manager (fltmgr.sys) in kernel mode.
The Filter Manager is responsible for managing and organising mini-filter drivers that are registered to intercept file system operations. By the way, If you guys noticed.. In the above image, you could see a mention of altitude. What are they? Why they are used here? Well, Let’s talk about them.The main reason for Mini-Filter Drivers being organised is because of altitudes, which define their position in the filter stack.
Altitudes are numerical values assigned to minifilters to determine their order of execution when processing I/O requests. A higher altitude means the minifilter is positioned closer to the top of the stack and processes requests before lower-altitude mini-filters. Altitudes are managed by Microsoft, with ranges defined for load order groups like FSFilter Anti-Virus or FSFilter Encryption, as per Load Order Groups and Altitudes for Minifilter Drivers — Windows drivers | Microsoft Learn.
NOTE: Pre-operation callbacks are called from highest to lowest altitude, while post-operation callbacks are processed in reverse, ensuring ordered processing.
Also you can list the Mini-Filters and their altitudes in your machine using the powershell command “fltmc filters”.
Microsoft ensures security with these.. For example, An antivirus software with high altitude would operate before other types of filters, while encryption or backup filters are placed lower to act after security checks. Now going back to the flow chart (1st & 2nd step will be same as of the legacy filter drivers):
3. Mini-filter B (altitude: 309000) — This mini-filter is positioned at the highest altitude, meaning it intercepts the request before the lower-altitude mini-filters.
4. Mini-filter B (altitude: 268000) — This mini-filter operates at a lower altitude than the first Mini-filter B but still processes the request before Mini-filter C.
5. Mini-filter C (altitude: 145000) — This mini-filter is positioned at the lowest altitude in the stack and processes the request last before it reaches the Filesystem Driver (e.g., ntfs.sys).
The communication between the Mini-Filter Drivers and the user-mdoe applications is established using Filter Communication Ports, which allow secure message passing between kernel-mode drivers and user-mode processes. Microsoft provides several APIs for this.
For Kernel Mode (Mini-filter Driver):
User Mode (Application):
Let’s write a simple Mini-Filter Driver for better understanding. Made using LLMs but it’s quite easy to understand once you are aware of the theory. I mean, MSDN explains each of the following functions in greate detail so I won’t be explaining the code much.. rather I have shared the associated MSDN links to each of those functions (see above).
#include <fltKernel.h>
PFLT_FILTER g_FilterHandle = NULL;
PFLT_PORT g_ServerPort = NULL;
PFLT_PORT g_ClientPort = NULL;
// Connection callback
NTSTATUS ConnectNotifyCallback(
PFLT_PORT ClientPort,
PVOID ServerPortCookie,
PVOID ConnectionContext,
ULONG SizeOfContext,
PVOID *ConnectionPortCookie
) {
UNREFERENCED_PARAMETER(ServerPortCookie);
UNREFERENCED_PARAMETER(ConnectionContext);
UNREFERENCED_PARAMETER(SizeOfContext);
g_ClientPort = ClientPort;
return STATUS_SUCCESS;
}
// Disconnection callback
VOID DisconnectNotifyCallback(PVOID ConnectionPortCookie) {
UNREFERENCED_PARAMETER(ConnectionPortCookie);
if (g_ClientPort) {
FltCloseClientPort(g_FilterHandle, &g_ClientPort);
g_ClientPort = NULL;
}
}
// Message callback
NTSTATUS MessageNotifyCallback(
PVOID PortCookie,
PVOID InputBuffer,
ULONG InputBufferLength,
PVOID OutputBuffer,
ULONG OutputBufferLength,
PULONG ReturnOutputBufferLength
) {
UNREFERENCED_PARAMETER(PortCookie);
if (InputBufferLength < sizeof(CHAR)) {
return STATUS_INVALID_PARAMETER;
}
DbgPrint("Mini-Filter received: %s\n", (char*)InputBuffer);
if (OutputBuffer && OutputBufferLength >= sizeof("ACK")) {
RtlCopyMemory(OutputBuffer, "ACK", 3);
*ReturnOutputBufferLength = 3;
}
return STATUS_SUCCESS;
}
// Create communication port
NTSTATUS CreateCommunicationPort() {
OBJECT_ATTRIBUTES objAttr;
UNICODE_STRING portName = RTL_CONSTANT_STRING(L"\\MyFilterPort");
InitializeObjectAttributes(&objAttr, &portName, OBJ_KERNEL_HANDLE, NULL, NULL);
PSECURITY_DESCRIPTOR sd;
FltBuildDefaultSecurityDescriptor(&sd, FLT_PORT_ALL_ACCESS);
NTSTATUS status = FltCreateCommunicationPort(
g_FilterHandle,
&g_ServerPort,
&objAttr,
NULL,
ConnectNotifyCallback,
DisconnectNotifyCallback,
MessageNotifyCallback,
1
);
FltFreeSecurityDescriptor(sd);
return status;
}
// Send message from kernel to user-mode
void SendMessageToUserMode() {
if (g_ClientPort) {
CHAR message[] = "Hello from Kernel!";
ULONG replyLength = 0;
NTSTATUS status = FltSendMessage(
g_FilterHandle, g_ClientPort, message, sizeof(message), NULL, &replyLength, NULL
);
if (NT_SUCCESS(status)) {
DbgPrint("Message sent successfully!\n");
}
}
}
// Filter unload routine
NTSTATUS FilterUnload(FLT_FILTER_UNLOAD_FLAGS Flags) {
UNREFERENCED_PARAMETER(Flags);
if (g_ServerPort) {
FltCloseCommunicationPort(g_ServerPort);
}
if (g_FilterHandle) {
FltUnregisterFilter(g_FilterHandle);
}
return STATUS_SUCCESS;
}
// Filter registration structure
CONST FLT_REGISTRATION FilterRegistration = {
sizeof(FLT_REGISTRATION), // Size
FLT_REGISTRATION_VERSION, // Version
0, // Flags
NULL, // Contexts
NULL, // Callbacks
FilterUnload, // Unload routine
NULL, // Instance setup
NULL, // Instance query teardown
NULL, // Instance teardown start
NULL, // Instance teardown complete
NULL, // Generate file name
NULL, // Normalize name component
NULL, // Normalize context cleanup
NULL, // Transaction notification
NULL, // Section notification
NULL // Padding
};
// Driver entry point
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath) {
UNREFERENCED_PARAMETER(DriverObject);
UNREFERENCED_PARAMETER(RegistryPath);
NTSTATUS status = FltRegisterFilter(DriverObject, &FilterRegistration, &g_FilterHandle);
if (!NT_SUCCESS(status)) {
return status;
}
status = CreateCommunicationPort();
if (!NT_SUCCESS(status)) {
FltUnregisterFilter(g_FilterHandle);
return status;
}
return FltStartFiltering(g_FilterHandle);
}
Basically the flow will be,
#include <windows.h>
#include <stdio.h>
#define PORT_NAME L"\\\\.\\MyFilterPort"
int main() {
HANDLE hPort;
DWORD bytesReturned;
char sendBuffer[] = "Hello from User!";
char recvBuffer[256] = {0};
// Open a handle to the filter's communication port
hPort = CreateFileW(PORT_NAME, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);
if (hPort == INVALID_HANDLE_VALUE) {
printf("Failed to connect to filter port. Error: %lu\n", GetLastError());
return 1;
}
printf("Connected to minifilter communication port.\n");
// Send message to minifilter
if (!WriteFile(hPort, sendBuffer, sizeof(sendBuffer), &bytesReturned, NULL)) {
printf("WriteFile failed. Error: %lu\n", GetLastError());
CloseHandle(hPort);
return 1;
}
printf("Sent to kernel: %s\n", sendBuffer);
// Receive response from minifilter
if (ReadFile(hPort, recvBuffer, sizeof(recvBuffer), &bytesReturned, NULL)) {
printf("Received from kernel: %s\n", recvBuffer);
} else {
printf("ReadFile failed. Error: %lu\n", GetLastError());
}
// Close the handle
CloseHandle(hPort);
return 0;
}
Here the flow would be,
I’m intentionally skipping the compilation, loading the driver part because there are a ton of resources & video tutorials available for the same.
That’s all for now! Hope you guys find this post interesting and useful. My next blog would be more of like a walkthrough of a driver from 2021 which was used by some APT Groups to disable AV/EDR Process or Other Protected Processes in General. There are already a couple of posts going through the same but I wanted to write it in a beginner friendly manner so let’s see.
Follow me on LinkedIn, Medium, X.
PEACE!
googleprojectzero.blogspot.com](https://googleprojectzero.blogspot.com/2021/01/hunting-for-bugs-in-windows-mini-filter.html?source=post_page-----391153c945d6---------------------------------------)
www.osr.com](https://www.osr.com/nt-insider/2019-issue1/the-state-of-windows-file-system-filtering-in-2019/?source=post_page-----391153c945d6---------------------------------------)
www.easefilter.com](https://www.easefilter.com/kb/understand-minifilter.htm?srsltid=AfmBOoqGpKK4VVbXlquTNq3pbMVT4IsReF1pY-CnDsrugeh_al2nlErd&source=post_page-----391153c945d6---------------------------------------)
https://nostarch.com/download/EvadingEDR_chapter6.pdf (It’s an awesome book, I would recommend everyone to buy this book if you are interested in Malware Development or Windows Internals in general.)