Chapter 15

Process Creation

What actually happens when Windows creates a process — the ten stages from CreateProcess to first instruction, kernel callbacks that EDRs hook, and the malware techniques that abuse the creation sequence

Scenario

A Sysmon Event ID 1 fires: cmd.exe spawned by winword.exe. That parent-child relationship is your detection. But to understand why Sysmon can see this — what kernel callbacks fire, at what point in process creation — you need to know the creation sequence. That knowledge also explains why process hollowing is so hard to detect at creation time, and why suspended-process injection is detectable by EDRs that hook the right callback.

The Process Creation Pipeline

Process creation flows through multiple layers before the first user instruction executes. The journey from a CreateProcess call to a running process involves the Win32 subsystem, the kernel executive, the memory manager, the loader, and security checks at each stage.


  Process Creation Flow (high level)
  ─────────────────────────────────────────────────────────────────────
  CreateProcessW()
       │ Win32 subsystem call
       ▼
  kernel32!CreateProcessInternalW()
       │ NT API call
       ▼
  NtCreateUserProcess()   ←── or NtCreateProcessEx for legacy
       │ Kernel-mode transition
       ▼
  ┌─────────────────────────────────────────────────────────────────┐
  │  PspCreateProcess() (kernel)                                   │
  │  1. Open executable image file                                 │
  │  2. Create process object (EPROCESS allocation)                │
  │  3. Create process address space (page directory)              │
  │  4. Map executable image + ntdll.dll                           │
  │  5. Create PEB (in process address space)                      │
  │  6. Inherit handle table from parent                           │
  │  7. Create initial thread (ETHREAD)                            │
  │  8. Fire PsSetCreateProcessNotifyRoutine callbacks  ← EDR hook │
  │  9. Return to user mode                                        │
  └─────────────────────────────────────────────────────────────────┘
       │
       ▼
  Process object returned, initial thread running ntdll!LdrInitializeThunk
       │ Loader runs:
       ▼
  1. Load required DLLs (import table resolution)
  2. Run DllMain for each loaded DLL (DLL_PROCESS_ATTACH)
  3. Run TLS callbacks
  4. Transfer control to AddressOfEntryPoint
       │
       ▼
  main() / WinMain() — user code begins

  

CreateProcess Stages

CreateProcessW (or CreateProcessA) takes 10 parameters. The critical ones for detection:

ParameterDescriptionMalware Abuse
lpApplicationName Full path to executable. If NULL, the executable path is parsed from lpCommandLine. When NULL, path resolution can be abused — search order manipulation puts a malicious binary first
lpCommandLine Full command line string including arguments. Command line can differ from the image path shown in process listings — useful for spoofing
dwCreationFlags Process creation flags — controls suspension, priority, window creation CREATE_SUSPENDED is the key flag for process hollowing
lpStartupInfo Startup configuration including standard handles (stdin/stdout/stderr), window position, flags Inheriting handles passes file/pipe handles to child for C2 communication
lpProcessInformation Output: handles to the new process and its primary thread The returned thread handle is used immediately for hollowing (SuspendThread was implied by CREATE_SUSPENDED)

Important Creation Flags

FlagValueEffect
CREATE_SUSPENDED0x04Initial thread created in suspended state. Process image is mapped but no code runs until ResumeThread is called. Essential for process hollowing.
CREATE_NO_WINDOW0x08000000No console window. Malware uses this to spawn cmd.exe or powershell.exe invisibly.
DETACHED_PROCESS0x08No console. Child cannot inherit console from parent.
CREATE_NEW_CONSOLE0x10New console window created for the child.
EXTENDED_STARTUPINFO_PRESENT0x00080000lpStartupInfo is actually a STARTUPINFOEX structure with an attribute list. Required for parent process spoofing and process mitigation policy inheritance.
DEBUG_PROCESS0x01Caller becomes the debugger of the new process. All debug events are sent to the caller.

STARTUPINFO and Handle Inheritance

The STARTUPINFO structure controls several aspects of the child process's initial state. For malware, the most important use is handle inheritance — passing file handles, pipe handles, or other kernel object handles to the child process.

// Create a child process with a pipe handle for C2 communication
HANDLE hReadPipe, hWritePipe;
SECURITY_ATTRIBUTES sa = {sizeof(sa), NULL, TRUE};  // bInheritHandle=TRUE
CreatePipe(&hReadPipe, &hWritePipe, &sa, 0);

STARTUPINFOA si = {};
si.cb = sizeof(si);
si.dwFlags = STARTF_USESTDHANDLES;
si.hStdInput  = hReadPipe;   // child reads from this pipe (attacker writes)
si.hStdOutput = hWritePipe;  // child writes to this (attacker reads)
si.hStdError  = hWritePipe;

PROCESS_INFORMATION pi;
CreateProcessA(NULL, "cmd.exe", NULL, NULL,
    TRUE,  // bInheritHandles = TRUE — passes pipe handles to child
    CREATE_NO_WINDOW, NULL, NULL, &si, &pi);
// Now we have a hidden cmd.exe communicating over pipes

