本文由2部分组成:第一部分解析 CreateProcess() 的进程创建流程,第二部分翻译

MSDN 站点上关于 CreateProcess() 的各种参数的解释与用法,最后给出编程实例


CreateProcess*() 系列函数是 32位 Windows 7 平台下用于创建进程的函数。Kernel32.dll 中导出的 CreateProcess*() 函数有:

CreateProcessA

CreateProcessAsUserW

CreateProcessInternalA

CreateProcessInternalW

CreateProcessW


advapi32.dll 中导出的 CreateProcess*() 函数有:

CreateProcessAsUserA

CreateProcessAsUserW

CreateProcessWithLogonW

CreateProcessWithTokenW


下面以应用程序员最长使用的 CreateProcessA/W 为例,说明进程的创建流程。

CreateProcessA/W 最终会调用 ntdll.dll 中导出的 NtCreateUserProcess(),在后者的内部通过 SYSENTER / INT 2Eh 指令切换到内核模式,调用相同名称的函数;为了便于区分,后文将用户模式下的 NtCreateUserProcess() 标记为

ntdll!NtCreateUserProcess();内核模式下的标记为  nt!NtCreateUserProcess()

关于从用户切换到内核模式的细节,请参考这篇博文:

http://shayi1983.blog.51cto.com/4681835/1710861

另外,由于用户模式下的流程比较简单,只是验证用户传入的参数是否合法,然后将某些可选的参数标志转换为对应的“原生”形式,因此本文将重点分析 nt!NtCreateUserProcess() 内部实际的进程创建流程。

鉴于 CreateProcess*() 的行为,下面的讨论中,不加以区分“主调进程/父进程”,以及“新进程/子进程”。


首先在命令行下执行 livekd.exe ,添加 -w 参数来调用 WinDbg.exe 进行实时内核调试。

1。在 nt!NtCreateUserProcess+0x199 处看到了对 nt!PspBuildCreateProcessContext() 的调用,这是一个比较重要的阶段,用于构建创建新进程所需的上下文环境:

wKioL1ZRUdiyFD0GAAAOMrSyp9Q506.png


2。在 nt!NtCreateUserProcess+0x2d1 处,逆序将  nt!ObReferenceObjectByHandle() 的6个参数压栈,其中最后一个压栈的为该函数的第一个参数,即主调进程的句柄,位于 [ebp-98h] 处;第二个压栈的为该函数的第五个参数,即存储父进程的 EPROCESS 结构(Windows执行体进程对象)地址的局部变量,位于 [ebp-67C] 处:

wKioL1ZRb5TAS-2DAABuBtsFJYw155.png

使用 IDAPRO 打开 ntkrnlpa.exe,在左侧的函数列表窗口中定位到 NtCreateUserProcess,然后双击,这样在右侧的反汇编窗口将导航到该函数的入口地址,可以看到关于该函数的栈变量统计信息,其中,IDAPRO 识别出此函数是基于 EBP 指针寻址局部变量和参数的(Attributes: bp-based frame);以及[ebp-67Ch] 处的栈变量为父进程的 EPROCESS 结构的指针(Object= dword ptr -67Ch), [ebp-98h] 处的栈变量为父进程句柄(Handle= dword ptr -98h):

wKiom1ZR3g6BtWxlAABD1_QyCsc778.png

IDAPRO 对 NtCreateUserProcess() 调用 nt!ObReferenceObjectByHandle() 机器指令序列的返回汇编输出,与 WinDbg 中的完全一致,并且多了对压栈参数的智能注释:

wKiom1ZR4DrRE9z0AAApCXDF4JU022.png


MSDN原型验证:

wKioL1ZR4yjglG5iAAB-BbvdmMY601.png

下面的 MSDN 文档讲的很清楚:当 ObReferenceObjectByHandle() 的第三个参数为 *PsProcessType 时(上一张 IDAPRO 截图识别出这个传入的实参),第五个参数就是一个指向执行体进程结构或内核进程结构的指针:

