Windows进程重父化技术解析与调试实战

本文深入探讨Windows进程重父化技术,通过Process Monitor和WinDbg工具分析进程创建机制,揭示安全产品如何被绕过,并提供内核调试和ETW事件追踪的实用技术细节。

Windows进程重父化技术解析与调试实战

Process Monitor与错误的调用栈

最近我在研究Windows Terminal的运行机制(未来可能会专门写文章介绍)。我通过开始菜单启动Windows Terminal,并用Process Monitor(ProcMon)记录其执行过程。ProcMon是SysInternals工具,可记录进程的执行、文件系统、注册表和网络操作。查看记录时,我注意到一些奇怪的现象:

根据ProcMon显示,explorer.exe启动了终端进程。这看起来合理,因为explorer.exe通常是许多用户应用程序的父进程。但仔细查看调用栈会发现一些缺口:第8和第9帧没有符号信息,甚至没有显示模块名称。许多人会认为这是shellcode:在常规模块之外从堆中运行的动态内存。我们可以使用调试器或Process Hacker(现称System Informer)等工具来调查这种可能性。Process Hacker的输出如下所示。

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

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

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

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
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 (*)()]

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

1
bp 0xfffff80681b36400; g

我们的断点被触发了!

1
2
3
4
1: kd> g
Breakpoint 0 hit
PROCMON24+0x6400:
fffff806`81b36400 4d8bc8          mov     r9,r8

为了更深入了解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
2
3
4
5
6
7
8
9
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

一旦头文件加载完成,我们就有了所需的一切来用正确的类型格式化输入参数:

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

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

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
dx @$curprocess
@$curprocess                 : svchost.exe [Switch To]
    KernelObject     [Type: _EPROCESS]
    Name             : svchost.exe
    Id               : 0x5ac
    Handle           : 0xf0f0f0f0
    Threads
    Modules
    Environment
    Devices
    Io

不幸的是,这并没有给我们完整的画面。如果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
26
27
#   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

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

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

 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
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

创建者进程ID似乎属于我们当前所在的同一个svchost.exe,所以这是创建Terminal进程的进程(也是ProcMon中显示调用栈的进程)。但父进程是explorer.exe,这就是ProcMon将其显示为创建者进程的原因——尽管它没有

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