6

Windows 进程的创建和终止 - frendguo

 2 years ago
source link: https://www.cnblogs.com/frendguo/p/16480012.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

创建一个进程

917989-20220715074546455-565666253.png

如图,创建一个进程主要分为两部分,用户态部分和内核部分。

既然我们想看看一个进程是怎么被创建的,那我们就用 WinDbg 来看看从用户态到内核态都调用了什么:

第一步:我们先看看 nt 下有哪些方法跟创建进程相关的

0: kd> x nt!*CreateProcess*fffff802`55d8a218 nt!PspSetCreateProcessNotifyRoutine (void)fffff802`55cd9714 nt!ExpWnfCreateProcessContext (void)fffff802`55dd9a2f nt!PspCreateProcess$filt$0 (void)fffff802`55be24f4 nt!PspDeleteCreateProcessContext (void)fffff802`55c40ed0 nt!MmCreateProcessAddressSpace (void)fffff802`55dbd430 nt!PspCreateProcess (void)fffff802`5594fb10 nt!ViCreateProcessCallback (void)fffff802`55fdaaa4 nt!ViCreateProcessCallbackInternal (ViCreateProcessCallbackInternal)fffff802`55f04550 nt!NtCreateProcessEx (NtCreateProcessEx)fffff802`55fd1ce0 nt!VerifierPsSetCreateProcessNotifyRoutineEx (VerifierPsSetCreateProcessNotifyRoutineEx)fffff802`559f4bf0 nt!ZwCreateProcessEx (ZwCreateProcessEx)fffff802`56349360 nt!pXdvPsSetCreateProcessNotifyRoutineEx = <no type information>fffff802`55cfd12c nt!PspValidateCreateProcessProtection (PspValidateCreateProcessProtection)fffff802`55d89ea0 nt!PsSetCreateProcessNotifyRoutineEx (PsSetCreateProcessNotifyRoutineEx)fffff802`5632e9d4 nt!PspCreateProcessNotifyRoutineCount = <no type information>fffff802`55d89f00 nt!PsSetCreateProcessNotifyRoutineEx2 (PsSetCreateProcessNotifyRoutineEx2)fffff802`5632e9d8 nt!PspCreateProcessNotifyRoutineExCount = <no type information>fffff802`55d8a050 nt!PsSetCreateProcessNotifyRoutine (PsSetCreateProcessNotifyRoutine)fffff802`55ed2e70 nt!MiCreateProcessDefaultAweInfo (MiCreateProcessDefaultAweInfo)fffff802`55be0d1c nt!PspBuildCreateProcessContext (PspBuildCreateProcessContext)fffff802`559f5970 nt!ZwCreateProcess (ZwCreateProcess)fffff802`562ec260 nt!PspCreateProcessNotifyRoutine = <no type information>fffff802`55fd1cc0 nt!VerifierPsSetCreateProcessNotifyRoutine (VerifierPsSetCreateProcessNotifyRoutine)fffff802`56349378 nt!pXdvPsSetCreateProcessNotifyRoutine = <no type information>fffff802`55f044c0 nt!NtCreateProcess (NtCreateProcess)

第二步:我们选择 nt!MmCreateProcessAddressSpace 打上断点(不要问我为啥选这个,实在不会选,就直接 bm nt!CreateProcess

0: kd> bu nt!MmCreateProcessAddressSpaceBreakpoint 2 hitnt!MmCreateProcessAddressSpace:fffff802`55c40ed0 488bc4 mov rax,rsp0: kd> k # Child-SP RetAddr Call Site00 ffff928c`4e5a7b48 fffff802`55d08608 nt!MmCreateProcessAddressSpace01 ffff928c`4e5a7b50 fffff802`55cff75a nt!PspAllocateProcess+0x13ec02 ffff928c`4e5a82c0 fffff802`55a096b5 nt!NtCreateUserProcess+0xa1a03 ffff928c`4e5a8a90 00007ff8`cfc2e634 nt!KiSystemServiceCopyEnd+0x2504 00000000`02aac598 00007ff8`cd818e73 ntdll!NtCreateUserProcess+0x1405 00000000`02aac5a0 00007ff8`cd8171a6 KERNELBASE!CreateProcessInternalW+0xfe306 00000000`02aadb70 00007ff8`ced4cbb4 KERNELBASE!CreateProcessW+0x6607 00000000`02aadbe0 00007ff8`cb56152d KERNEL32!CreateProcessWStub+0x5408 00000000`02aadc40 00007ff8`cb4f6722 windows_storage!CInvokeCreateProcessVerb::CallCreateProcess+0x2cd09 00000000`02aadef0 00007ff8`cb55a75c windows_storage!CInvokeCreateProcessVerb::_PrepareAndCallCreateProcess+0x2d60a 00000000`02aadf70 00007ff8`cb55a583 windows_storage!CInvokeCreateProcessVerb::_TryCreateProcess+0x3c0b 00000000`02aadfa0 00007ff8`cb55a46d windows_storage!CInvokeCreateProcessVerb::Launch+0xef0c 00000000`02aae040 00007ff8`cb599dc4 windows_storage!CInvokeCreateProcessVerb::Execute+0x5d0d 00000000`02aae080 00007ff8`cb481d87 windows_storage!CBindAndInvokeStaticVerb::InitAndCallExecute+0x2140e 00000000`02aae100 00007ff8`cb4f5787 windows_storage!CBindAndInvokeStaticVerb::TryCreateProcessDdeHandler+0x630f 00000000`02aae180 00007ff8`cb54586d windows_storage!CBindAndInvokeStaticVerb::Execute+0x1e710 00000000`02aae4a0 00007ff8`cb545785 windows_storage!RegDataDrivenCommand::_TryInvokeAssociation+0xad11 00000000`02aae500 00007ff8`ce152b22 windows_storage!RegDataDrivenCommand::_Invoke+0x14112 00000000`02aae570 00007ff8`ce1529da SHELL32!CRegistryVerbsContextMenu::_Execute+0xce13 00000000`02aae5e0 00007ff8`ce15630c SHELL32!CRegistryVerbsContextMenu::InvokeCommand+0xaa14 00000000`02aae8e0 00007ff8`ce15618d SHELL32!HDXA_LetHandlerProcessCommandEx+0x10c15 00000000`02aae9f0 00007ff8`cb93be08 SHELL32!CDefFolderMenu::InvokeCommand+0x13d16 00000000`02aaed50 00007ff8`cb93c7b6 windows_storage!CShellLink::_InvokeDirect+0x1d017 00000000`02aaf070 00007ff8`cb93945a windows_storage!CShellLink::_ResolveAndInvoke+0x20218 00000000`02aaf230 00007ff8`ce15630c windows_storage!CShellLink::InvokeCommand+0x1aa19 00000000`02aaf310 00007ff8`ce15618d SHELL32!HDXA_LetHandlerProcessCommandEx+0x10c1a 00000000`02aaf420 00007ff8`ce3709d5 SHELL32!CDefFolderMenu::InvokeCommand+0x13d1b 00000000`02aaf780 00007ff8`ce6244f9 SHELL32!SHInvokeCommandOnContextMenu2+0x1f51c 00000000`02aaf9c0 00007ff8`ceeec3f9 SHELL32!s_DoInvokeVerb+0xc91d 00000000`02aafa30 00007ff8`ced47034 shcore!_WrapperThreadProc+0xe91e 00000000`02aafb10 00007ff8`cfbe2651 KERNEL32!BaseThreadInitThunk+0x141f 00000000`02aafb40 00000000`00000000 ntdll!RtlUserThreadStart+0x21