wKioL1ZR5dKgtx5NAACpSPfQlT4541.png

综上所述,调用 nt!ObReferenceObjectByHandle() 的目的在于方便nt!NtCreateUserProcess() 的后续代码在创建子进程的 EPROCESS 结构时,将某些结构成员的值初始化为父进程的相应结构成员。


3。在 nt!NtCreateUserProcess+0x2d1 处,调用 nt!ZwOpenFile() ,打开要在新进程中执行的二进制映像文件(映像的名称和路径由主调进程指定);在 nt!ZwOpenFile() 内部,请求系统服务调度器 nt!KiSystemService 执行 nt!NtOpenFile 来实际打开映像文件:

wKioL1ZRdgKSmreMAAAN5_dhhic183.png

关于系统服务调度器的细节,同样请参

考 http://shayi1983.blog.51cto.com/4681835/1710861


4。nt!ZwOpenFile() 返回后,在 nt!NtCreateUserProcess+0x388 处,调用 nt!ZwCreateSection(),创建一个内存节/区对象,该对象与 nt!ZwOpenFile() 打开的可执行文件句柄关联,但尚未映射到新进程的地址空间中,后面在初始化新进程的地址空间阶段,通过调用 nt!MiMapViewOfSection() 将此内存节/区对象映射到新进程的地址空间:

wKioL1ZRdzjzKKmZAAAWDAM3enM529.png

类似地,在 nt!ZwCreateSection() 内部,实际的对象创建工作由nt!NtCreateSection() 完成。

另外,分析调用 nt!ZwOpenFile() 时传入的第一个参数(最后一个压栈的局部变量,用于保存打开的文件句柄),与调用 nt!ZwCreateSection() 时传入的最后一个参数(第一个压栈的局部变量,用于指定创建的内存节/区对象要和那个打开的文件关联),2者都是 [ebp-84h],可以证实创建的内存节/区对象与一个打开的文件关联:

wKioL1ZReS6RPN-JAABR2Zym72Q588.png


MSDN原型验证:

wKioL1ZReo-hQh8XAABzmzseI4k124.png



wKiom1ZRfV7BVUH5AABtSg-Xcow734.png


MSDN原型验证:

wKioL1ZRfsGAu4VJAADG6hgDAHQ523.png



5。nt!NtCreateUserProcess 接下来的一个重要阶段位于其偏移 +0x515 处,也就是对 nt!PspAllocateProcess() 的调用,后者负责创建并初始化子进程的“Windows执行体进程对象”(即 EPROCESS 结构),创建并初始化子进程的地址空间,创建并初始化子进程的“Windows内核进程对象”(即  KPROCESS 结构)并链接到 EPROCESS 结构,创建并初始化子进程的 PEB(进程环境块)并链接到 EPROCESS 结构。。。等等:

wKiom1ZRzdmBv036AAALEhahizM566.png

由此可知,nt!PspAllocateProcess() 内部的流程比较复杂。下面转入其内部探索,我们将在分析完 nt!PspAllocateProcess() 的逻辑后跟随该函数返回 nt!NtCreateUserProcess() 继续分析。

1。在 nt!PspAllocateProcess+0xc4 处,发现了连续压栈的9个参数,紧接着就是对 nt!ObCreateObject() 的调用,而后者将实际创建新进程的 EPROCESS 结构:

wKioL1ZSnx3wQ_TDAAA9N2O5vyA093.png

上图中,第4个 push    edi 指令,就是 nt!ObCreateObject 的第6个参数,它是 EPROCESS 结构的字节大小。即复制到 edi 寄存器中的值 0x2d8。在 IDA PRO 中能够快速识别出压栈参数个数以及重要参数的含义,注意下图中复制到 ECX 寄存器中的第一个压栈参数,它是 nt!PspAllocateProcess() 内的局部变量 [ebp+var_60]

位于 ebp-60h 处,它将作为 nt!ObCreateObject() 的最后一个参数传入,也就是指向创建的 EPROCESS 结构 的指针:

