A Primer on Process Reparenting in Windows
By Yarden Shafir
Process reparenting is a technique used in Microsoft Windows to create a child process under a different parent process than the one making the call to CreateProcess
. Malicious actors can use this technique to evade security products or break process ancestry ties, making detection more challenging. However, process reparenting is also used legitimately across the operating system, for example during execution of packaged or store applications. Like many features, process reparenting can confuse both security products and security teams, leading to either missed detections or false positives on otherwise-innocent applications. This blog post will look at how to investigate this interesting behavior.
Process Monitor and the Incorrect Stack Trace
Lately I was playing around with the Windows Terminal and the way it runs and operates (something I might write about more in a future blog post). I ran the Windows Terminal through the Windows start menu and recorded its execution with Process Monitor (ProcMon), a SysInternals tool that records the execution, file system, registry, and network operations of a process. When I looked at the recording, I noticed something strange:
According to ProcMon, explorer.exe
is starting the terminal process. This makes sense, as explorer.exe
is generally the parent process of many user applications. But a close look at the call stack reveals some gaps: Frames 8 and 9 have no symbols and don’t even show a module name. Many would assume this is a shellcode: dynamic memory running from the heap, outside of a regular module. We can investigate this possibility using a debugger or a tool like Process Hacker (now known as System Informer). The output of Process Hacker is shown below.
The memory range to which these stack frames point isn’t mapped at all. So either this is an especially sneaky shellcode and I should recheck my system for yet another nation-state attack, or there is a different explanation.
To get to the root cause, I turn to the (almost) always-reliable debugger: WinDbg. We’ll use a kernel debugger to track the process creation of the Windows Terminal and observe the data on which ProcMon operates, which should give some indication about what’s really going on.
First, let’s start a recording session with ProcMon, which makes it load its kernel driver and register a process notify routine. Many Endpoint Detection and Response (EDR) systems and system monitoring tools use this callback to get notified about process creation and termination. To follow ProcMon’s steps, we’ll set a breakpoint on this callback and see what happens.
The list of process creation callbacks is saved in an unexported kernel symbol called PspCreateProcessNotifyRoutine
. Unfortunately, the callbacks themselves are saved in a data structure that isn’t available in the public symbols, so parsing them can be a bit of a pain. But the structure itself is sufficiently well known and stable that we can use hard-coded offsets to parse it. I wrote a simple one-line script to print all the registered callbacks (many other examples are available). If you’re using the newest version of WinDbg, you can even use the new symbol builder to push the structure and use it as if it were available in the symbols!
Running this script, we can easily find ProcMon’s process callback:
dx ((__int64(*)[64])&nt!PspCreateProcessNotifyRoutine)->Where(p => p)->Select(p => (void(*)())(*(((__int64*)(p & ~0xf)) + 1))) ((__int64(*)[64])&nt!PspCreateProcessNotifyRoutine)->Where(p => p)->Select(p => (void(*)())(*(((__int64*)(p & ~0xf)) + 1))) [0] : 0xfffff80673f78900 : cng!CngCreateProcessNotifyRoutine+0x0 [Type: void (*)()] [1] : 0xfffff80674b29f50 : WdFilter+0x49f50 [Type: void (*)()] [2] : 0xfffff80673dbb4b0 : ksecdd!KsecCreateProcessNotifyRoutine+0x0 [Type: void (*)()] [3] : 0xfffff8067510db70 : tcpip!CreateProcessNotifyRoutineEx+0x0 [Type: void (*)()] [4] : 0xfffff8067561d990 : iorate!IoRateProcessCreateNotify+0x0 [Type: void (*)()] [5] : 0xfffff80673eea160 : CI!I_PEProcessNotify+0x0 [Type: void (*)()] [6] : 0xfffff80678d6a590 : dxgkrnl!DxgkProcessNotify+0x0 [Type: void (*)()] [7] : 0xfffff8068184acf0 : peauth+0x3acf0 [Type: void (*)()] [8] : 0xfffff80681b36400 : PROCMON24+0x6400 [Type: void (*)()]
The next step is setting a breakpoint on this callback, resuming the machine’s execution, and running the Windows Terminal from the start menu:
bp 0xfffff80681b36400; g
And our breakpoint gets hit!
1: kd> g Breakpoint 0 hit PROCMON24+0x6400: fffff806`81b36400 4d8bc8 mov r9,r8
To get more insight into what ProcMon sees, we should parse the function arguments. I’ll skip a couple of reverse engineering steps (if I don’t, this post will just keep on going forever) and simply let you know that on modern systems, ProcMon registers its process notify routine using PsSetCreateProcessNotifyRoutineEx2. This matters because different versions of the process-notify routine receive slightly different arguments. In this case, the routine has the type PCREATE_PROCESS_NOTIFY_ROUTINE_EX:
void PcreateProcessNotifyRoutineEx ( [_Inout_] PEPROCESS Process, [in] HANDLE ProcessId, [in, out, optional] PPS_CREATE_NOTIFY_INFO CreateInfo )
With this knowledge, we can use the debugger data model to present the arguments with the correct types, just as the driver sees them. There’s only one issue: PS_CREATE_NOTIFY_INFO
isn’t included in the public symbols, so we don’t have easy access to it. It is, however, included in the public ntddk.h
header, so we can simply copy the structure definition (with minimal adjustments) into a separate header and use it in the debugger through Synthetic Types. To that end, let’s create the header file under c:\temp\ntddk_structs.h
:
typedef struct _PS_CREATE_NOTIFY_INFO { ULONG64 Size; union { _In_ ULONG Flags; struct { _In_ ULONG FileOpenNameAvailable : 1; _In_ ULONG IsSubsystemProcess : 1; _In_ ULONG Reserved : 30; }; }; HANDLE ParentProcessId; _CLIENT_ID CreatingThreadId; _FILE_OBJECT *FileObject; _UNICODE_STRING *ImageFileName; _UNICODE_STRING *CommandLine; ULONG CreationStatus; } PS_CREATE_NOTIFY_INFO, *PPS_CREATE_NOTIFY_INFO;
Next, let’s load it into the debugger through synthetic types:
dx Debugger.Utility.Analysis.SyntheticTypes.ReadHeader("c:\\temp\\ntddk_structs.h", "nt") Debugger.Utility.Analysis.SyntheticTypes.ReadHeader("c:\\temp\\ntddk_structs.h", "nt") : ntkrnlmp.exe(ntddk_structs.h) ReturnEnumsAsObjects : false RegisterSyntheticTypeModels : false Module : ntkrnlmp.exe Header : ntddk_structs.h Types
(Side note: Try not to make any mistakes in your header files or you’ll have to restart the debugger session to reload the fixed version of the structure. It’s not currently possible to unload or reload header files, so the only options are to reload a separate header file with a differently named structure, or to restart the debugger session and try again.)
Once the header is loaded, we have everything we need to format the input arguments with the correct types:
dx @$procNotifyInput = new { Process = (nt!_EPROCESS*)@rcx, ProcessId = @rdx, CreateInfo = Debugger.Utility.Analysis.SyntheticTypes.CreateInstance("_PS_CREATE_NOTIFY_INFO", @r8) } dx @$procNotifyInput = new { Process = (nt!_EPROCESS*)@rcx, ProcessId = @rdx, CreateInfo = Debugger.Utility.Analysis.SyntheticTypes.CreateInstance("_PS_CREATE_NOTIFY_INFO", @r8) } @$procNotifyInput = new { Process = (nt!_EPROCESS*)@rcx, ProcessId = @rdx, CreateInfo = Debugger.Utility.Analysis.SyntheticTypes.CreateInstance("_PS_CREATE_NOTIFY_INFO", @r8) } Process : 0xffffae0f92e0f0c0 [Type: _EPROCESS *] ProcessId : 0x197c [Type: unsigned __int64] CreateInfo
With this, we can look further into CreateInfo
to gain more information about this new process—and more importantly, who created it:
dx @$procNotifyInput.CreateInfo @$procNotifyInput.CreateInfo Size : 0x48 Flags : 0x1 FileOpenNameAvailable : 0x1 IsSubsystemProcess : 0x0 Reserved : 0x0 ParentProcessId : 0x1738 [Type: void *] CreatingThreadId [Type: _CLIENT_ID] FileObject : 0xffffae0f90ac7d70 : "\Program Files\WindowsApps\Microsoft.WindowsTerminal_1.15.2713.0_x64__8wekyb3d8bbwe\WindowsTerminal.exe" - Device for "\FileSystem\Ntfs" [Type: _FILE_OBJECT *] ImageFileName : 0xffffd28d2a447578 : "\??\C:\Program Files\WindowsApps\Microsoft.WindowsTerminal_1.15.2713.0_x64__8wekyb3d8bbwe\WindowsTerminal.exe" [Type: _UNICODE_STRING *] CommandLine : 0xffffae0f92c5b070 : ""C:\Program Files\WindowsApps\Microsoft.WindowsTerminal_1.15.2713.0_x64__8wekyb3d8bbwe\WindowsTerminal.exe" " [Type: _UNICODE_STRING *] CreationStatus : 0x0 dx @$procNotifyInput.CreateInfo.CreatingThreadId @$procNotifyInput.CreateInfo.CreatingThreadId [Type: _CLIENT_ID] [+0x000] UniqueProcess : 0x5ac [Type: void *] [+0x008] UniqueThread : 0x69c [Type: void *]
First, we can now be sure that the newly created process is the Windows Terminal. And second, we can spot some interesting details about who created it. Two fields are of interest here: ParentProcessId
and CreatingThreadId
, the latter of which also contains a UniqueProcess
field (this is the process ID of the process that owns this thread). Before we try to understand why these are different, let’s take a small step back and examine the context of the process we’re currently in. Since process-notify routines are called in the context of the process that is creating the new child process, this might explain the strange call stack we saw earlier and clarify the creation of this Terminal process.
You might be surprised by what we discover: Unlike what the ProcMon GUI showed, in the driver it seems that we are running in the context of an svchost.exe
process and not explorer.exe
. So it is actually svchost.exe
that is creating the new Terminal process!
dx @$curprocess @$curprocess : svchost.exe [Switch To] KernelObject [Type: _EPROCESS] Name : svchost.exe Id : 0x5ac Handle : 0xf0f0f0f0 Threads Modules Environment Devices Io
Unfortunately, this doesn’t give us the full picture. If svchost.exe
is creating the new process, why does the GUI claim it is explorer.exe
? What is this service, and why is it creating the Terminal process at all?
To get some more information, let’s examine the call stack:
# Child-SP RetAddr Call Site 00 ffffd28d`2a446bd8 fffff806`731bacc2 PROCMON24+0x6400 01 ffffd28d`2a446be0 fffff806`730993a5 nt!PspCallProcessNotifyRoutines+0x206 02 ffffd28d`2a446cb0 fffff806`7308cec0 nt!PspInsertThread+0x639 03 ffffd28d`2a446d80 fffff806`72e39375 nt!NtCreateUserProcess+0xe10 04 ffffd28d`2a447a30 00007ff8`29185514 nt!KiSystemServiceCopyEnd+0x25 05 0000005a`52c7c308 00007ff8`268c8648 ntdll!NtCreateUserProcess+0x14 06 0000005a`52c7c310 00007ff8`268eea13 KERNELBASE!CreateProcessInternalW+0x2228 07 0000005a`52c7dc50 00007ff8`277bba80 KERNELBASE!CreateProcessAsUserW+0x63 08 0000005a`52c7dcc0 00007ff8`0cd1239e KERNEL32!CreateProcessAsUserWStub+0x60 09 0000005a`52c7dd30 00007ff8`0cd131f1 appinfo!AiLaunchProcess+0x69e 0a 0000005a`52c7e5b0 00007ff8`27633803 appinfo!RAiLaunchProcessWithIdentity+0x901 0b 0000005a`52c7ec00 00007ff8`275c280a RPCRT4!Invoke+0x73 0c 0000005a`52c7ece0 00007ff8`276169f2 RPCRT4!NdrAsyncServerCall+0x2ba 0d 0000005a`52c7edf0 00007ff8`275d324f RPCRT4!DispatchToStubInCNoAvrf+0x22 0e 0000005a`52c7ee40 00007ff8`275d2e58 RPCRT4!RPC_INTERFACE::DispatchToStubWorker+0x1af 0f 0000005a`52c7ef20 00007ff8`275e2995 RPCRT4!RPC_INTERFACE::DispatchToStubWithObject+0x188 10 0000005a`52c7efc0 00007ff8`275e1fe7 RPCRT4!LRPC_SCALL::DispatchRequest+0x175 11 0000005a`52c7f090 00007ff8`275e166b RPCRT4!LRPC_SCALL::HandleRequest+0x837 12 0000005a`52c7f190 00007ff8`275e1341 RPCRT4!LRPC_SASSOCIATION::HandleRequest+0x24b 13 0000005a`52c7f210 00007ff8`275e0f77 RPCRT4!LRPC_ADDRESS::HandleRequest+0x181 14 0000005a`52c7f2b0 00007ff8`275e7559 RPCRT4!LRPC_ADDRESS::ProcessIO+0x897 15 0000005a`52c7f3f0 00007ff8`29102160 RPCRT4!LrpcIoComplete+0xc9 16 0000005a`52c7f480 00007ff8`290f6e48 ntdll!TppAlpcpExecuteCallback+0x280 17 0000005a`52c7f500 00007ff8`277b54e0 ntdll!TppWorkerThread+0x448 18 0000005a`52c7f7f0 00007ff8`290e485b KERNEL32!BaseThreadInitThunk+0x10 19 0000005a`52c7f820 00000000`00000000 ntdll!RtlUserThreadStart+0x2b
Now this is getting interesting. Look at the user-mode stack (starting from frame 5) and compare it to the user-mode stack seen in ProcMon–they look nearly identical. And the two missing frames seem to belong inside appinfo.dll
. So what is happening?
To answer that, we’ll go back to our CreateInfo
data and the Parent vs. Creator process issue. We’ll use the process list to find which process each of these IDs represent:
dx @$parentProcessId = @$procNotifyInput.CreateInfo.ParentProcessId @$parentProcessId = @$procNotifyInput.CreateInfo.ParentProcessId : 0x1738 [Type: void *] dx @$creatorProcessId = @$procNotifyInput.CreateInfo.CreatingThreadId.UniqueProcess @$creatorProcessId = @$procNotifyInput.CreateInfo.CreatingThreadId.UniqueProcess : 0x5ac [Type: void *] dx @$cursession.Processes[@$parentProcessId] @$cursession.Processes[@$parentProcessId] : explorer.exe [Switch To] KernelObject [Type: _EPROCESS] Name : explorer.exe Id : 0x1738 Handle : 0xf0f0f0f0 Threads Modules Environment Devices Io dx @$cursession.Processes[@$creatorProcessId] @$cursession.Processes[@$creatorProcessId] : svchost.exe [Switch To] KernelObject [Type: _EPROCESS] Name : svchost.exe Id : 0x5ac Handle : 0xf0f0f0f0 Threads Modules Environment Devices Io
The Creator process ID seems to belong to the same svchost.exe
whose context we’re currently in, so this is the process creating the Terminal process (and the one whose call stack is shown in ProcMon). But the parent process is explorer.exe
, which is the reason ProcMon is displaying it as the creator process—although it doesn’t consider the case where the creator process and the parent process are different, causing this call stack to be incorrectly linked to explorer.exe
.
Process Reparenting, Explained
The mechanism we’re seeing here is called process reparenting. When creating a process, the creator can set a PROC_THREAD_ATTRIBUTE_PARENT_PROCESS
attribute and include the handle to a different process, which will be used as the parent process. This mechanism has various uses across the system, such as creating a process in a different session than the creator process. To have a logical process tree, as well as for technical reasons, svchost.exe
must reparent the child process to a different parent in session 1 (such as explorer.exe
) in order to allow the child process to use the console and the UI. This mechanism can also be used to hide the actual origin of processes and confuse EDRs.
ProcMon misinterprets the data it receives by not checking to see if the process requesting the process creation is the same one as the requested parent, causing the incorrect stack we observed. However, by using a kernel driver and process creation notifications, we can have all the data necessary to tell if a process is being reparented. In fact, we can also do this from user mode, through the Microsoft-Windows-Kernel-Process ETW channel. This channel is not enabled by default, but you can register as a consumer and receive events, or use logman.exe
to generate a trace and view it in Event Viewer. Note that these traces were run on a different system, so the PIDs are unrelated to the ones seen earlier in the post:
Event ID 1, ProcessStart, is the one we care about. The parsed data shown to us by the “general” description isn’t too helpful, as it will still point to the reparented process as the “parent.” However, the raw data in the event includes a third field that tells us more:
Here we see, side by side, two process creation events. In the raw data are three helpful fields:
System.Execution.ProcessID
: The ID of the process (and thread) that requested the creation of the new processEventData.ProcessID
: The ID of the newly created child processEventData.ParentProcessID
: The ID of the process that was chosen as the parent
If the creating process ID is identical to the parent process ID (on the left side), this process wasn’t reparented. But if the two PIDs are not identical (on the right side), then this process was reparented and we get the IDs of both the creator process and the chosen parent!
We’re still processing
At this point, we’ve investigated process reparenting and the strange behavior we saw in ProcMon. Of course, this still doesn’t fully explain the mechanism behind the creation of the Terminal process, the service creating it, and the appinfo DLL. That all relates to the behavior and implementation of packaged applications, which is a whole other topic. For those who might be curious about the creation mechanism, you can find more information about that here, and I might add some more details (and debugging tips) in a future blog post.