如上 Windbg 输出的结果所示,正是描述了从用户态的 CreateProcess → 内核态的 NtCreateUserProcess. 其他链路,比如 CreateProcessAsTokenW 我们也可以验证下,这里就不做赘述。

用户态部分,包含一些我们常用到的方法:CreateProcess, CreateProcessAsUser, CreateProcessWithLogonW, CreateProcessAsTokenW.

而内核部分,则都是通过 NT 下的 NtCreateUserProcess 来进行创建。

💡 NtCreateUserProcess 只负责创建用户模式的进程,而内核模式的进程则会通过 NtCreateProcessEx 来创建。两者虽然是不同的调用,但都会调用相同的 PspAllocateProcess。

创建进程流程

创建一个进程,主要以下7个步骤。

917989-20220715074444863-115154730.png

步骤1:转换、验证参数和标志

这一步骤主要是将从用户态参数到内核态参数的一个转换,同时添加必要的标识。

其中主要包含以下部分:

  1. 优先级的确定
  2. Native 属性和 Win32 属性的映射
  3. 对现代应用(modern application)的特定标识(PROC_THREAD_ATTRIBUTE_PACKAGE_FULL_NAME),方便后续特殊处理
  4. Debug 和 Error 的预设
  5. 确定特定的桌面环境(指进程需要创建到哪个特定的虚拟桌面)

