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
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. Note 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 on 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 { nullptr};
OBJECT_ATTRIBUTES ParentAttributes{};
CLIENT_ID ParentID {};
ParentID.UniqueProcess = UlongToHandle(ParentProcessID);
if(!NT_SUCCESS(NtOpenProcess(&ParentHandle, PROCESS_CREATE_PROCESS, &ParentAttributes, &ParentID)))
{
std::werr << L"Failed to get handle to process with ID: " << ParentProcessID;
return -1;
}
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;
}