Windows进程重父化技术解析:安全检测的挑战与应对

本文深入探讨Windows进程重父化技术,分析其如何被恶意利用规避安全检测,并通过WinDbg调试和ETW跟踪揭示ProcMon工具在进程创建栈追踪中的误判机制。

Windows进程重父化技术解析

进程监控与错误的调用栈

近期我在研究Windows Terminal的运行机制时,使用Process Monitor(ProcMon)记录了其执行过程。ProcMon是一款SysInternals工具,能够记录进程的执行、文件系统、注册表和网络操作。在分析记录时,我注意到一个奇怪的现象:ProcMon显示explorer.exe启动了终端进程,这看似合理,因为explorer.exe通常是许多用户应用程序的父进程。但仔细查看调用栈后发现,第8和第9帧没有符号信息,甚至没有显示模块名称。许多人可能会认为这是堆中运行的动态内存shellcode。为了验证这一可能性,我使用调试器或Process Hacker(现称为System Informer)进行了调查。

这些栈帧指向的内存范围根本没有映射。因此,要么这是一个特别隐蔽的shellcode,我应该重新检查系统是否遭受了国家级攻击,要么就有其他解释。

为了找到根本原因,我转向了几乎总是可靠的调试器:WinDbg。我们将使用内核调试器来跟踪Windows Terminal的进程创建,并观察ProcMon操作的数据,这应该能揭示真正发生的事情。

首先,启动ProcMon记录会话,这会加载其内核驱动程序并注册进程通知例程。许多终端检测与响应(EDR)系统和系统监控工具使用此回调来获取进程创建和终止的通知。为了跟随ProcMon的步骤,我们将在此回调上设置断点并观察发生了什么。

进程创建回调列表保存在一个未导出的内核符号PspCreateProcessNotifyRoutine中。不幸的是,回调本身保存在一个公共符号中不可用的数据结构中,因此解析它们可能有些麻烦。但结构本身足够知名和稳定,我们可以使用硬编码偏移来解析它。我写了一个简单的单行脚本来打印所有注册的回调(还有许多其他示例可用)。如果您使用最新版本的WinDbg,甚至可以使用新的符号生成器推送结构,并像在符号中可用一样使用它!

运行此脚本,我们可以轻松找到ProcMon的进程回调:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
dx ((__int64(*)[64])&nt!PspCreateProcessNotifyRoutine)->Where(p => p)->Select(p =>
(void(*)())(*(((__int64*)(p & ~0xf)) + 1)))
    [0]              : 0xfffff80673f78900 : cng!CngCreateProcessNotifyRoutine+0x0
    [1]              : 0xfffff80674b29f50 : WdFilter+0x49f50
    [2]              : 0xfffff80673dbb4b0 : ksecdd!KsecCreateProcessNotifyRoutine+0x0
    [3]              : 0xfffff8067510db70 : tcpip!CreateProcessNotifyRoutineEx+0x0
    [4]              : 0xfffff8067561d990 : iorate!IoRateProcessCreateNotify+0x0
    [5]              : 0xfffff80673eea160 : CI!I_PEProcessNotify+0x0
    [6]              : 0xfffff80678d6a590 : dxgkrnl!DxgkProcessNotify+0x0
    [7]              : 0xfffff8068184acf0 : peauth+0x3acf0
    [8]              : 0xfffff80681b36400 : PROCMON24+0x6400

下一步是在此回调上设置断点,恢复机器执行,并从开始菜单运行Windows Terminal:

1
bp 0xfffff80681b36400; g

断点命中后,我们解析函数参数以了解ProcMon看到的内容。在现代系统上,ProcMon使用PsSetCreateProcessNotifyRoutineEx2注册其进程通知例程。这很重要,因为不同版本的进程通知例程接收的参数略有不同。在这种情况下,例程的类型为PCREATE_PROCESS_NOTIFY_ROUTINE_EX

1
2
3
4
5
void PcreateProcessNotifyRoutineEx (
  [_Inout_]           PEPROCESS Process,
  [in]                HANDLE ProcessId,
  [in, out, optional] PPS_CREATE_NOTIFY_INFO CreateInfo
)

有了这些知识,我们可以使用调试器数据模型以正确的类型呈现参数,就像驱动程序看到的那样。只有一个问题:PS_CREATE_NOTIFY_INFO未包含在公共符号中,因此我们无法轻松访问它。然而,它包含在公共ntddk.h头文件中,因此我们可以简单地将结构定义(稍作调整)复制到单独的头文件中,并通过Synthetic Types在调试器中使用它。为此,我们在c:\temp\ntddk_structs.h创建头文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
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;