wKioL1ZSplnSOv9_AAAs041NLXA593.png

概括起来讲, nt!ObCreateObject() 是执行体组件之一“对象管理器”实现的内核对象创建例程(从其函数名的“Ob”前缀可以猜测出),通过将该函数的第二个参数指定不同的值,可以创建不同类型的内核对象(包括进程,线程,文件,信号量,事件,访问令牌。。等对象)。而在此处,就是通过传入 PsProcessType 来请求 nt!ObCreateObject() 创建执行体进程对象,然后 nt!ObCreateObject() 会将创建好的子进程 EPROCESS 结构地址保存在 [ebp+var_60] 处。尽管 nt!ObCreateObject() 没有文档化,MSDN 中也没有函数原型记载,通过分析我们得知其原型类似如下:

ObCreateObject ( 
    IN KPROCESSOR_MODE ObjectAttributesAccessMode OPTIONAL,
    IN POBJECT_TYPE Type,
    IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,
    IN KPROCESSOR_MODE AccessMode,
    IN OUT PVOID ParseContext OPTIONAL,
    IN ULONG ObjectSize,
    IN ULONG PagedPoolCharge OPTIONAL,
    IN ULONG NonPagedPoolCharge OPTIONAL,
    OUT PVOID * Object
)

需要指出,ObCreateObject() 内部通过调用 ObpAllocateObject() 为对象申请系统内存空间并实际创建所请求的对象,限于篇幅,这里不分析后者的逻辑,有兴趣的可以自行跟踪进入该函数研究。另一个验证所请求创建的对象是否为执行体进程对象的方法是,通过 dt nt!_eprocess 命令查看 EPROCESS 结构,定位到其最后一个成员,它位于该结构起始偏移 0x2d0 处,大小为8字节,因此整个EPROCESS 结构大小为 0x2d0+8=0x2d8字节,与上面几张图中,第4个压栈的参数(ObjectSize,对象大小)值相符:

wKioL1ZSshihWNm5AAAb4tqG86I912.png


2。在 nt!PspAllocateProcess+0x1ac 处,调用 nt!MmGetDefaultPagePriority(),设置子进程的默认页面优先级。跟踪进入该函数,发现其非常有趣,因为只有4字节的机器码:返回立即数5,这意味着默认的页面优先级为5:

wKioL1ZSuY7wYDQWAAAtHzFzwO4837.png


wKioL1ZSuIvQ_OFBAAAYTogvmOU272.png

通过其名称前缀能够推测出,该函数是执行体组件之一“内存管理器”中的一个例程。

3。在 nt!PspAllocateProcess+0x24d 处调用 nt!PspUpdateCreateInfo() ,由于后者是一个比较重要的函数,涉及对新进程 EPROCESS 结构的某些成员执行初始化的工作,这里暂时跟进该函数分析:

wKioL1ZTNtHBBCojAAAMwUWEjqE633.png

在 nt!PspUpdateCreateInfo+0x7a 处,调用了 nt!ObDuplicateObject() 例程:

wKiom1ZTN-iCWsHFAAAM0Ua_zsw605.pngnt!PspUpdateCreateInfo() 内部包括对  nt!ObDuplicateObject() 的调用,从后者的原型声明可以看出,它是一个复制进程句柄的例程:

NTSTATUS
ObDuplicateObject (
IN PEPROCESS SourceProcess,
IN HANDLE SourceHandle,
IN PEPROCESS TargetProcess OPTIONAL,
OUT PHANDLE TargetHandle OPTIONAL,
IN ACCESS_MASK DesiredAccess,
IN ULONG HandleAttributes,
IN ULONG Options,
IN KPROCESSOR_MODE PreviousMode
)

其中前面4个参数分别为“源”进程的 EPROCESS 结构指针,“源”句柄,“目标”进程的 EPROCESS 结构指针,“目标”句柄。实际上,

