File Interception Flow
This document traces the complete path of a file system operation from application I/O request through kernel interception to user-mode notification.
Related: Driver Architecture Β· Kernel Driver Module Β· Communication Architecture
1. Overview
Every file operation on Windows travels through the I/O Manager β Filter Manager β file system driver stack. Our minifilter intercepts operations at the Filter Manager level and selectively forwards notifications to user mode.
flowchart TB
subgraph Application
App["Application\n(e.g., malware dropper)"]
end
subgraph Kernel
IoMgr["I/O Manager"]
FltMgr["Filter Manager"]
subgraph OurDriver["WindowsFileSystemMinifilter.sys"]
PreOp["Pre-Operation Callback"]
PostOp["Post-Operation Callback"]
Filter["Extension + PID Filter"]
Send["SendMessageToUserMode()"]
end
FS["NTFS"]
end
subgraph UserMode
Monitor["FsMinifilterMonitor.exe"]
end
App -->|"CreateFile / WriteFile / DeleteFile"| IoMgr
IoMgr --> FltMgr
FltMgr --> PreOp
PreOp --> FS
FS --> PostOp
PostOp --> Filter
Filter -->|".exe/.dll only"| Send
Send -->|"FltSendMessage"| Monitor
style OurDriver fill:#2d1b69,color:#fff
2. CREATE Flow (File Open / New File)
This is the most complex flow because it handles both notifications and delete-on-close tracking.
sequenceDiagram
participant App as Application
participant FM as Filter Manager
participant Pre as PreOperationCreate
participant NTFS as NTFS
participant Post as PostOperationCreate
participant UM as User-Mode Monitor
App->>FM: IRP_MJ_CREATE (open "malware.exe")
FM->>Pre: PreOperationCreate()
alt FILE_DELETE_ON_CLOSE flag set
Pre->>Pre: Allocate FS_STREAM_CONTEXT
Pre->>Pre: Set CompletionContext = streamContext
Pre-->>FM: FLT_PREOP_SYNCHRONIZE
else Normal open
Pre-->>FM: FLT_PREOP_SUCCESS_WITH_CALLBACK
end
FM->>NTFS: Forward to file system
NTFS-->>FM: STATUS_SUCCESS
FM->>Post: PostOperationCreate()
Post->>Post: FltGetFileNameInformation()
Post->>Post: FltParseFileNameInformation()
Post->>Post: Check: Is .exe or .dll?
Post->>Post: Check: PID β monitor PID?
alt Target extension + not self
Post->>UM: SendMessageToUserMode(MSG_TYPE_FILE_CREATE)
end
alt Stream context provided
Post->>Post: FltSetStreamContext()
Post->>Post: Mark DeleteOnClose = TRUE
end
Post-->>FM: FLT_POSTOP_FINISHED_PROCESSING
3. READ Flow
The read flow is simpler β a pre-operation-only notification with no post-op callback needed.
sequenceDiagram
participant App as Application
participant FM as Filter Manager
participant Pre as PreOperationRead
participant UM as User-Mode Monitor
App->>FM: IRP_MJ_READ (read "target.dll")
FM->>Pre: PreOperationRead()
Pre->>Pre: Check: g_clientPort != NULL?
Pre->>Pre: Check: Read.Length > 0?
Pre->>Pre: Check: PID β monitor PID?
alt All checks pass
Pre->>Pre: FltGetFileNameInformation()
Pre->>Pre: Check: IsTargetExtension(.dll)?
alt Is target
Pre->>UM: SendMessageToUserMode(MSG_TYPE_FILE_READ)
end
end
Pre-->>FM: FLT_PREOP_SUCCESS_NO_CALLBACK
Note over FM: I/O continues to file system normally
4. WRITE / MODIFY Flow
Identical structure to READ but sends MSG_TYPE_FILE_MODIFY.
sequenceDiagram
participant App as Application
participant FM as Filter Manager
participant Pre as PreOperationWrite
participant UM as User-Mode Monitor
App->>FM: IRP_MJ_WRITE (modify "target.exe")
FM->>Pre: PreOperationWrite()
Pre->>Pre: Skip if: no client, zero-length, self-PID
alt Valid write to .exe/.dll
Pre->>Pre: FltGetFileNameInformation()
Pre->>UM: SendMessageToUserMode(MSG_TYPE_FILE_MODIFY)
end
Pre-->>FM: FLT_PREOP_SUCCESS_NO_CALLBACK
5. DELETE Flow (Multi-IRP)
File deletion is the most intricate flow, spanning three IRP types and using stream contexts for state tracking.
sequenceDiagram
participant App as Application
participant FM as Filter Manager
participant PreSet as PreOperationSetInfo
participant NTFS as NTFS
participant PostSet as PostOperationSetInfo
participant PreClean as PreOperationCleanup
participant PostClean as PostOperationCleanup
participant UM as User-Mode Monitor
Note over App: Method 1: SetFileDispositionInfo
App->>FM: IRP_MJ_SET_INFORMATION (FileDispositionInfo)
FM->>PreSet: PreOperationSetInfo()
PreSet->>PreSet: GetOrSetStreamContext()
PreSet->>PreSet: InterlockedIncrement(NumOps)
alt NumOps == 1 (no race)
PreSet-->>FM: FLT_PREOP_SYNCHRONIZE
FM->>NTFS: Forward
NTFS-->>FM: STATUS_SUCCESS
FM->>PostSet: PostOperationSetInfo()
PostSet->>PostSet: streamContext.SetDisp = DeleteFile
PostSet->>PostSet: InterlockedDecrement(NumOps)
else NumOps > 1 (race detected)
PreSet->>PreSet: Release context, skip postop
end
Note over App: Later: Close handle triggers cleanup
App->>FM: IRP_MJ_CLEANUP
FM->>PreClean: PreOperationCleanup()
PreClean->>PreClean: FltGetStreamContext()
alt Context exists (delete candidate)
PreClean-->>FM: FLT_PREOP_SYNCHRONIZE
FM->>NTFS: Cleanup
NTFS-->>FM: Done
FM->>PostClean: PostOperationCleanup()
PostClean->>PostClean: Check: SetDisp or DeleteOnClose?
PostClean->>NTFS: FltQueryInformationFile()
alt STATUS_FILE_DELETED
PostClean->>PostClean: InterlockedIncrement(IsNotified)
alt IsNotified == 1 (first notification)
PostClean->>UM: SendMessageToUserMode(MSG_TYPE_FILE_DELETE)
end
end
end
6. Filtering Decision Tree
This diagram shows the complete decision logic applied in every callback before sending a message:
flowchart TD
Start["IRP Received by Callback"]
Start --> ClientCheck{"g_clientPort\n!= NULL?"}
ClientCheck -->|No| NoSend["No send\n(no listener)"]
ClientCheck -->|Yes| LenCheck{"Data length > 0?\n(READ/WRITE only)"}
LenCheck -->|No| NoSend2["No send\n(empty operation)"]
LenCheck -->|Yes| PIDCheck{"Requestor PID ==\ng_clientProcessId?"}
PIDCheck -->|Yes| NoSend3["No send\n(self-exclusion)"]
PIDCheck -->|No| NameQuery["FltGetFileNameInformation()"]
NameQuery --> ExtCheck{"Extension ==\n.exe or .dll?"}
ExtCheck -->|No| NoSend4["No send\n(not target)"]
ExtCheck -->|Yes| Send["SendMessageToUserMode()"]
style Send fill:#2d6a4f,color:#fff
style NoSend fill:#6c757d,color:#fff
style NoSend2 fill:#6c757d,color:#fff
style NoSend3 fill:#6c757d,color:#fff
style NoSend4 fill:#6c757d,color:#fff
7. Timing & Performance
| Operation | Kernel Overhead | User-Mode Latency |
|---|---|---|
| Extension check | ~100ns (string compare) | β |
FltGetFileNameInformation |
~1β5Β΅s (cached) | β |
FltSendMessage |
~10β50Β΅s | β |
FilterGetMessage |
β | Blocks until message |
| End-to-end (IRP β monitor receives) | β | ~50β100Β΅s |
The 100ms timeout on FltSendMessage ensures the kernel callback never blocks for long. If the user-mode monitor is busy, the message is dropped silently (timeout error ignored).
Next Steps
- See what happens after the monitor receives a message: Scan Pipeline Flow
- Deep-dive into each callbackβs code: Kernel Driver Module
- Understand the message format: Kernel β User Interface