接下来,通过synthetic types将其加载到调试器中:

1
dx Debugger.Utility.Analysis.SyntheticTypes.ReadHeader("c:\\temp\\ntddk_structs.h", "nt")

加载头文件后,我们拥有以正确类型格式化输入参数所需的一切:

1
dx @$procNotifyInput = new { Process = (nt!_EPROCESS*)@rcx, ProcessId = @rdx, CreateInfo = Debugger.Utility.Analysis.SyntheticTypes.CreateInstance("_PS_CREATE_NOTIFY_INFO", @r8) }

通过此,我们可以进一步查看CreateInfo以获取有关此新进程的更多信息,更重要的是,谁创建了它:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
dx @$procNotifyInput.CreateInfo
    Size             : 0x48
    Flags            : 0x1
    FileOpenNameAvailable : 0x1
    IsSubsystemProcess : 0x0
    Reserved         : 0x0
    ParentProcessId  : 0x1738
    CreatingThreadId [Type: _CLIENT_ID]
    FileObject       : 0xffffae0f90ac7d70 : "\Program Files\WindowsApps\Microsoft.WindowsTerminal_1.15.2713.0_x64__8wekyb3d8bbwe\WindowsTerminal.exe"
    ImageFileName    : 0xffffd28d2a447578 : "\??\C:\Program Files\WindowsApps\Microsoft.WindowsTerminal_1.15.2713.0_x64__8wekyb3d8bbwe\WindowsTerminal.exe"
    CommandLine      : 0xffffae0f92c5b070 : ""C:\Program Files\WindowsApps\Microsoft.WindowsTerminal_1.15.2713.0_x64__8wekyb3d8bbwe\WindowsTerminal.exe" "
    CreationStatus   : 0x0

dx @$procNotifyInput.CreateInfo.CreatingThreadId
    [+0x000] UniqueProcess    : 0x5ac
    [+0x008] UniqueThread     : 0x69c

您可能会对我们发现的内容感到惊讶:与ProcMon GUI显示的不同,在驱动程序中,我们似乎是在svchost.exe进程的上下文中运行,而不是explorer.exe。因此,实际上是svchost.exe正在创建新的Terminal进程!

1
2
3
dx @$curprocess
    Name             : svchost.exe
    Id               : 0x5ac

不幸的是,这并没有给我们完整的画面。如果svchost.exe正在创建新进程,为什么GUI声称它是explorer.exe?这个服务是什么,为什么它要创建Terminal进程?

为了获取更多信息,让我们检查调用栈:

 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
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     ntdll!NtCreateUserProcess+0x14
05 0000005a`52c7c308 00007ff8`268c8648     KERNELBASE!CreateProcessInternalW+0x2228
06 0000005a`52c7c310 00007ff8`268eea13     KERNELBASE!CreateProcessAsUserW+0x63
07 0000005a`52c7dc50 00007ff8`277bba80     KERNEL32!CreateProcessAsUserWStub+0x60
08 0000005a`52c7dcc0 00007ff8`0cd1239e     appinfo!AiLaunchProcess+0x69e
09 0000005a`52c7dd30 00007ff8`0cd131f1     appinfo!RAiLaunchProcessWithIdentity+0x901
0a 0000005a`52c7e5b0 00007ff8`27633803     RPCRT4!Invoke+0x73
0b 0000005a`52c7ec00 00007ff8`275c280a     RPCRT4!NdrAsyncServerCall+0x2ba
0c 0000005a`52c7edf0 00007ff8`276169f2     RPCRT4!DispatchToStubInCNoAvrf+0x22
0d 0000005a`52c7ee40 00007ff8`275d324f     RPCRT4!RPC_INTERFACE::DispatchToStubWorker+0x1af
0e 0000005a`52c7ef20 00007ff8`275d2e58     RPCRT4!RPC_INTERFACE::DispatchToStubWithObject+0x188
0f 0000005a`52c7efc0 00007ff8`275e2995     RPCRT4!LRPC_SCALL::DispatchRequest+0x175
10 0000005a`52c7f090 00007ff8`275e1fe7     RPCRT4!LRPC_SCALL::HandleRequest+0x837
11 0000005a`52c7f190 00007ff8`275e166b     RPCRT4!LRPC_SASSOCIATION::HandleRequest+0x24b
12 0000005a`52c7f210 00007ff8`275e1341     RPCRT4!LRPC_ADDRESS::HandleRequest+0x181
13 0000005a`52c7f2b0 00007ff8`275e0f77     RPCRT4!LRPC_ADDRESS::ProcessIO+0x897
14 0000005a`52c7f3f0 00007ff8`275e7559     RPCRT4!LrpcIoComplete+0xc9
15 0000005a`52c7f480 00007ff8`29102160     ntdll!TppAlpcpExecuteCallback+0x280
16 0000005a`52c7f500 00007ff8`290f6e48     ntdll!TppWorkerThread+0x448
17 0000005a`52c7f7f0 00007ff8`277b54e0     KERNEL32!BaseThreadInitThunk+0x10
18 0000005a`52c7f820 00000000`00000000     ntdll!RtlUserThreadStart+0x2b