nt!ObDuplicateObject() 的主要任务在于初始化新进程的句柄表,其内部会检查新进程是否要继承父进程的所有句柄。如果为用户模式下的 CreateProcess() 第5个参数 bInheritHandles 传入 TRUE ,则内核例程 nt!ObDuplicateObject()  会调用 nt!ObReferenceProcessHandleTable(),来获取父进程的句柄表,然后将其中所有“可继承的”句柄复制到新进程的句柄表中(CreateProcess() API 的所有参数详解请参考本文第二部分):

wKiom1ZTSgrzHUdWAAAOGxE3Ha4419.png

初始化的最后阶段就是将新进程 EPROCESS 结构中的一个成员指向新进程句柄表的地址,该成员的位置如下:

wKioL1ZTTO3A4ow2AAAP3rgYbfE979.png

可以看到是一个指向 _HANDLE_TABLE 的32位指针。下面引用自《深入解析Windows操作系统第6版上册》一书中,进程 EPROCESS 结构中的重要成员示意图,以及句柄表指针的位置:

wKioL1ZTUP7i1wa_AAGvmJ7nRbE638.png



4。从 nt!PspUpdateCreateInfo() 返回后,在 nt!PspAllocateProcess+0x31a 处,调用 nt!RtlpOpenImageFileOptionsKey(),后者查询注册表键:

HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Image File Execution Options

这个母键下面,是否有一个子键,其名称为新进程的可执行映像文件名。如果有的话,继续读取这个子键中的各个键值,这些键值中配置的数据将作为新进程的映像文件执行选项,用于后面的进程地址空间初始化阶段,以及将可执行映像映射(通过与其关联的内存区/节对象)到新进程的地址空间阶段。举个例子,在上述注册表母键下,我们观察到几个子键,其中一个子键代表 IE 浏览器进程的可执行文件映像,查看其右侧的三个键值及其数据:


wKiom1ZWrCOxVan2AAFVI6AcOZ0795.png


换言之,主调进程通过 CreateProcess() 创建 IE 浏览器子进程时,根据上面对应的映像文件执行选项信息,将执行启用异常链验证

(DisableExceptionChainValidation 键值的数据为 0),禁止用户模式回调过滤器等操作。这意味着我们可以在 Image File Execution Options 母键下面添加想要通过编程方式调用 CreateProcess() 执行的程序名作为子键,然后设置各种键值来指定该程序的运行时行为。另外提一下,ntdll.dll 中存在一个叫做 LdrOpenImageFileOptionsKey() 的导出函数,它执行与上述相同的操作;从2者名称的相似性来推测,ntdll!LdrOpenImageFileOptionsKey() 在内部应该会调用 nt!RtlpOpenImageFileOptionsKey() 进行实际的操作,各位可以自行调试验证。


5。在 nt!PspAllocateProcess+0x3d9 处,调用 nt!RtlAcquirePrivilege(),后者

尝试获取创建新进程所需的所有特权,在该函数内部会调用 

nt!RtlpOpenThreadToken() 或者 nt!ZwOpenProcessTokenEx() 打开父进程的“主”访问令牌或其线程的“模拟”令牌,然后,调用 nt!ZwAdjustPrivilegesToken() 修改(或提升)令牌的访问权限。如此一来,

nt!RtlAcquirePrivilege() 返回时就带着提权后的父进程令牌,后续创建新进程的令牌时,可直接继承该令牌;相反,CreateProcessAsUser() 将允许程序员为要创建的新进程指定一个与其主调进程完全不同的访问令牌。限于篇幅,这里不详细分析nt!RtlAcquirePrivilege() 的内部逻辑,有条件的童鞋可以自行利用 IDA PRO 的函数内部执行流程图生成功能,来对该函数进行完整的分析。另外,从前面的进程 EPROCESS 结构示意图中可以看到,指向进程的主访问令牌的指针就位于进程的句柄表指针前面,因此,无论是从父进程处继承,还是使用不同的访问令牌,最后都涉及初始化该指针成员来指向实际的令牌地址。

下面给出一个实例,chrome.exe 主进程的主访问令牌:


wKiom1ZXPGnxzFJ7AABw2GpKIbk879.png