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 process
  • EventData.ProcessID: The ID of the newly created child process
  • EventData.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.