现在这变得有趣了。查看用户模式栈(从第5帧开始)并与ProcMon中看到的用户模式栈进行比较——它们看起来几乎相同。而两个缺失的帧似乎属于appinfo.dll内部。那么发生了什么?

为了回答这个问题,我们将回到我们的CreateInfo数据和父进程与创建者进程的问题。我们将使用进程列表来查找每个ID代表的进程:

1
2
3
4
5
dx @$parentProcessId = @$procNotifyInput.CreateInfo.ParentProcessId : 0x1738
dx @$creatorProcessId = @$procNotifyInput.CreateInfo.CreatingThreadId.UniqueProcess : 0x5ac

dx @$cursession.Processes[@$parentProcessId] : explorer.exe
dx @$cursession.Processes[@$creatorProcessId] : svchost.exe

创建者进程ID似乎属于我们当前所在的同一个svchost.exe,因此这是创建Terminal进程的进程(也是ProcMon中显示调用栈的进程)。但父进程是explorer.exe,这就是ProcMon将其显示为创建者进程的原因——尽管它没有考虑创建者进程和父进程不同的情况,导致此调用栈被错误地链接到explorer.exe

进程重父化,解释

我们在这里看到的机制称为进程重父化。在创建进程时,创建者可以设置一个PROC_THREAD_ATTRIBUTE_PARENT_PROCESS属性,并包含指向不同进程的句柄,该进程将被用作父进程。此机制在系统中有各种用途,例如在不同于创建者进程的会话中创建进程。为了拥有逻辑进程树,以及出于技术原因,svchost.exe必须将子进程重父化到会话1中的不同父进程(如explorer.exe),以允许子进程使用控制台和UI。此机制也可用于隐藏进程的实际来源并混淆EDR。

ProcMon通过不检查请求进程创建的进程是否与请求的父进程相同来误解其接收的数据,导致我们观察到的错误栈。然而,通过使用内核驱动程序和进程创建通知,我们可以拥有所有必要的数据来判断进程是否被重父化。事实上,我们也可以从用户模式通过Microsoft-Windows-Kernel-Process ETW通道实现这一点。此通道默认未启用,但您可以注册为消费者并接收事件,或使用logman.exe生成跟踪并在事件查看器中查看。请注意,这些跟踪是在不同系统上运行的,因此PID与文章中之前看到的无关:

事件ID 1,ProcessStart,是我们关心的。由“常规”描述向我们显示的解析数据没有太大帮助,因为它仍然将重父化的进程指向“父”。然而,事件中的原始数据包括第三个字段,告诉我们更多:

在这里,我们并排看到两个进程创建事件。原始数据中有三个有用的字段:

  • System.Execution.ProcessID:请求创建新进程的进程(和线程)的ID
  • EventData.ProcessID:新创建的子进程的ID
  • EventData.ParentProcessID:被选为父进程的进程的ID

如果创建进程ID与父进程ID相同(左侧),则此进程未被重父化。但如果两个PID不相同(右侧),则此进程被重父化,我们获得创建者进程和所选父进程的ID!

我们仍在处理中

至此,我们已经调查了进程重父化以及我们在ProcMon中看到的奇怪行为。当然,这仍然没有完全解释Terminal进程的创建机制、创建它的服务以及appinfo DLL。所有这些都与打包应用程序的行为和实现有关,这是一个完全不同的话题。对于那些可能对创建机制感到好奇的人,您可以在此处找到更多信息,我可能会在未来的博客文章中添加更多细节(和调试技巧)。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计