[Research] Walking Through Windows Minifilter Drivers (EN)
Hello, this is banda. This is my first time greeting you with an article on Windows Minifilter Drivers. 🤗
While studying Windows Kernel Drivers recently, I discovered that there are various types of drivers categorized by purpose, such as Bus Drivers, Filter Drivers, FSDs, and Minifilters. I became curious about the structural differences between Bus or Filter Drivers and traditional Function Drivers, as well as how vulnerabilities manifest in these different types.
Today, we will explore the structure and operation of Minifilter Drivers, examine their internal components, and analyze potential vulnerabilities.
1. About Minifilter Drivers
A Minifilter driver is a specialized type of driver in Windows designed to monitor, intercept, and modify file system I/O requests such as file creation, opening, reading, writing, and deletion. It is commonly used for precise monitoring of file system activity.
If you think about it in terms of “monitoring, blocking, or modifying file access”, it starts to sound familiar, doesn’t it? Indeed — this functionality is well-suited for products like antivirus software, EDR solutions, and backup programs. In fact, I’ve personally confirmed that many such products rely on Minifilter drivers.
A Minifilter driver can intercept or manipulate the following three types of requests:
- IRP (I/O Request Packet)
- Fast I/O
- File System Filter Callbacks
1.1 Filter Manager → Minifilter Flow
A Minifilter operates on top of the Windows Filter Manager (fltmgr.sys
). The Filter Manager receives file I/O requests from the I/O Manager and forwards them to the registered Minifilter drivers in the order determined by their altitude. In other words, the altitude controls the order in which filters are loaded and invoked.
Let’s take a look at how file I/O requests are processed in Windows.
- An application issues an I/O operation by calling APIs such as
CreateFile
,ReadFile
, orWriteFile
. - The I/O Manager receives this request and forwards it to the Filter Manager (
fltmgr.sys
). - The Filter Manager checks the list of all registered Minifilter drivers and passes the request to each driver in the order of their altitude.
- After the Minifilter completes its processing, the request is passed on to the file system filter driver.
- Finally, the request is delivered to the disk driver (Storage Driver Stack), which accesses the physical disk or processes the data accordingly.
You can check the list of Minifilter drivers loaded on the system by running the fltmc
command in the Command Prompt.
The higher the altitude value, the higher the priority, meaning the Minifilter can intercept or modify I/O requests earlier. However, the processing order differs depending on whether it is a pre-operation or post-operation callback.
- Pre-operation: Called in descending altitude order (high → low)
- Post-operation: Called in ascending altitude order (low → high)
Looking back at the overall flow… a Minifilter driver does not directly handle IRPs in the traditional way.
Instead, the Filter Manager receives I/O requests on its behalf and passes them to the Minifilter through callback functions.
In other words, a Minifilter driver does not need to set up the DispatchRoutine that we commonly associate with traditional drivers!
1.2 Minifilter Callback Routine
How can a Minifilter driver operate only on specific file operations?
This is possible thanks to a mechanism called callbacks.
As mentioned earlier, a Minifilter driver does not directly process IRPs through a DispatchRoutine
.
Instead, it can “hook” I/O requests delivered via the Filter Manager.
By registering pre-operation (PreOperation Callback
) and post-operation (PostOperation Callback
) callbacks, a Minifilter can monitor or control I/O operations at the system level when they occur.
- Pre-operation callback (
PFLT_PRE_OPERATION_CALLBACK
)
PFLT_PRE_OPERATION_CALLBACK PfltPreOperationCallback;
FLT_PREOP_CALLBACK_STATUS PfltPreOperationCallback(
[in, out] PFLT_CALLBACK_DATA Data,
[in] PCFLT_RELATED_OBJECTS FltObjects,
[out] PVOID *CompletionContext
)
{...}
Called before the I/O request is passed to the file system or lower drivers.
This is where the core logic of the Minifilter runs, with powerful capabilities such as returning
FLT_PREOP_COMPLETE
, FLT_PREOP_SUCCESS_WITH_CALLBACK
, or FLT_PREOP_SUCCESS_NO_CALLBACK
.
PFLT_PRE_OPERATION_CALLBACK callback function (fltkernel.h)
- Post-operation callback (
PFLT_POST_OPERATION_CALLBACK
)
PFLT_POST_OPERATION_CALLBACK PfltPostOperationCallback;
FLT_POSTOP_CALLBACK_STATUS PfltPostOperationCallback(
[in, out] PFLT_CALLBACK_DATA Data,
[in] PCFLT_RELATED_OBJECTS FltObjects,
[in, optional] PVOID CompletionContext,
[in] FLT_POST_OPERATION_FLAGS Flags
)
{...}
Called on the way back after the I/O request has been fully processed by lower drivers and the file system.
It is typically used to verify the success of the operation, log results, or modify the outcome if needed.
PFLT_POST_OPERATION_CALLBACK callback function (fltkernel.h)
In short, these callbacks are registered in the FLT_OPERATION_REGISTRATION
structure, which specifies which pre-/post-operation callbacks are associated with which I/O operations (MajorFunctions).
1.3 Communication Between Minifilter and User-Mode
Communication between a Minifilter driver and a User-Mode application is handled via a filter communication port.
This port acts as a dedicated high-speed communication channel between the Minifilter and the application, allowing safe message exchange between Kernel-Mode drivers and User-Mode processes.
Let’s look into the code and explore the APIs provided by Microsoft for this purpose.
Driver Code
#include <fltKernel.h>
PFLT_FILTER gFilter = NULL;
PFLT_PORT gServerPort = NULL, gClientPort = NULL;
VOID OnDisconnect(PVOID Cookie) {
UNREFERENCED_PARAMETER(Cookie);
FltCloseClientPort(gFilter, &gClientPort);
gClientPort = NULL;
}
NTSTATUS OnConnect(PFLT_PORT ClientPort, PVOID SrvCookie, PVOID Ctx, ULONG Size, PVOID* ConnCookie) {
UNREFERENCED_PARAMETER(SrvCookie);
UNREFERENCED_PARAMETER(Ctx);
UNREFERENCED_PARAMETER(Size);
UNREFERENCED_PARAMETER(ConnCookie);
gClientPort = ClientPort;
return STATUS_SUCCESS;
}
FLT_PREOP_CALLBACK_STATUS PreCreate(PFLT_CALLBACK_DATA Data, PCFLT_RELATED_OBJECTS FltObjects, PVOID* Buff) {
UNREFERENCED_PARAMETER(FltObjects);
UNREFERENCED_PARAMETER(Buff);
PFLT_FILE_NAME_INFORMATION nameInfo;
if (gClientPort && NT_SUCCESS(FltGetFileNameInformation(Data, FLT_FILE_NAME_NORMALIZED, &nameInfo))) {
FltSendMessage(gFilter, &gClientPort, nameInfo->Name.Buffer, nameInfo->Name.Length, NULL, NULL, NULL);
FltReleaseFileNameInformation(nameInfo);
}
return FLT_PREOP_SUCCESS_NO_CALLBACK;
}
NTSTATUS Unload(FLT_FILTER_UNLOAD_FLAGS Flags) {
UNREFERENCED_PARAMETER(Flags);
FltCloseCommunicationPort(gServerPort);
FltUnregisterFilter(gFilter);
return STATUS_SUCCESS;
}
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath) {
UNREFERENCED_PARAMETER(RegistryPath);
NTSTATUS status;
const FLT_OPERATION_REGISTRATION Cbs[] = { { IRP_MJ_CREATE, 0, PreCreate, NULL }, { IRP_MJ_OPERATION_END } };
const FLT_REGISTRATION Reg = { sizeof(FLT_REGISTRATION), FLT_REGISTRATION_VERSION, 0, NULL, Cbs, Unload };
status = FltRegisterFilter(DriverObject, &Reg, &gFilter);
if (!NT_SUCCESS(status)) return status;
UNICODE_STRING portName = RTL_CONSTANT_STRING(L"\\FileActivityMonitorPort");
OBJECT_ATTRIBUTES oa = { sizeof(oa), NULL, &portName, OBJ_KERNEL_HANDLE | OBJ_CASE_INSENSITIVE, NULL };
status = FltCreateCommunicationPort(gFilter, &gServerPort, &oa, NULL, OnConnect, OnDisconnect, NULL, 1);
if (!NT_SUCCESS(status)) {
FltUnregisterFilter(gFilter);
return status;
}
return FltStartFiltering(gFilter);
}
Focusing on the representative driver code APIs FltCreateCommunicationPort()
, FltSendMessage()
, and FltCloseCommunicationPort()
, I have implemented an example Minifilter driver code.
Let’s walk through the flow together:
The driver registers itself using
FltRegisterFilter()
.At this stage, it specifies a PreCreateCallback function to monitor file creation and open (
IRP_MJ_CREATE
) requests, and starts I/O monitoring withFltStartFiltering()
.Using the
FltCreateCommunicationPort()
function, the driver can create a communication port.In the code above, it creates the port
\\FileActivityMonitorPort
, which allows a User-Mode application to connect and receive notifications.When a file create/open event occurs, PreCreateCallback is invoked, and the function collects information about which process accessed which file.
- The driver then uses
FltSendMessage()
to immediately send the real-time file access information collected in PreCreateCallback to the connected User-Mode application. - Finally, in the FilterUnload routine, when the driver is unloaded, it closes the communication port with
FltCloseCommunicationPort()
and unregisters the filter.
User-Mode Application Code
#include <windows.h>
#include <fltuser.h>
#include <stdio.h>
#pragma comment(lib, "fltlib.lib")
int main() {
HANDLE port;
HRESULT hr;
BYTE buffer[sizeof(FILTER_MESSAGE_HEADER) + 1024];
PFILTER_MESSAGE_HEADER header = (PFILTER_MESSAGE_HEADER)buffer;
printf("Connecting to driver...\n");
hr = FilterConnectCommunicationPort(L"\\FileActivityMonitorPort", 0, NULL, 0, NULL, &port);
if (IS_ERROR(hr)) {
printf("Connection failed. Error 0x%X\n", hr);
return 1;
}
printf("Connected. Waiting for file events...\n");
while (TRUE) {
hr = FilterGetMessage(port, header, sizeof(buffer), NULL);
if (SUCCEEDED(hr)) {
printf("File Accessed: %S\n", (PWSTR)header->MessageBody);
} else {
printf("Connection lost. Error 0x%X\n", hr);
break;
}
}
CloseHandle(port);
return 0;
}
Let’s now look at the communication flow from the User-Mode application side.
Representative APIs include FilterConnectCommunicationPort()
and FilterSendMessage()
.
- First, the application uses
FilterConnectCommunicationPort()
to connect to the Minifilter driver’s communication port in the kernel,\\FileActivityMonitorPort
, and obtains a HANDLE for communication. It then calls
FilterGetMessage()
to directly receive the file path string from the driver.If the message is received successfully, the application outputs the file path to the screen.
2. [CVE-2024-30085] 1-Day Analysis
This is a Windows Minifilter Driver vulnerability that was also introduced in “1day1line” (reference).
Let’s try to reproduce CVE-2024-30085 and, in the process, learn more about Minifilter drivers.
🪟 Environment: Windows 11 22H2/23H2 10.0.2261.3672
The Windows Cloud Files Mini Filter driver is responsible for the Windows cloud sync feature.
For example, I’ve taken a screenshot of one of my folders.
To understand this Minifilter driver vulnerability, we first need some background knowledge about Stub Files and Reparse Points.
What is a Stub File?
A stub file refers to a file that exists only as a placeholder locally, without containing any actual data.
As shown in the image above, the “Pictures” folder has a blue cloud icon in its status — this indicates that the file is in a stub state.
On NTFS, its size, name, and icon are displayed, but its actual contents are not stored locally.
Reparse Point Metadata
So, what happens when a user tries to access such a file?
NTFS checks the Reparse Tag and determines, “Oh… this is a stub file!”.
At this point, the Windows Cloud Files Minifilter (cldflt.sys
) reads the Reparse Point
metadata structure and decides how to handle the file.
After that, the Minifilter prepares for communication with the remote server.
However, cldflt.sys
itself does not communicate directly with the server — instead, it delegates the actual work to a User-Mode process.
This matches the concept we saw earlier: the Minifilter plays the role of an I/O interpreter, while the User-Mode client handles the actual data manipulation.
I’ve added the Windows Cloud Files Minifilter’s Reparse Point structure definitions to Local Types.
Let’s take a look at the CldFlt structure set I defined.
typedef struct _REPARSE_DATA_BUFFER {
DWORD ReparseTag;
WORD ReparseDataLength;
WORD Reserved;
WORD Flags;
WORD UncompressedSize;
REPARSE_CLD_BUFFER ReparseCldBuffer;
} REPARSE_DATA_BUFFER, *PREPARSE_DATA_BUFFER;
REPARSE_DATA_BUFFER
is the standard header structure used to represent all Reparse Point data on NTFS.
Here, however, we use a redefined version to make analyzing Cloud Files easier.
This structure stores top-level metadata such as ReparseTag
(to identify the owning driver), ReparseDataLength
, and Flags
, and then points to the actual data, which continues as an HSM_REPARSE
structure.
struct HSM_REPARSE {
USHORT hsmFlags;
USHORT hsmSize;
struct HSM_RP_DATA fileData;
};
HSM_REPARSE
is the full container for a Cloud Files-specific Reparse Point.
hsmFlags
and hsmSize
indicate whether compression is used and the total size of the HSM block.
The fileData
field contains an HSM_RP_DATA
structure.
struct HSM_RP_DATA {
ULONG magic;
ULONG crc32;
ULONG totalSize;
USHORT dataFlags;
USHORT elemCount;
struct HSM_RP_ELEMENT elements[5];
};
HSM_RP_DATA
is the main header, containing the layout and structure of the entire metadata block.
magic
identifies the data type, and if the CRC bit is set in dataFlags
, crc32
is validated using RtlComputeCrc32
.
The elements[]
array stores HSM_RP_ELEMENT
structures that define the type, size, and offset of each metadata element.
struct HSM_RP_ELEMENT {
USHORT elemType;
USHORT elemSize;
ULONG elemOffset;
};
typedef enum HSM_RP_ELEM_TYPE {
HSM_RP_ELEMENT_NONE = 0x00,
HSM_RP_ELEMENT_U64 = 0x06,
HSM_RP_ELEMENT_BYTE = 0x07,
HSM_RP_ELEMENT_U32 = 0x0a,
HSM_RP_ELEMENT_BITMAP = 0x11,
HSM_RP_ELEMENT_MAX = 0x12
} HSM_RP_ELEMENT_TYPE;
HSM_RP_ELEMENT
defines each individual metadata element’s type, size, and offset.
The HSM_RP_ELEMENT_TYPE
enumeration is used to classify element types.
2.1 Root Cause Analysis
The vulnerability occurs when a file is created and the HsmFltPostCREATE
callback is executed to process the file’s Reparse Point.
Within HsmFltPostCREATE
, when handling the bitmap information stored in the Reparse Point, the function HsmIBitmapNORMALOpen()
is called — and this is where the flaw exists.
bitmap_size
is read directly from the User-Mode request buffer.- A fixed-size buffer of 0x1000 bytes is allocated via
ExAllocatePoolWithTag
, but the user-controlledbitmap_size
value is passed directly tomemmove
without any boundary checks.
Therefore, if bitmap_size
is greater than 0x1000, a Heap-based Buffer Overflow is expected to occur.
Looking at the HsmpBitmapIsReparseBufferSupported()
function, it returns an error if hdr->elements[4].elemSize
is greater than 0x1000
.
Looking a bit further up at the conditional statement code, we can see that hdr->elements[2]
is being strictly validated. Checks such as total ≥ 0x18
and hdr->elemCount
boundary verification must all be passed in order to proceed. If these conditions are not met, it would return a failure, right?
However, in the same function, if hasBuf
is set to false
, the code returns result = 0
(success) simply by checking the 1-byte flag of element[1]
, without performing any separate bitmap length validation. In this path, there is no check to determine whether the bitmap length exceeds 0x1000
, so the validation is skipped and the data is treated as valid.
2.2 Exploit
The cldflt.sys
minifilter driver does not scan all file system I/O by default; instead, it only operates on paths within the Sync Root that are registered through the CfAPI. Therefore, the first step was to reach the root directory of a cloud sync folder by calling the CfRegisterSyncRoot()
function.
Once the Sync Root registration code is built and executed, such a folder is created. Inside this folder, cloud stub files and metadata may appear, and this becomes the point I can exploit.
-> HsmFltPostCREATE()
-> HsmiFltPostECPCREATE()
-> HsmpSetupContexts()
-> HsmpCtxCreateStreamContext()
-> HsmIBitmapNORMALOpen()
Through dynamic analysis, it was confirmed that in order to reach the vulnerable function HsmIBitmapNORMALOpen()
, the execution must sequentially pass through the above function chain and satisfy all conditional checks. My goal, therefore, is to fulfill all those conditions and reach the vulnerable memmove
inside HsmIBitmapNORMALOpen()
.
Understanding the Flow as a Minifilter Driver
Earlier, I mentioned that a minifilter driver calls specific callbacks depending on the IRP code when an I/O request occurs. To enter the vulnerable path, the process must begin at the IRP_MJ_CREATE
Post-Create Callback (HsmFltPostCREATE
) and proceed sequentially to the target function. These intermediate functions validate entry conditions based on file/stream attributes and Reparse Point information, so we can bypass them by crafting a special file structure.
- Use
MakeDataBuffer()
to create anIO_REPARSE_TAG_CLOUD
structure → The minifilter recognizes the file as a cloud stub file and enters the Reparse Point parsing logic. - Set Item Tag =
0x11
(Bitmap) → ConfigureSize
to0x1000 + overSize
to trigger a heap overflow inmemmove()
by copying more than the allocated size. Ensure that other elements (Tags0x7
,0x6
,0xA
, etc.) are also set to pass the minifilter’s boundary checks. - Include a fake kernel object pointer in the overflow data → Apply it with
FSCTL_SET_REPARSE_POINT
, then triggerHsmIBitmapNORMALOpen()
by callingCreateFile()
.
When these conditions are met, we can confirm that the flow starts from FltMgr
and reaches HsmIBitmapNORMALOpen()
.
I’d like to cover the full exploit process, but since today’s focus is on explaining the minifilter and I’ve already gone overboard with the length… let’s wrap up with a summary of the entire exploit scenario.
Proof of Concept Overview
EPROCESS Structure Analysis and Token Field Offset Calculation
Calculate the offset of the Token field within the EPROCESS structure to prepare for the subsequent SYSTEM token swap.
First
WNF_STATE_DATA
Spray and Hole CreationMass-allocate (
spray
)WNF_STATE_DATA
objects of size 0x1000 (0xff0 data) and then free them to create heap holes in the kernel heap.Open Vulnerable Bitmap File and Trigger First Overflow
Use
CfRegisterSyncRoot()
and manipulate the Reparse Point directory to prepare a vulnerable bitmap file within the Sync Root. Then, open the file withCreateFile()
to reach the path:IRP_MJ_CREATE()
→HsmFltPostCREATE()
→HsmiFltPostECPCREATE()
→HsmpSetupContexts()
→HsmpCtxCreateStreamContext()
→HsmIBitmapNORMALOpen()
Trigger a heap overflow to modify the
DataSize
of an adjacentWNF_STATE_DATA
, gaining OOB (Out-of-Bounds) read/write capability.Kernel Pointer Leak
Use the modified
WNF_STATE_DATA
to read the_KALPC_RESERVE
pointer and leak a kernel address.Second
WNF_STATE_DATA
Spray and Hole CreationRepeat the same spray-and-free process for
WNF_STATE_DATA
objects, but this time arrange for aPipeAttribute
structure to be placed adjacent to the WNF object.Second Overflow to Manipulate
PipeAttribute
Open a second bitmap file to trigger another heap overflow, overwriting the
Flink
pointer of the adjacentPipeAttribute
with the address of a user-space fakePipeAttribute
structure.Arbitrary Read to Retrieve EPROCESS/Token Addresses
Use the fake
PipeAttribute
to access the ALPC Port structure, sequentially reading the EPROCESS address and Token address of the target process.Token Swapping and SYSTEM Privilege Escalation
Perform an arbitrary write to replace the current process’s Token value with the SYSTEM Token value, then launch
cmd.exe
with SYSTEM privileges.
ALPC / WNF
The _WNF_STATE_DATA
and _ALPC_HANDLE_TABLE
structures are used to allocate arbitrary-sized kernel objects for heap holes and to leak kernel memory addresses. Since ALPC and WNF might be unfamiliar concepts, here is a brief explanation of the two subsystems relevant to this exploit:
ALPC (Asynchronous Local Procedure Call)
ALPC is an inter-process communication (IPC) mechanism within the Windows kernel that uses client and server ports to exchange messages. By leveraging the _ALPC_HANDLE_ENTRY
in the ALPC handle table, it is possible to store message buffer addresses. Since the size of this table is variable, it allows the creation of kernel objects of arbitrary size.
- When an ALPC port is created, an
_ALPC_HANDLE_TABLE
is allocated in the paged pool with a size of 0x80. - Each call to
NtAlpcCreateResourceReserve
creates a_KALPC_RESERVE
object, and its address is added to the handle table. By tampering with this structure, it becomes possible to obtain arbitrary kernel address read/write primitives.
→ In the PoC, a fake
_KALPC_RESERVE
is injected to achieve arbitrary R/W.ALPC handles can also be controlled from user mode, making them highly useful for exploitation.
WNF (Windows Notification Facility)
WNF is the Windows Notification Facility, and the WNF_NAME_INSTANCE
kernel object contains an internal _WNF_STATE_DATA
field whose size is variable. This allows direct control of the kernel object size from user mode using NtCreateWnfStateName
+ NtUpdateWnfStateData
.
_WNF_STATE_DATA
can be allocated with a size of 0x1000 (0x10 header + 0xFF0 data).- WNF objects can be mass-created for heap spraying, placing them adjacent to a target structure (e.g., an ALPC object).
- In the PoC, WNF is used to create heap holes and then place them next to ALPC objects to trigger an overflow into the ALPC handle table.
In particular, the PoC required registering a routine to create a pipe, which I found interesting.
struct PipeAttribute {
LIST_ENTRY list;
char *AttributeName;
uint64_t AttributeValueSize;
char *AttributeValue;
char data[0];
};
The pipe is configured so that the AttributeValue
pointer in the modified PipeAttribute
structure is set to a kernel memory address. This causes the kernel to read data from that address and return it to user mode. By combining the memory layout control achieved via ALPC with the WNF overflow, the pipe can be turned into an arbitrary read primitive, allowing leakage of kernel addresses.
3. Conclusion
I’ll wrap up by sharing the results of the LPE I implemented using the characteristics of the cldflt.sys
Minifilter driver combined with the WNF + ALPC technique.
While studying Minifilters, I realized that although their operation, functions, and concepts may feel a bit unfamiliar, the way vulnerabilities are triggered and their root causes are, in the end, quite similar. So, there’s no need to be too intimidated about diving into Minifilter exploitation!
If I get the chance, I’d like to explore and study various other drivers I haven’t looked into yet. I hope you’ll look forward to my next write-up as well!
Reference
https://exploitreversing.com/2023/04/11/exploiting-reversing-er-series/
https://learn.microsoft.com/en-us/windows-hardware/drivers/ifs/filter-manager-concepts
https://starlabs.sg/blog/2024/all-i-want-for-christmas-is-a-cve-2024-30085-exploit/
https://ssd-disclosure.com/ssd-advisory-cldflt-heap-based-overflow-pe/
본 글은 CC BY-SA 4.0 라이선스로 배포됩니다. 공유 또는 변경 시 반드시 출처를 남겨주시기 바랍니다.