Kernel-Side: What PspCreateProcess Does

At the kernel level, NtCreateUserProcess calls internal routines that:

  1. Open the image file: The executable path is resolved to a file object. The file is opened and the image section is created (a section object backed by the PE file).
  2. Allocate EPROCESS: The process executive object is allocated from the kernel pool and initialized.
  3. Create virtual address space: A new page directory is created — the clean address space for the new process.
  4. Map the image: The executable's sections are mapped into the new address space at ImageBase (or a random ASLR address). ntdll.dll is also mapped.
  5. Create PEB: The Process Environment Block is created in the new process's user-mode address space.
  6. Copy handle table: If bInheritHandles=TRUE, inheritable handles from the parent's handle table are copied.
  7. Create initial thread: ETHREAD is allocated; the thread's start address is set to ntdll!LdrInitializeThunk (not the PE's entry point — the loader runs first).
  8. Fire process creation callbacks: PsSetCreateProcessNotifyRoutineEx callbacks are called — this is where EDR drivers see the new process.

Process Creation Callbacks (EDR Hook Point)

The Windows kernel provides a callback registration mechanism that EDR drivers use to monitor process creation:

// Kernel driver registers a callback for process creation/termination
// Called in the context of the thread that created the process

VOID ProcessNotifyCallback(
    PEPROCESS  Process,      // new process's EPROCESS
    HANDLE     ProcessId,    // new process's PID
    PPS_CREATE_NOTIFY_INFO CreateInfo  // non-NULL = create, NULL = terminate
) {
    if (CreateInfo) {
        // Process is being created
        PUNICODE_STRING imgPath = CreateInfo->ImageFileName;
        PUNICODE_STRING cmdLine = CreateInfo->CommandLine;
        HANDLE parentPid = CreateInfo->ParentProcessId;
        // Log: new PID, image path, command line, parent PID
        // This is exactly what Sysmon Event ID 1 captures
    }
}

// Register at driver load time:
PsSetCreateProcessNotifyRoutineEx(&ProcessNotifyCallback, FALSE);

This callback fires for every new process. The CreateInfo struct includes the image path, command line, and parent PID — the core data in Sysmon Event ID 1. Because this is a kernel callback, it cannot be bypassed by hooking user-mode APIs. However, some techniques can influence the data seen by the callback.

Malware Process Creation Patterns

TechniqueCreation Flag / MethodDetection
Process hollowing CREATE_SUSPENDED → hollow → ResumeThread Suspended process at creation; later memory scan shows image mismatch; start address differs from PE header EP
Parent PID spoofing EXTENDED_STARTUPINFO_PRESENT + PROC_THREAD_ATTRIBUTE_PARENT_PROCESS Sysmon Event 1 shows unexpected parent; verify with process lineage tree
Hidden window (no console) CREATE_NO_WINDOW + STARTF_USESHOWWINDOW PowerShell/cmd spawned with CREATE_NO_WINDOW; STARTUPINFO shows wShowWindow=0
Command line spoofing Modify PEB.ProcessParameters.CommandLine after creation ETW command-line capture at creation time vs runtime PEB read may differ; kernel callback captures original
Token-based creation CreateProcessAsUser / CreateProcessWithToken Process token doesn't match parent's token; unexpected privilege level

Q & A

How does parent PID spoofing work and why does it matter for detection?

Parent PID spoofing uses the PROC_THREAD_ATTRIBUTE_PARENT_PROCESS attribute in STARTUPINFOEX to declare a different parent for the new process. The kernel then: (1) inherits handle tables from the declared parent rather than the real caller, (2) reports the spoofed parent PID in the process creation notification callback (so Sysmon Event ID 1 shows the spoofed parent). This matters because many detection rules check parent-child relationships: "if cmd.exe's parent is winword.exe, alert." If the attacker's malware (running as malware.exe) spawns cmd.exe but spoof's the parent to be explorer.exe, the Sysmon log shows explorer.exe → cmd.exe — a normal-looking relationship. The real calling process is not visible in standard telemetry. Detection improvements: (1) correlation with process lineage — explore.exe creating cmd.exe at an unusual time, (2) kernel-level monitoring that captures both the real caller and the declared parent, (3) behavior-based rules that don't rely on parent-child relationships.

At what point in CreateProcess does the process become visible to other processes?

A process becomes visible in the system-wide process list (ActiveProcessLinks) after PspCreateProcess inserts the new EPROCESS into the list — this happens before the process creation notify callbacks fire. So at the moment an EDR's callback is called, the process is already in the list and enumerable by tools like Task Manager. However, the initial thread hasn't started executing yet — the process object exists but is in a very early state. The handle returned to the creating process (via PROCESS_INFORMATION.hProcess) is valid immediately after CreateProcess returns. The process becomes "running" (initial thread begins executing) only after the kernel unblocks the initial thread, which for suspended processes (CREATE_SUSPENDED) doesn't happen until ResumeThread is called explicitly. This is why process hollowing can modify the process before it runs anything — you have a window between the process object existing and the process actually executing.