进程的创建和终止

date
Sep 13, 2023
slug
create-and-terminal-process
status
Published
tags
Windows Internals
Process
summary
这篇内容主要讲述了如何创建一个进程,分为用户态和内核态两部分。在用户态,通过一系列方法如CreateProcess、CreateProcessAsUser等,确定进程的参数和标志。然后在内核态,通过NtCreateUserProcess创建用户模式的进程。进程创建过程包括参数转换、打开要执行的镜像文件、创建进程对象等步骤。其中还提到了镜像劫持(IFEO)的原理,可以通过注册表实现打开一个进程时实际运行另一个进程。总体来说,文章详细介绍了创建进程的流程和相关细节。
type
Post
notion image

创建一个进程

总述

notion image
如图,创建一个进程主要分为两部分,用户态部分和内核部分。
既然我们想看看一个进程是怎么被创建的,那我们就用 WinDbg 来看看从用户态到内核态都调用了什么:
第一步:我们先看看 nt 下有哪些方法跟创建进程相关的
第二步:我们选择 nt!MmCreateProcessAddressSpace 打上断点(不要问我为啥选这个,实在不会选,就直接 bm nt!*CreateProcess*)
 
如上 Windbg 输出的结果所示,正是描述了从用户态的 CreateProcess → 内核态的 NtCreateUserProcess. 其他链路,比如 CreateProcessAsTokenW 我们也可以验证下,这里就不做赘述。
用户态部分,包含一些我们常用到的方法:CreateProcess, CreateProcessAsUser, CreateProcessWithLogonW, CreateProcessAsTokenW.
而内核部分,则都是通过 NT 下的 NtCreateUserProcess 来进行创建。
💡
NtCreateUserProcess 只负责创建用户模式的进程,而内核模式的进程则会通过 NtCreateProcessEx 来创建。两者虽然是不同的调用,但都会调用相同的 PspAllocateProcess。

创建进程流程

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

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

这一步骤主要是将从用户态参数到内核态参数的一个转换,同时添加必要的标识。
其中主要包含以下部分:
  1. 优先级的确定
  1. Native 属性和 Win32 属性的映射
  1. 对现代应用(modern application)的特定标识(PROC_THREAD_ATTRIBUTE_PACKAGE_FULL_NAME),方便后续特殊处理
  1. Debug 和 Error 的预设
  1. 确定特定的桌面环境(指进程需要创建到哪个特定的虚拟桌面)
Windows 的虚拟桌面其实只有一个 Desktop 对象,使用 desktop.exe 可以真正的创建多个虚拟桌面。
  1. 将参数做转换。(比如 c:\temp\a.exe 可能转换成 \device\harddiskvolume1\temp\a.exe)
做完这些工作,创建进程的用户态的初始化基本就结束了。接下来就会尝试调用内核态的 NtCreateUserProcess 来创建进程。

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

notion image
这个部分已经切换到内核模式执行了,主要目的就是确定要怎么打开镜像。
主要包含以下部分:
  1. 如上图,根据要打开的镜像文件确认需要真正执行的进程。(比如,当文件是一个 .cmd 文件时,真正需要执行的是 cmd.exe 这个进程,那就需要重新回到步骤一 CreateProcessInternalW )
  1. 如果进程是现代应用,则需要确定他的证书,确保它是可以被运行的。(比如非商店应用在Windows 设定不运行旁加载的情况下是不能被运行的)
  1. 如果进程是 Trustlet,还需要添加特定的标识
  1. 会尝试去打开 Windows exe 文件。首先创建 section object,然后判断其是否可以被打开。
  1. 然后会在 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 文件来说,有以下这些行为:
notion image
至此,Windows 就已经可以打开一个可执行文件并且创建了部分对象,并映射到新的进程的地址空间了。

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

这个部分主要是通过 PspAllocateProcess 来创建 Windows 执行体对象(也就是内核中描述进程的对象)。主要分为以下几个部分:
  1. 设置 EPROCESS 对象。初始化或者从父进程继承属性,同时会根据 IFEO 的各种 key 来确定对应的值(比如:UseLargePages、PerfOptions、IoPriority、PagePriority、CpuPriorityClass、WorkingSetLimitInKB)
  1. 创建初始进程地址空间。
  1. 创建内核进程结构,也就是是初始化 KPROCESS。
  1. 完成进程地址空间的设置。(这块需要有一些内存管理上的知识,有点迷糊,后面再来补上
  1. 配置 PEB。
  1. 完成执行体进程对象的配置。
到此,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。
  1. 如果软件策略有约束,则会为此进程创建一个约束的 token,并将其存储到 PEB 中
  1. CreateProcessInternalW 会调用一些内部方法来获取系统的 SxS 信息
  1. 根据收集到的要发送到 Csrss 的信息构造到 Windows 子系统的消息。
  1. 在收到消息后,Windows 子系统将执行以下步骤:
    1. CsrCreateProcess 会为NT进程和线程复制句柄。进程和线程的使用计数(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. 安装异常链。
  1. 将 IRQL 降低至 PASSIVE_LEVEL(也就是0)
  1. 禁用运行时交换主进程 token 的能力
  1. 根据内核模式下的数据结构(KTHREAD) 设置 TEB 中的 local ID 和线程的首选处理器
  1. 调用 DbgkCreateThread 来检查新的进程是否向镜像发送消息。(用于 load dll)
  1. 然后继续做一些列的检查。
  1. 接下来就会切换到用户模式下,回到 RtlUserThreadStart 中。
到这里,进程的创建就结束了,将执行 Image 中的入口方法,进到进程的上下文中去。

结束一个进程

结束进程主要分为两种方式:主动结束(ExitProcess)和被动停止(TerminateProcess)。
ExitProcess 和 TerminateProcess 调用的执行体中的 NtTerminalProcess。它主要执行以下逻辑:
  1. 轮询进程中所有线程,如果不是当前线程,调用 PspTerminateThreadByPointer 结束(需要等待返回)。
  1. 如果需要终止的是当前进程,则判断如果是当前线程,调用 PspTerminateThreadByPointer 结束,不等待返回。
  1. 最后再清理句柄表和解引用对象。
💡
退出进程并不代表进程对象会被删除。只有当进程的最后一个句柄被关闭时,此进程对象才会被移除。

总结

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

© Frend Guo 2022 - 2024