Windows 的虚拟桌面其实只有一个 Desktop 对象,使用 desktop.exe 可以真正的创建多个虚拟桌面。

  1. 将参数做转换。(比如 c:\temp\a.exe 可能转换成 \device\harddiskvolume1\temp\a.exe)

做完这些工作,创建进程的用户态的初始化基本就结束了。接下来就会尝试调用内核态的 NtCreateUserProcess 来创建进程。

步骤2:打开要执行的镜像

917989-20220715074602701-2141013567.png

这个部分已经切换到内核模式执行了,主要目的就是确定要怎么打开镜像。

主要包含以下部分:

  1. 如上图,根据要打开的镜像文件确认需要真正执行的进程。(比如,当文件是一个 .cmd 文件时,真正需要执行的是 cmd.exe 这个进程,那就需要重新回到步骤一 CreateProcessInternalW )
  2. 如果进程是现代应用,则需要确定他的证书,确保它是可以被运行的。(比如非商店应用在Windows 设定不运行旁加载的情况下是不能被运行的)
  3. 如果进程是 Trustlet,还需要添加特定的标识
  4. 会尝试去打开 Windows exe 文件。首先创建 section object,然后判断其是否可以被打开。
  5. 然后会在 Computer\HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options 下寻找特定的 option。例如要打开的文件是SppExtComObj.exe,就会找到确认 Image File Execution Options 下是否存在 SppExtComObj.exe 子项,如果存在 PspAllocateProcess 就会再去找是否存在 debugger 的key,如果这个key存在,就会将 debugger 的值替换 SppExtComObj.exe,并且重新运行步骤一。

这就是镜像劫持(IFEO)的原理了,通过自行在注册表中创建子项,就可以实现想打开A进程,实际打开B进程

  1. 对于非 Windows exe 文件来说,有以下这些行为:
917989-20220715074613719-1422512352.png

至此,Windows 就已经可以打开一个可执行文件并且创建了部分对象,并映射到新的进程的地址空间了。

步骤3:创建 Windows 执行体进程对象

这个部分主要是通过 PspAllocateProcess 来创建 Windows 执行体对象(也就是内核中描述进程的对象)。主要分为以下几个部分:

  1. 设置 EPROCESS 对象。初始化或者从父进程继承属性,同时会根据 IFEO 的各种 key 来确定对应的值(比如:UseLargePages、PerfOptions、IoPriority、PagePriority、CpuPriorityClass、WorkingSetLimitInKB)
  2. 创建初始进程地址空间。
  3. 创建内核进程结构,也就是是初始化 KPROCESS。
  4. 完成进程地址空间的设置。(这块需要有一些内存管理上的知识,有点迷糊,后面再来补上 @frend guo )
  5. 配置 PEB。
  6. 完成执行体进程对象的配置。

到此,Windows 执行进程对象已经创建完成了。接下来就该创建第一个线程了。

步骤4:创建初始线程的线程栈和上下文

这个部分主要是创建进程中的第0个线程,并且将其栈和上下文初始化完成。

由于其是在内核中直接创建的线程,所以跟用户模式下创建线程会有些不一样。这里主要分为 PspAllocateThread 和 PspInsertThread 两个部分来分析。

对于 PspAllocateThread 主要包含以下工作:

  • 阻止 WOW64 进程的 UMS,还阻止了用户模式下 System 进程中创建线程的调用
  • 创建执行体线程对象并初始化
  • LPC、IO管理和执行体用到的各种列表都将初始化
  • 线程创建时间、TID都将被创建
  • 创建线程的栈和上下文
  • 为新线程分配 TEB
  • 配置 ETHREAD、KTHREAD(通过KeInitThread)

