Module 3: File System States & Delete-Pending
Understanding the NTFS delete-pending state — the key mechanism that makes Process Ghosting possible.
Module Objective
Understand how NTFS file deletion really works at the kernel level, the delete-pending state, the difference between FILE_DELETE_ON_CLOSE and FileDispositionInformation, how mapped files interact with deletion, and why the delete-pending state creates a window where the file exists (for the handle owner) but is inaccessible (to everyone else).
1. How File Deletion Actually Works in Windows
When you call DeleteFile in Win32, the file does not immediately vanish from disk. Instead, Windows follows a multi-stage process that involves marking the file and then removing it only when all handles are closed. The NT-native view reveals the actual mechanics:
The Two-Stage Deletion Model
- Stage 1 — Mark for deletion: The file is marked as “delete-pending.” The MFT (Master File Table) entry is flagged, but the file’s data and metadata remain intact. The file still exists in the namespace.
- Stage 2 — Actual removal: When the last handle to the file is closed, the filesystem completes the deletion. The directory entry is removed and the data clusters are freed.
This two-stage model exists because Windows must handle the case where a file is in use when deletion is requested. Unlike Unix (where unlink immediately removes the directory entry while keeping the inode alive for existing file descriptors), Windows keeps the directory entry until all handles close.
2. Two Ways to Mark a File for Deletion
There are two mechanisms to put a file into the delete-pending state:
2.1 FILE_DELETE_ON_CLOSE (at open time)
The FILE_DELETE_ON_CLOSE flag is specified when opening or creating a file. It tells the filesystem to delete the file when the last handle with this flag is closed:
// FILE_DELETE_ON_CLOSE - set at file open time
HANDLE hFile;
IO_STATUS_BLOCK iosb;
OBJECT_ATTRIBUTES objAttr;
NtCreateFile(
&hFile,
DELETE | GENERIC_WRITE, // DELETE access required
&objAttr,
&iosb,
NULL,
FILE_ATTRIBUTE_NORMAL,
0, // no sharing
FILE_SUPERSEDE, // create or replace
FILE_DELETE_ON_CLOSE, // <-- delete when handle closes
NULL, 0
);
2.2 FileDispositionInformation (after open)
The NtSetInformationFile call with FileDispositionInformation can mark an already-open file for deletion. This is the approach Process Ghosting uses:
// FileDispositionInformation - set AFTER file is open
FILE_DISPOSITION_INFORMATION dispInfo;
dispInfo.DeleteFile = TRUE;
IO_STATUS_BLOCK iosb;
NTSTATUS status = NtSetInformationFile(
hFile, // existing open handle
&iosb,
&dispInfo,
sizeof(dispInfo),
FileDispositionInformation // information class
);
Why Ghosting Uses FileDispositionInformation
Process Ghosting uses NtSetInformationFile with FileDispositionInformation rather than FILE_DELETE_ON_CLOSE because it needs to control the order of operations: create the file, mark it delete-pending, write the payload, then create the section. The FILE_DELETE_ON_CLOSE flag does not allow this fine-grained control because the delete-on-close behavior is tied to handle closure, not to an explicit API call.
| Property | FILE_DELETE_ON_CLOSE | FileDispositionInformation |
|---|---|---|
| When set | At NtCreateFile time | Anytime via NtSetInformationFile |
| Reversible? | No (once set, cannot be cleared) | Yes (set DeleteFile = FALSE to unmark) |
| Access rights needed | DELETE at open time | DELETE on the handle |
| When deletion occurs | When handle with this flag closes | When the last handle closes |
| Used by Ghosting | No | Yes |
3. The Delete-Pending State in Detail
Once a file is marked for deletion (by either mechanism), it enters the delete-pending state. This is a real state tracked by the filesystem driver (NTFS). While in this state:
Delete-Pending State Properties
| Operation | From Existing Handle | From New Open Attempt |
|---|---|---|
| Read file | Succeeds (handle is still valid) | Fails: STATUS_DELETE_PENDING |
| Write file | Succeeds (handle is still valid) | Fails: STATUS_DELETE_PENDING |
| Create section | Succeeds (handle is still valid) | Cannot open file to create section |
| Open file (NtCreateFile/NtOpenFile) | N/A | Fails: STATUS_DELETE_PENDING |
| Delete file again | No-op (already pending) | Fails: STATUS_DELETE_PENDING |
| List in directory | May still appear (implementation-dependent) | |
The critical insight is the asymmetry: the handle owner can still do everything with the file (read, write, create sections), but no one else can open the file. This is exactly the property Process Ghosting exploits.
4. Interaction with Section Objects
A key question for Process Ghosting is: can you create a section from a delete-pending file? The answer is yes, as long as you use the existing open handle.
// This works: create section from delete-pending file via existing handle
HANDLE hSection = NULL;
NTSTATUS status = NtCreateSection(
&hSection,
SECTION_ALL_ACCESS,
NULL,
NULL,
PAGE_READONLY,
SEC_IMAGE,
hFile // handle to delete-pending file - WORKS
);
Once the section is created, it holds a reference to the underlying file object in the kernel. Even when the file handle is closed and the file is deleted from disk, the section persists because the kernel keeps the file object alive through the section’s CONTROL_AREA → FILE_OBJECT reference chain.
What About Existing Mapped Sections?
Windows prevents deletion of files that have existing image sections mapped. If you try to delete a file that is currently mapped as a SEC_IMAGE section (e.g., a running executable), you get STATUS_CANNOT_DELETE. Process Ghosting avoids this by marking the file delete-pending before creating the section. The order matters: mark delete first, then create section.
5. The Ghosting Order of Operations
The specific order in which operations are performed is critical. Here is why each step must happen in this exact sequence:
Step-by-Step Rationale
- NtCreateFile — Creates the file. It must exist to write to it and create a section from it.
- NtSetInformationFile (FileDispositionInformation) — Marks the file delete-pending. This must happen before writing the payload, because once the payload is written and a section is created, the file would have an image section mapped, which would prevent deletion. By marking delete-pending first, the deletion is already queued.
- NtWriteFile — Writes the malicious PE content. The file is delete-pending but the handle is still valid for writes.
- NtCreateSection (SEC_IMAGE) — Creates the image section. The file is delete-pending and contains the malicious PE. The section captures this content.
- NtClose (hFile) — Closes the file handle. Since the file is delete-pending, closing the last handle triggers actual deletion. The file vanishes from disk.
- NtCreateProcessEx — Creates the process from the section. The section is still valid even though the backing file is gone.
File State Transitions
Normal file
Marked, still open
PE content, section created
Handle closed, gone from disk
6. STATUS_DELETE_PENDING vs STATUS_FILE_DELETED
When another process (like an AV scanner) tries to open a file in these states, it receives different error codes depending on timing:
| File State | NtCreateFile/NtOpenFile Result | Meaning |
|---|---|---|
| Delete-pending (handle still open) | STATUS_DELETE_PENDING (0xC0000056) | File exists in MFT but is queued for deletion. Cannot open. |
| Deleted (all handles closed) | STATUS_OBJECT_NAME_NOT_FOUND (0xC0000034) | File no longer exists in the directory namespace. |
In both cases, the AV scanner cannot open or read the file. This is the fundamental evasion mechanism of Process Ghosting. The file is either locked by the delete-pending state or gone entirely.
7. NTFS Internals: The MFT Perspective
At the NTFS level, deletion involves the Master File Table (MFT) entry for the file:
MFT Entry Lifecycle During Ghosting
| Phase | MFT State | Directory Entry | Data Clusters |
|---|---|---|---|
| File created | Allocated, flags = IN_USE | Present in parent directory index | Allocated to file |
| Delete-pending | Still IN_USE, pending delete flag | Still present (may be hidden from some queries) | Still allocated |
| After last handle closes | Marked free (IN_USE cleared) | Removed from parent directory index | Freed to bitmap |
The key point is that during the delete-pending phase, the MFT entry and data clusters are fully intact. The file’s data (our malicious PE) is physically present on disk and readable through the existing handle. It is only inaccessible to processes that try to open it, because NTFS rejects new opens for delete-pending files.
8. Practical Implications for Security Tools
AV Scanner Perspective
When a process creation notification fires (via PsSetCreateProcessNotifyRoutineEx, which fires at first thread insertion via NtCreateThreadEx, not at process object creation), the AV driver receives the FILE_OBJECT of the image backing the process. The driver typically tries to read this file to scan its content. But if the file is delete-pending or already deleted:
- The
FILE_OBJECTmay point to a file that no longer has a directory entry - Trying to open the file by name fails with
STATUS_DELETE_PENDINGorSTATUS_OBJECT_NAME_NOT_FOUND - The
FILE_OBJECTitself may still be valid (referenced by the section), but the data may not be easily accessible through normal file I/O paths
This creates a blind spot: the process is running, but its backing file cannot be scanned through conventional means.
Knowledge Check
Q1: What happens when another process tries to open a file that is in the delete-pending state?
Q2: Why does Process Ghosting use NtSetInformationFile (FileDispositionInformation) instead of FILE_DELETE_ON_CLOSE?
Q3: Can you create a SEC_IMAGE section from a file that is in the delete-pending state using the existing open handle?