Module 7: AV/EDR Scan Timing Window
Understanding exactly when antivirus and EDR products scan files during process creation — and why Process Ghosting bypasses all three inspection points.
Module Objective
Analyze the three primary inspection points where AV/EDR attempts to scan a process’s backing file (file creation/write, section mapping, process creation callback), understand the kernel mechanisms behind each (minifilter callbacks, PsSetCreateProcessNotifyRoutineEx, image load callbacks), and demonstrate why Process Ghosting defeats all three by ensuring the file is either inaccessible or non-existent at each point.
1. The Three Inspection Points
AV and EDR products have three primary opportunities to scan a process’s backing executable. Each occurs at a different stage of the file → section → process pipeline:
AV/EDR Inspection Points
File Create/Write
Minifilter IRP
Image Load
PsSetLoadImageNotify
First Thread Insert
PsSetCreateProcess
NotifyRoutineEx
| Inspection Point | Kernel Mechanism | When It Fires | What AV Typically Does |
|---|---|---|---|
| 1. File I/O | Minifilter driver (IRP callbacks) | On file create, write, close | Scan file content on write/close |
| 2. Image Load | PsSetLoadImageNotifyRoutine | When an image section is mapped | Verify image integrity, check known signatures |
| 3. Process Create | PsSetCreateProcessNotifyRoutineEx | When the first thread is inserted via NtCreateThreadEx (not at NtCreateProcessEx time) | Open and scan the image file, apply policies |
2. Inspection Point 1: File I/O Minifilter
The file system minifilter is the first line of defense. AV products register minifilter drivers that intercept file I/O operations (IRPs). The relevant callbacks are:
Minifilter IRP Callbacks for AV Scanning
| IRP | When | AV Action |
|---|---|---|
IRP_MJ_CREATE | File is opened/created | Check file path, apply policies, may deny access |
IRP_MJ_WRITE | Data is written to file | May buffer writes for later scanning |
IRP_MJ_CLEANUP | Last user handle closed | Trigger on-close scan of file content |
IRP_MJ_SET_INFORMATION | File metadata changed (incl. disposition) | May detect delete-pending, but typically does not trigger a scan |
Why Ghosting Bypasses Minifilter Scanning
During the ghosting sequence:
- IRP_MJ_CREATE: The file is created empty. Nothing malicious to scan.
- IRP_MJ_SET_INFORMATION: The file is marked delete-pending. Still empty. The minifilter sees a deletion request for an empty file — benign.
- IRP_MJ_WRITE: The payload is written. The minifilter may see this write, but the file is already in the delete-pending state. If the AV tries to open the file independently to scan the written content, it gets
STATUS_DELETE_PENDINGregardless of the file’sShareAccessflags. It cannot read the file through a separate handle. - IRP_MJ_CLEANUP: When we close the handle, the file is deleted. The minifilter sees a close on a delete-pending file, which triggers deletion. There is nothing left to scan.
Some AV minifilters can scan data from the write IRP buffer directly (without opening a separate handle). However, most on-access scanners are designed to scan on IRP_MJ_CLEANUP (handle close) and rely on being able to re-open the file for scanning. The delete-pending state defeats this model.
3. Inspection Point 2: Image Load Notification
The kernel provides PsSetLoadImageNotifyRoutine for drivers to register callbacks that fire when an image (EXE or DLL) is mapped into a process. This callback provides the image’s FILE_OBJECT and full image name:
// Kernel callback prototype
typedef VOID (*PLOAD_IMAGE_NOTIFY_ROUTINE)(
PUNICODE_STRING FullImageName,
HANDLE ProcessId,
PIMAGE_INFO ImageInfo
);
// IMAGE_INFO structure
typedef struct _IMAGE_INFO {
union {
ULONG Properties;
struct {
ULONG ImageAddressingMode : 8;
ULONG SystemModeImage : 1;
ULONG ImageMappedToAllPids : 1;
ULONG ExtendedInfoPresent : 1;
// ...
};
};
PVOID ImageBase;
ULONG ImageSelector;
SIZE_T ImageSize;
ULONG ImageSectionNumber;
} IMAGE_INFO, *PIMAGE_INFO;
Ghosting vs Image Load Callback
The image load callback fires when the image section is mapped into the process’s address space (during NtCreateProcessEx or when DLLs are loaded). At this point, the ghost file has already been closed and deleted. The FullImageName parameter points to the path of the deleted file. If the AV driver tries to open this path for scanning, it fails with STATUS_OBJECT_NAME_NOT_FOUND because the file no longer exists.
The FILE_OBJECT referenced by the section’s control area may still exist in kernel memory, but it is not directly provided to the image load callback in a way that allows easy scanning. The callback gets the image name (a path), not a handle or file object.
4. Inspection Point 3: Process Creation Notification
The most important inspection point is PsSetCreateProcessNotifyRoutineEx. Despite its name, this callback does not fire when the process object is created (NtCreateProcessEx). It fires when the first thread is inserted into the process via NtCreateThreadEx. This timing gap — between process object creation and first thread insertion — is exactly what Process Ghosting exploits. The file can be deleted after creating the process object but before the notify routine fires. The callback provides the AV driver with the information needed to make a scan/block decision:
// Extended process notification callback
typedef VOID (*PCREATE_PROCESS_NOTIFY_ROUTINE_EX)(
PEPROCESS Process,
HANDLE ProcessId,
PPS_CREATE_NOTIFY_INFO CreateInfo // NULL for process exit
);
// PS_CREATE_NOTIFY_INFO (on process creation)
typedef struct _PS_CREATE_NOTIFY_INFO {
SIZE_T Size;
union {
ULONG Flags;
struct {
ULONG FileOpenNameAvailable : 1;
ULONG IsSubsystemProcess : 1;
// ...
};
};
HANDLE ParentProcessId;
CLIENT_ID CreatingThreadId;
struct _FILE_OBJECT *FileObject; // THE IMAGE FILE OBJECT
PUNICODE_STRING ImageFileName; // image file path
PUNICODE_STRING CommandLine;
NTSTATUS CreationStatus; // can set to block!
} PS_CREATE_NOTIFY_INFO, *PPS_CREATE_NOTIFY_INFO;
The Critical Field: FileObject
The FileObject field in PS_CREATE_NOTIFY_INFO points to the file object of the image that backs the new process. For a normal process, this is a valid, open file object that the AV can use to read and scan the file. For a ghosted process, this file object points to a file that is deleted. The AV driver receives a reference to a file object whose backing file no longer exists in the filesystem namespace.
What happens when the AV tries to use this file object to scan the image?
| AV Action | Result with Ghost File |
|---|---|
Open file by name (ImageFileName) | STATUS_OBJECT_NAME_NOT_FOUND — file deleted |
Read from FileObject directly | May succeed or fail depending on implementation. The file object exists in kernel memory but has no directory entry. Some drivers cannot handle this state. |
| Query file information | May return stale or incomplete information since the file is deleted |
Set CreationStatus to block | Could block the process, but only if the AV has a reason to block (requires successful scan first) |
5. The Complete Timing Analysis
Here is a detailed timeline showing the state of the file at each inspection point during the ghosting sequence:
Ghosting Timeline vs AV Inspection Points
| Ghosting Step | File State | Active Inspection | AV Can Scan? | Why Not? |
|---|---|---|---|---|
NtCreateFile | Empty, normal | Minifilter IRP_MJ_CREATE | Yes, but file is empty | Nothing malicious to find |
NtSetInformationFile | Delete-pending, empty | Minifilter IRP_MJ_SET_INFO | No content to scan | File still empty |
NtWriteFile | Delete-pending, has PE | Minifilter IRP_MJ_WRITE | Cannot re-open file | STATUS_DELETE_PENDING blocks new opens |
NtCreateSection | Delete-pending, has PE | None (no callback for section creation from files) | No | No relevant callback fires |
NtClose(hFile) | Deleted | Minifilter IRP_MJ_CLEANUP | File gone | STATUS_OBJECT_NAME_NOT_FOUND |
NtCreateProcessEx | Deleted | None (callback does not fire here) | No | PsSetCreateProcessNotifyRoutineEx fires later at thread insertion |
| Image mapping | Deleted | PsSetLoadImageNotifyRoutine | Cannot open by name | File does not exist |
NtCreateThreadEx | Deleted | PsSetCreateProcessNotifyRoutineEx | Cannot open by name | File does not exist |
At every single inspection point, the AV either sees an empty file, cannot open the file because it is delete-pending, or cannot find the file because it has been deleted. There is no point in the sequence where the AV can both access the file and see malicious content.
6. Comparison: How Each Technique Handles the Scan Points
| Inspection Point | Hollowing | Doppelgänging | Herpaderping | Ghosting |
|---|---|---|---|---|
| File I/O scan | Clean file (legitimate EXE) | Transacted (visible only in TxF context) | Malicious content visible during write | Empty during create; delete-pending during write |
| Image load | Mismatch detectable | Clean (transaction rolled back) | File now has benign content | File deleted |
| Process create | File is clean; memory is not | File reverted to clean | File has benign content | File does not exist |
| Overall evasion | Weak (memory/file mismatch) | Good (but TxF is flagged) | Good (but file still on disk) | Strong (no file at all) |
7. Kernel Callback Limitations
Understanding why the kernel’s callback architecture has this gap reveals a fundamental design assumption:
Design Assumptions the Kernel Makes
- Files persist: The process creation callback (
PsSetCreateProcessNotifyRoutineEx) fires at first thread insertion, not at process object creation. It was designed assuming the image file would still exist on disk at that point. The callback provides a file path and file object, expecting the AV to be able to use them. - Sequential operations: The kernel assumes file creation, section creation, process creation, and thread creation happen in a predictable order without interleaved manipulation of the file’s disposition state. The gap between
NtCreateProcessExandNtCreateThreadEx(when the callback actually fires) gives the attacker time to delete the file. - No cross-operation dependency: There is no kernel mechanism to atomically bind file creation, section creation, and process creation into a single transaction that the AV can inspect as a unit.
Why Microsoft Has Not Fully Fixed This
Microsoft could add a callback that fires at NtCreateSection time, providing the file object and content at the moment the section captures the image. However, this would be a significant kernel change with performance implications (every DLL load would trigger the callback). As of Windows 11, PsSetCreateProcessNotifyRoutineEx2 provides improved information but the fundamental timing gap remains exploitable. The kernel would need to prevent section creation from delete-pending files entirely, or provide a mandatory scan window, to fully close this gap.
8. ETW (Event Tracing for Windows) Visibility
ETW provides additional visibility but has its own limitations in detecting ghosting:
| ETW Provider | Events Available | Ghosting Detection Capability |
|---|---|---|
| Microsoft-Windows-Kernel-File | File create, write, delete, rename | Can see the create → set disposition → write → close sequence, but requires correlation |
| Microsoft-Windows-Kernel-Process | Process create, thread create | Can see process creation but file is already gone |
| Microsoft-Windows-Kernel-Audit-API-Calls | NtCreateSection, NtCreateProcessEx | Can see section and process creation, but requires correlating with file events |
ETW-based detection is possible but requires event correlation: linking the file creation event, the disposition change, the write, the section creation, and the process creation into a single chain. This is non-trivial and most EDR products do not correlate these events in real time.
Detection via ETW Correlation
A detection strategy based on ETW would look for: (1) NtCreateFile for a new file, (2) NtSetInformationFile with FileDispositionInformation shortly after, (3) NtWriteFile to the same handle, (4) NtCreateSection from the same handle, (5) NtCreateProcessEx with the section. Finding all five events in sequence on the same handle is a strong indicator of ghosting. Module 8 covers detection in detail.
Knowledge Check
Q1: At which point in the ghosting sequence does the AV first have the opportunity to scan malicious content in the file?
Q2: What does PsSetCreateProcessNotifyRoutineEx provide to the AV driver in its PS_CREATE_NOTIFY_INFO structure?
Q3: Why can an AV minifilter not scan the payload when NtWriteFile writes it to the delete-pending file?