对于 PspInsertThread,主要包含以下工作:

  • 跟据属性做一些线程的初始化工作,然后再插入到进程的线程列表里。比如初始化线程的首选处理器(thread ideal processor)、线程组亲和性(thread group affinity)、初始化安全线程对象(如果是IUM下)、调度设置、动态优先级、线程量子(thread quantum)。
  • 将线程对象插入到进程句柄表(process handle table)
  • 如果是进程的第一个线程被创建,所有进程注册的回调都会被调用。
  • 会调用 KeReadyThread 回应执行体,已经处理准备好的状态。

到这里,已经创建了必要的进程和线程对象了。

步骤5:执行 Windows 子系统特定的初始化

这个步骤主要是做一些用户模式下的检查和初始化。也是 Windows 子系统登记此进程的过程。

  1. Windows 会做一些检查来确保 Windows 是允许该进程运行的。比如校验镜像版本、确保 Windows 认证是否阻止此进程(策略)以及在一些特定 Windows 版本中,是否导入了系统不允许导入的 DLL 或者 API。
  2. 如果软件策略有约束,则会为此进程创建一个约束的 token,并将其存储到 PEB 中
  3. CreateProcessInternalW 会调用一些内部方法来获取系统的 SxS 信息
  4. 根据收集到的要发送到 Csrss 的信息构造到 Windows 子系统的消息。
  5. 在收到消息后,Windows 子系统将执行以下步骤:
    1. CsrCreateProcess 会为进程和线程复制句柄。进程和线程的使用计数(usage count)将会从1增加到2
    2. 分配 Csrss process structure (CSR_PROCESS)
    3. Csrss 线程结构(CSR_THREAD)将会被分配并初始化
    4. 通过 CsrCreateThread 将线程插入到进程的线程列表中
    5. 会话中的进程计数会递增
    6. 设置进程的关闭优先级(The process shutdown level)为 0x280。也就是进程默认的等级。
    7. 新创建的 csrss 进程结构将会被插入到 Windows 子系统范畴的进程列表中。

到此,进程、线程的环境建好,需要使用的资源也已经分配好了,Windows 子系统也知道并登记此进程和线程。于是就可以开始执行初始线程了。

步骤6:开始执行初始线程

这个阶段,除非调用者指定 CREATE_SUSPENDED,否则,初始化线程都将恢复运行,并开始执行后续的进程初始化工作。此时的初始化工作已经切换到新进程中了。

步骤7:在新进程的上下文中执行进程的初始化

新的线程开始运行,将在内核模式运行 KiStartUserThread,它将线程的 IRQL 从 DPC 降低到 APC,然后再调用系统初始化线程例程 PspUserThreadStartup,它将执行以下步骤:

  1. 安装异常链。
  2. 将 IRQL 降低至 PASSIVE_LEVEL(也就是0)
  3. 禁用运行时交换主进程 token 的能力
  4. 根据内核模式下的数据结构(KTHREAD) 设置 TEB 中的 local ID 和线程的首选处理器
  5. 调用 DbgkCreateThread 来检查新的进程是否向镜像发送消息。(用于 load dll)
  6. 然后继续做一些列的检查。
  7. 接下来就会切换到用户模式下,回到 RtlUserThreadStart 中。

到这里,进程的创建就结束了,将执行 Image 中的入口方法,进到进程的上下文中去。

结束一个进程

结束进程主要分为两种方式:主动结束(ExitProcess)和被动停止(TerminateProcess)。

ExitProcess 和 TerminateProcess 调用的执行体中的 NtTerminalProcess。它主要执行以下逻辑:

  1. 轮询进程中所有线程,如果不是当前线程,调用 PspTerminateThreadByPointer 结束(需要等待返回)。
  2. 如果需要终止的是当前进程,则判断如果是当前线程,调用 PspTerminateThreadByPointer 结束,不等待返回。
  3. 最后再清理句柄表和解引用对象。

💡 退出进程并不代表进程对象会被删除。只有当进程的最后一个句柄被关闭时,此进程对象才会被移除。

创建一个继承需要考虑非常多的点,从用户模式到内核模式的转换,内核对象的构建再回到用户模式与子系统的通信。这个部分牵涉到的内容非常多,也非常值得详细研究。先了解大框架,再来细化其中的每个点。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK