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将其显示为创建者进程的原因——尽管它没有