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