Windows进程和作业
本章将介绍Windows在处理进程和作业时涉及的数据结构和算法。首先概括介绍进程的创建过程,随后介绍进程的内部结构,接下来还会介绍进程保护机制,以及受保护进程和不受保护进程的区别。
3.1 创建进程
windowsAPI
提供了很多用于创建进程的函数,其中最简单的就是CreateProcess
该函数会尝试使用与创建者相同的访问令牌新建一个进程。如果需要不同的令牌,可以使用CreateProcessAsUser
函数,该函数可以接受一个额外的参数(第一个参数),即已经通过其他方式(如调用LogonUser
函数)获取的令牌对象句柄。
进程创建的函数都会涉及到一个通用的内部函数(处于Kernel32.dll)的CreateProcessInternal函数,该函数将启动创建用户模式windows进程的实际工作。如果一切顺利的话,它会在内核层中的NtCreateUserProcess中继续执行进程创建过程中需要在内核模式下执行的工作。
3.1.1 CreateProcess*函数的参数
用户模式下创建的进程在创建时始终会在其中包含一个线程,这个线程最终将执行可执行文件的主函数。
1 | BOOL CreateProcessA( |
其中分别代表的参数有:
- 创建的应用程序名称
- 命令行参数
- 该 结构确定子进程是否可以继承返回到新进程对象的句柄,如果//lpProcessAttributes为****NULL******,则不能继承该句柄
- 该 结构确定子进程是否可以继承返回到新线程对象的句柄,如果//lpThreadAttributes为NULL,则不能继承该句柄
- 如果此参数为TRUE,则新进程将继承调用进程中的每个可继承句柄。
- dwCreationFlags // 控制优先级类别和流程创建的标志
- 例如有:CREATE_SUSPENDED 新进程的主线程在挂起状态下创建,随后可调用Resume Thread执行该线程。
- DEBUG_PROCESS。将创建的进程宣告为调试器,并在其控制下新建进程。
- 指向新进程的环境块的指针
- 进程当前目录的完整路径
- 设置扩展属性
- 在进程创建成功后,用于提供输出的PROCESS_INFORMATION结构。该结构包含了新产生的唯一进程ID,新产生的唯一线程ID,以及新进程的一个句柄和新线程的句柄。
1 |
|
3.1.2 创建windows“现代化”进程
现代化进程也被称为UWP进程,是在win8之后新增的一种应用程序类型。UWP 是创建适用于 Windows 的客户端应用程序的众多方法之一。 UWP 应用使用 WinRT API 来提供强大的 UI 和高级异步功能,这些功能非常适用于 Internet 连接的设备。
特点:
3.1.3 创建其他类型的进程
除了传统的和现代化的进程之外,执行体还可以创建如原生进程(比如会话管理器SMSS),最小进程或Pico进程。
原生进程是在内核直接创建的,因此并没有使用CreateProcess
API,而是使用了NtCreateUserProcess
函数。
windows应用程序无法创建原生进程,因为CreateProcessInternal
函数会拒绝类型为”原生子系统”的映像。为了解决类似这样的并发问题,windows专门抽象出了NtCreateUserProcess
函数,提供了更简单的包装,用于创建用户模式的进程。
面对最小进程(System进程和Memory Compression进程)是通过NtCreateProcessEx
系统调用提供的。
面对Pico进程,可调用助手函数PspCreatePicoProcess
,该函数是不可导出的,仅供Pico提供程序通过专用接口使用。
3.2 进程内部构造
每个windows进程都可以用一种执行体进程(EPROCESS)结构来表示,除了包含与进程有关的众多属性,EPROCESS还包含并指向其他一系列相关数据结构。例如,每个进程会有一个或多个线程,每个线程都可以用一个执行体线程(ETHREAD)结构来表示。
EPROCESS及其大部分相关数据结构都位于系统地址空间中。唯一的例外是进程环境块(PEB),它位于进程(用户地址空间中)。
实验
EPROCESS结构展示:
在windbg调试器中,我们可以使用dt nt!_eprocess
命令来查看EPROCESS的结构。结构如下:
1 | +0x000 Pcb : _KPROCESS |
很多而且很烦,但是我们要关注的是该结构的第一个成员PCB,这个成员的结构其实是一种KPROCESS类型的嵌入式结构,这里存储了与调度和时间记账有关的信息,我们可以使用与EPROCESS相同的方法(dt nt!_kprocess
)查看内核进程结构的格式。
1 | +0x000 Header : _DISPATCHER_HEADER |
如果你想要详细查看一个结构体的一个字段,比如eprocess的UniqueprocessID字段,那么可以使用dt nt!_eprocess UniqueProcessID
:
查看一个结构体的字段信息。
但是这样只能查看字段信息,如果要查看一个某个进程的实例,那么我们需要先找到实例,然后再去看
找寻实例可以使用!process 0
命令。列出所有进程。
然后可以使用.process /p ffffc70f1c468040; !peb 00000000
类似命令查看详细信息。
CSR_PROCESS结构
对于每个执行了windows程序的进程,windows子系统进程(Csrss)维护了一种名为CSR_PROCESS的平行结构。Csrss进程是受保护的,因此无法将用户模式调试器连接到Csrss进程,必须使用类似于windbg的调试器。
1.列出Csrss进程
1 | lkd> !process 0 0 csrss.exe |
2.选择任何一个进程并更改调试器的上下文,指向所选进程,使用用户模式的模块可见。(/p开关可以将调试器的进程上下文更改为所提供的进程对象,/r开关可以请求加载用户模式符号。)
1 | lkd> .process /r /P ffffc70f219e1240 |
3.查看CSR_PROCESS结构
1 | lkd> dt csrss!_csr_process |
3.3 受保护进程
在Windows安全模型中,任何进程运行时所用的令牌只要包含调试权限(例如使用管理员账户运行)就可以向计算机上运行的任何其他进程请求所有访问权限。但是这种进程有可能会造成一些文件资源的泄密,比如高清蓝光电影等。因此,windows提供了受保护进程(比如Audiodg.exe)这个概念,此类进程能够与普通windows进程并存,但会对系统中其他进程(哪怕是使用管理员特权运行的进程)向此类进程请求访问权限的过程中带来很多限制。
3.6 CreateProcess的流程
创建一个windows进程,共涉及到操作系统的3个部分:Windows系统客户端库(Kernel32.dll),Windows执行体,以及Windows子系统进程(Csrss)。
下面是简略步骤:
- 验证参数,将windows子系统标志和选项转化为原生性质,解析,验证并转换属性列表为原生形式。
- 打开要在进程中执行的映像文件(.exe)
- 创建Windows执行体进程对象
- 创建初始线程
- 执行创建之后,与Windows子系统有关的进程初始化操作
- 开始执行初始线程
- 在新进程和线程的上下文中完成地址空间初始化操作(如加载所需的DLL),并开始执行程序的入口点。
第一步不做解释,从第二步开始解释:
打开要在进程中执行的映像文件:
Windows操作系统会在内核模式中,使用系统调用NtCreateUserProcess
来完成自己工作。它的主要工作是找到运行调用方指定的可执行文件对应的Windows映像,并创建区域对象,随后将其映射到新进程的地址空间。
什么叫做找到对应的Windows映像?其实意思是系统会判断它是一个什么程序,如果是DOS.bat就会使用Cmd.exe,如果是win16,就是用NTvdm.exe,windows的话就直接运行exe。
如果指定的可执行文件是Windows Exe文件,NtCreateUserProcess
函数会试图打开该文件并未其创建区域对象(内存对象),此时该内存对象并没有映射到内存中。
创建Windows执行体进程对象:
这一步主要是为了创建以EPROCESS为核心的相关数据结构,主要包括:
- 设置EPROCESS对象
- 创建初始进程地址空间
- 初始化内核进程结构(Kprocess)
- 结束进程地址空间的设置
- 设置PEB
- 完成执行体进程对象的摄制工作
创建初始线程:
现在,虽然进程对象已经建立起来,但是它没有线程,所以,它自己还不能做任何事情。接下来需要创建一个初始线程,在此之前,首先要构造一个栈以及一个可供运行的环境。初始线程的栈的大小可以通过映像文件获得,而创建线程则可以通过调用ntdll.dll 中的NtCreateThread
函数来完成。
这个阶段是通过调用NtCreateThread()
完成的,主要包括:
创建和设置目标线程的ETHREAD数据结构,并处理好与EPROCESS的关系(例如进程块中的线程计数等等)。
在目标进程的用户空间创建并设置目标线程的TEB。
将目标线程在用户空间的起始地址设置成指向Kernel32.dll中的
BaseProcessStart()
或BaseThreadStart()
,前者用于进程中的第一个线程,后者用于随后的线程。用户程序在调用NtCreateThread()
时也要提供一个用户级的起始函数(地址),BaseProcessStart()
和BaseThreadStart()
在完成初始化时会调用这个起始函数。
通知windows子系统:
每个进程在创建/退出的时候都要向Windows子系统进程Csrss.exe进程发出通知,因为它负担着对window所有进程的管理的责任,注意,这里发出通知的是CreateProcess的调用者,不是新建出来的进程,因为它还没有开始运行。
开始执行线程:
至此,进程环境已经确定,线程运行所需的资源已分配,进程中包含了线程,Windows子系统也了解了新进程。除非调用方指定了CREATE_SUSPEND标志,否则初始线程已经恢复并且开始运行,可以在新进程的上下文中执行后续的进程初始化工作。
用户空间的初始化和Dll连接
新进程的生命始于内核模式线程启动例程KiStartUserThread。该例程会将线程的IRCQ级别从延迟过程调用(DPC)级别降低到APC级别。接着LdrInitializeThunk例程会被调用,这是映像加载器(Image loader)的初始化函数。
LdrInitializeThunk 函数完成加载器、堆管理器等初始化工作,然后加载任何必要的DLL,并且调用这些DLL 的入口函数。
最后,当LdrInitializeThunk 返回到用户模式APC 分发器时,该线程开始在用户模式下执行,调用应用程序指定的线程启动函数,此启动函数的地址已经在APC 交付时被压到用户栈中。
3.7进程的终止
进程有两种终止的函数,分别是ExitProcess
,TerminateProcess
。
进程的终止可以用ExitProcess
函数,从而”优雅的退出”,当进程的第一个线程从其主函数返回时,该线程的进程启动代码会代表该进程调用ExitProcess
。什么是优雅的,这个词意味着载入该进程的DLL将有机会在接获进程即将退出的通知后,使用DLL_PROCESS_DETACH调用自己的DLLMain函数执行一些工作。
如果说ExitProcess
是优雅的退出方法,那么TerminateProcess
函数就是不优雅的方法。该函数可以从外部调用(比如我们的软件杀手,任务管理器)。TerminateProcess
要求使用PROCESS_TERMINATE
访问掩码打开一个到进程的句柄,该句柄可能别允许,也可能被拒绝。这也是某些进程(如Csrss)很难(甚至无法)终止的原因。发出请求的用户无法获得具备所需访问掩码的句柄。这种不优雅的退出方式,意味着DLL无法执行退出代码。
3.8映像加载程序
当一个进程在系统上启动时,内核将创建一个进程对象来代表它,并执行各种内核相关的初始化任务。然而,这些任务不会真正执行应用,而只是进行上下文和环境的准备工作。应用程序不像驱动(内核模式),它执行在用户模式下。因此,大部分初始化工作实际上都是在内核外完成的,且该工作是由Image Loader(Ldr)执行的。
Image Loader存在于用户模式下的系统DLL Ntdll.dll中,而不在内核库中。Ntdll.dll总是会被加载,因此,Loader段代码总是存在运行进程中,且作为新应用程序运行在用户模式下的第一部分代码(即Loader运行在实际中应用程序代码之前,且对用户和开发者是不可见的)。
它的主要作用如下:
- 为应用程序初始化用户模式状态,例如创建初始堆、设置线程本地存储(TLS)和纤程本地存储(FLS)槽。
- 解析应用程序的导入表地址(Import Address Table,IAT),查找所需的全部DLL,接着解析DLL的导出表,以确保函数确实存在。
- 在运行时或者需要时加载和卸载DLL,维护包含所有已加载模块的列表。
- 顾及hotpatching
- 处理清单文件
- 启用对API集和API重定向的支持
- 通过SwitchBack机制启用动态运行时兼容性缓解措施。
3.9作业
作业是一种可命名,可保护,可共享的内核对象,借此可以用”组”的方式控制一个或多个进程。作业对象的基本功能在于,可以将一组进程作为一个单元加以管理和操作。
作业在很多系统机制中扮演了重要角色:
- 管理现代化应用(UWP进程)
- 借助一种名为Serber Silo机制,可实现Windows对容器的支持
- 可用于实现自定义内存分区等
作业的限制:
- 活动进程的最大数量,限制了作业对象中可同时存在的进程数量。
- 作业范围内的用户模式CPU时间限制,限制了该作业中的进程。
- 作业的处理器相关性
- 作业组的相关性等