一、进程创建过程
所有进程都通过 PspCreateProcess 函数创建,包括 System 进程。它被三个函数调用,分别是NtCreateProcessEx、PsCreateSystemProcess 和 PspInitPhase0 。
NtCreateProcessEx 是 CreateProcess 的内核服务;
PspInitPhase0 函数是系统初始化早期调用的,它创建了 System 进程,System 进程的句柄保存在全局变量 PspInitialSystemProcessHandle 中,EPROCESS 保存在 PsInitialSystemProcess 中;
PsCreateSystemProcess 是用来创建系统进程的,它创建的进程都是 PsInitialSystemProcess 的子进程。
PspCreateProcess 的大致流程在136-140页分析了。这里简单介绍一下,完整代码放在文末。
- 创建进程对象
- 初始化进程对象
- 初始化内存区对象 SectionObject
- 初始化调试端口
- 初始化异常端口
- 创建进程地址空间
- 初始化进程基本优先级,CPU亲和性,页目录基址,超空间页帧号
- 初始化进程安全属性(从父进程复制令牌)
- 设置优先级类别
- 初始化句柄表
- 初始化进程地址空间
- 创建进程ID
- 审计此次进程创建行为
- 作业相关操作
- 创建或复制PEB
- 新进程对象插入 PsActiveProcessHead 链表
- 新进程对象插入当前进程句柄表
- 计算新进程的基本优先级和时限重置值
- 设置进程访问权限,当前进程句柄可访问,允许进程终止
- 设置进程创建时间
PspCreateProcess 创建了进程,此时进程中的代码还没运行起来,进程还是死的,因为此时还没有线程,接下来介绍线程的创建过程。
二、线程创建过程
NtCreateThread 和 PsCreateSystemThread 函数会调用 PspCreateThread ,分别用于创建用户线程和系统线程。
下面简单概括 PspCreateThread 创建线程的工作,可能有遗漏或错误。
- 获取当前CPU模式
- 获取进程对象
- 创建线程对象并初始化为0
- 设置父进程
- 创建CID
- 初始化线程结构的部分属性
- 初始化定时器链表
- 如果是用户线程,创建并初始化TEB,用 ThreadConatext 初始化R0 R3的入口点
- 如果是系统线程,用 StartRoutine 初始化R0入口点
- 不管是用户线程还是内核线程,都调用 KeInitThread 初始化 Header, WaitBlock, ServiceTable, APC,定时器,内核栈等属性
- 进程的活动线程计数加1
- 新线程加入进程的线程链表
- 调用 KeStartThread 初始化剩余的域,主要是和调度相关的优先级、时限、CPU亲和性等
- 如果是该进程的第一个线程,则触发该进程的创建通知
- 工作集相关的操作
- 线程对象的引用计数加2,一个针对当前的创建操作,一个针对要返回的线程句柄
- 如果 CreateSuspended 为真,指示新线程立即被挂起
- 根据指定的期望访问权限,调用 SeCreateAccessStateEx 创建一个访问状态结构
- 把新线程对象插入到当前进程的句柄表
- 设置输出参数 ThreadHandle
- 设置输出参数 ClientId
- 设置线程创建时间
- 设置线程访问权限
- 新线程加入就绪链表,等待调度;或者此时进程不在内存中,设置新线程状态为转移
- 引用计数减1
PspCreateThread 函数返回后,新线程随时可以被调度执行。
三、创建进程的全貌
进程创建后才创建线程,但是 PspCreateProcess 函数中根本没有创建线程的动作,也没有打开进程可执行映像文件的代码。在WRK中,我们看不到完整的进程创建过程,这是不行的!下面从 CreateProcess 函数调用开始,分析进程创建的全过程。
实际上,0环的东西已经分析过了,我们只需要分析3环 CreateProcessW 进0环之前干了啥就行。
CreateProcessW 本身没干啥,而是调用了 CreateProcessInternalW 函数,源码见文末。
CreateProcessInternalW 函数在 base\win32\client\process.c,总共 2582 行C代码,我看不懂,主要是不知道怎么把文件名转化成 SectionHandle 的,代码实在太多了,而且有很多看不懂的操作。
BOOL
WINAPI
CreateProcessInternalW(
HANDLE hUserToken,
LPCWSTR lpApplicationName,
LPWSTR lpCommandLine,
LPSECURITY_ATTRIBUTES lpProcessAttributes,
LPSECURITY_ATTRIBUTES lpThreadAttributes,
BOOL bInheritHandles,
DWORD dwCreationFlags,
LPVOID lpEnvironment,
LPCWSTR lpCurrentDirectory,
LPSTARTUPINFOW lpStartupInfo,
LPPROCESS_INFORMATION lpProcessInformation,
PHANDLE hRestrictedUserToken
)
CreateProcessInternalW 打开指定的可执行文件,并创建一个内存区对象,注意,内存区对象并没有被映射到内存中(由于目标进程尚未创建,不可能完成内存映射),但它确实是打开了。
接下来就是调用 ntdll 的 NtCreateProcessEx,通过系统调用,CPU模式变成内核模式,进入0环 KiSystemService / KiFastCallEntry 分发函数,然后调用执行体的 NtCreateProcessEx 函数。
接下来的工作我们先前已经分析过了,这里再次过一遍。NtCreateProcessEx 函数执行前面介绍的进程创建逻辑,包括创建并初始化 EPROCESS 对象,创建初始的进程地址空间,创建和初始化句柄表,设置 EPROCESS KPROCESS 的各种属性,如进程优先级,安全属性,创建时间等。到这里,执行体层的进程对象已经建立,进程地址空间已经初始化,PEB也已初始化。
接下来是创建线程,首先需要构造一个栈,和一个上下文环境。栈的大小通过映像文件获得,创建线程通过 ntdll 的 NtCreateThread 调用执行体的 NtCreateThread 完成,具体工作就是先前介绍过的,包括创建并初始化 ETHREAD,生成线程ID,建立TEB和设置线程安全性。
进程的第一个线程启动函数是 kernel32 的 BaseProcessStart ,这里创建的线程并不会立即执行,而是要等进程完全初始化后才执行。
到此为止,从内核角度来看,进程对象和第一个线程对象已经建立起来了,但是对子系统而言,进程创建才刚刚开始。kernel32 给windows子系统发送一个消息,消息中包括进程和线程的句柄、进程创建者的ID等必要信息。windows子系统 csrss.exe 接收到此消息,执行以下操作:
- 保留一份句柄
- 设定新进程的优先级类别
- 在子系统中分配一个内部进程块
- 设置新进程的异常端口,从而子系统可以接收到该进程中发生的异常
- 对于被调试进程,设置它的调试端口,从而子系统可以接收到该进程的调试事件
- 分配并初始化一个内部线程块,并插入到进程的线程列表中
- 窗口会话中的进程计数加1
- 设置进程的停机级别位默认级别
- 将新进程插入到子系统的进程列表中
- 分配并初始化一块内存供子系统的内核模式部分使用(W32PROCESS结构)
- 显示应用程序启动光标
到此为止,进程环境已经建好,其线程将要使用的资源也分配好了,windows子系统已经知道并登记了此进程和线程。所以,初始线程被恢复执行,余下部分的初始化工作是在初始线程在新进程环境中完成的。在内核中,新线程启动的例程是 KiThreadStartup 函数,这是在 PspCreateThread 函数中调用 KeInitThread 时,KeInitThread 函数又调用 KiInitializeContextThread 函数来设置的。
cPublicProc _KiThreadStartup ,1
xor ebx,ebx ; clear registers
xor esi,esi ;
xor edi,edi ;
xor ebp,ebp ;
LowerIrql APC_LEVEL ; KeLowerIrql(APC_LEVEL)
pop eax ; (eax)->SystemRoutine
call eax ; SystemRoutine(StartRoutine, StartContext)
pop ecx ; (ecx) = UserContextFlag
or ecx, ecx
jz short kits10 ; No user context, go bugcheck
mov ebp,esp ; (bp) -> TrapFrame holding UserContext
jmp _KiServiceExit2
kits10: stdCall _KeBugCheck, <NO_USER_MODE_CONTEXT>
stdENDP _KiThreadStartup
KiThreadStartup 函数首先将 IRQL 降低到 APC_LEVEL,然后调用系统初始的线程函数 PspUserThreadStartup (PspCreateThread 函数在调用KeInitThread 时指定的,如果是创建系统线程,这里就是 PspSystemThreadStartup 函数)。线程启动函数被作为一个参数传递给 PspUserThreadStartup ,此处应是 kernel32 的 BaseProcessStart。
PspUserThreadStartup 函数设置异步函数调用APC机制,基本流程如下:
-
获得当前线程和进程对象。
-
是否由于创建过程中出错而需要终止本线程。
-
如果需要,通知调试器。
-
如果这是进程中的第一个线程,则判断系统是否支持应用程序预取的特性,如果
是,则通知缓存管理器预取可执行映像文件中的页面(见2 106 行的CcPfBeginAppLaunch
调用)。所谓应用程序预取,是指将该进程上一次启动的前10 s 内引用到的页面直接读
入到内存中。 -
然后,PspUserThreadStartup 把一个用户模式APC 插入到线程的用户APC 队列中,
此APC 例程是在全局变量PspSystemDll 中指定的,指向ntdll.dll 的LdrInitializeThunk 函数。 -
接下来填充系统范围的一个Cookie 值。
PspUserThreadStartup 返回后,KiThreadStartup 函数返回到用户模式,此时,PspUserThreadStartup 插入的APC 被交付,于是 LdrInitializeThunk 函数被调用,这是映像加载器(image loader)的初始化函数,完成加载器,堆管理器等初始化工作,然后加载必要的dll,并调用它们的入口函数。最后,当 LdrInitializeThunk 返回到用户模式 APC 分发器时,该线程开始在用户模式下执行,调用应用程序指定的线程启动函数,此启动函数的地址已经在APC交付时被压到用户栈中。
至此,进程创建完毕,开始执行用户空间中的代码。
本文涉及的函数源码
PspCreateProcess
NTSTATUS
PspCreateProcess(
OUT PHANDLE ProcessHandle,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,
IN HANDLE ParentProcess OPTIONAL,
IN ULONG Flags,
IN HANDLE SectionHandle OPTIONAL,
IN HANDLE DebugPort OPTIONAL,
IN HANDLE ExceptionPort OPTIONAL,
IN ULONG JobMemberLevel
)
/*++
Routine Description:
This routine creates and initializes a process object. It implements the
foundation for NtCreateProcess and for system initialization process
creation.
这个函数创建并初始化一个进程对象。NtCreateProcess 会调用它;系统进程初始化也会调用它。
Arguments:
ProcessHandle - Returns the handle for the new process.
输出参数,返回新进程句柄
DesiredAccess - Supplies the desired access modes to the new process.
期望对新进程的访问权限
ObjectAttributes - Supplies the object attributes of the new process.
新进程的对象属性
ParentProcess - Supplies a handle to the process' parent process. If this
parameter is not specified, then the process has no parent
and is created using the system address space.
指定父进程句柄。如果未指定,说明没有父进程,那就是用系统地址空间创建进程。
Flags - Process creation flags
进程创建标志
SectionHandle - Supplies a handle to a section object to be used to create
the process' address space. If this parameter is not
specified, then the address space is simply a clone of the
parent process' address space.
提供一个内存区对象句柄用来创建进程地址空间。如果此参数未指定,则简单地复制父进程
的地址空间。
DebugPort - Supplies a handle to a port object that will be used as the
process' debug port.
调试端口句柄
ExceptionPort - Supplies a handle to a port object that will be used as the
process' exception port.
异常端口句柄
JobMemberLevel - Level for a create process in a jobset
工作集等级
--*/
{
NTSTATUS Status;
PEPROCESS Process;
PEPROCESS CurrentProcess;
PEPROCESS Parent;
PETHREAD CurrentThread;
KAFFINITY Affinity;
KPRIORITY BasePriority;
PVOID SectionObject;
PVOID ExceptionPortObject;
PVOID DebugPortObject;
ULONG WorkingSetMinimum, WorkingSetMaximum;
HANDLE LocalProcessHandle;
KPROCESSOR_MODE PreviousMode;
INITIAL_PEB InitialPeb;
BOOLEAN CreatePeb;
ULONG_PTR DirectoryTableBase[2];
BOOLEAN AccessCheck;
BOOLEAN MemoryAllocated;
PSECURITY_DESCRIPTOR SecurityDescriptor;
SECURITY_SUBJECT_CONTEXT SubjectContext;
NTSTATUS accesst;
NTSTATUS SavedStatus;
ULONG ImageFileNameSize;
HANDLE_TABLE_ENTRY CidEntry;
PEJOB Job;
PPEB Peb;
AUX_ACCESS_DATA AuxData;
PACCESS_STATE AccessState;
ACCESS_STATE LocalAccessState;
BOOLEAN UseLargePages;
SCHAR QuantumReset;
#if defined(_WIN64)
INITIAL_PEB32 InitialPeb32;
#endif
PAGED_CODE();
// 获取当前线程、CPU模式、当前进程
CurrentThread = PsGetCurrentThread ();
PreviousMode = KeGetPreviousModeByThread(&CurrentThread->Tcb);
CurrentProcess = PsGetCurrentProcessByThread (CurrentThread);
CreatePeb = FALSE;
UseLargePages = FALSE;
DirectoryTableBase[0] = 0;
DirectoryTableBase[1] = 0;
Peb = NULL;
//
// Reject bogus create parameters for future expansion
// 如果 Flags 里的保留位被置1,就是非法参数
if (Flags&~PROCESS_CREATE_FLAGS_LEGAL_MASK) {
return STATUS_INVALID_PARAMETER;
}
//
// Parent
// 检查有无指定父进程
//
if (ARGUMENT_PRESENT (ParentProcess)) {
// 如果指定了父进程句柄,就获取它的EPROCESS
Status = ObReferenceObjectByHandle (ParentProcess,
PROCESS_CREATE_PROCESS,
PsProcessType,
PreviousMode,
&Parent,
NULL);
if (!NT_SUCCESS (Status)) {
return Status;
}
// 工作集相关
if (JobMemberLevel != 0 && Parent->Job == NULL) {
ObDereferenceObject (Parent);
return STATUS_INVALID_PARAMETER;
}
// 继承父进程的CPU亲和性
Affinity = Parent->Pcb.Affinity;
// 用全局变量初始化工作集最大最小值
WorkingSetMinimum = PsMinimumWorkingSet;
WorkingSetMaximum = PsMaximumWorkingSet;
} else {
// 没有父进程
Parent = NULL;
Affinity = KeActiveProcessors;
WorkingSetMinimum = PsMinimumWorkingSet;
WorkingSetMaximum = PsMaximumWorkingSet;
}
//
// Create the process object
// 创建进程对象 EPROCESS
//
Status = ObCreateObject (PreviousMode,
PsProcessType,
ObjectAttributes,
PreviousMode,
NULL,
sizeof (EPROCESS),
0,
0,
&Process);
if (!NT_SUCCESS (Status)) {
goto exit_and_deref_parent;
}
//
// The process object is created set to NULL. Errors
// That occur after this step cause the process delete
// routine to be entered.
// EPROCESS 数据清零。此后发生的错误会导致调用进程删除函数
//
// Teardown actions that occur in the process delete routine
// do not need to be performed inline.
//
RtlZeroMemory (Process, sizeof(EPROCESS));
ExInitializeRundownProtection (&Process->RundownProtect);
PspInitializeProcessLock (Process);
InitializeListHead (&Process->ThreadListHead);
#if defined(_WIN64)
if (Flags & PROCESS_CREATE_FLAGS_OVERRIDE_ADDRESS_SPACE) {
PS_SET_BITS (&Process->Flags, PS_PROCESS_FLAGS_OVERRIDE_ADDRESS_SPACE);
}
#endif
PspInheritQuota (Process, Parent);
ObInheritDeviceMap (Process, Parent);
if (Parent != NULL) {
Process->DefaultHardErrorProcessing = Parent->DefaultHardErrorProcessing;
Process->InheritedFromUniqueProcessId = Parent->UniqueProcessId;
} else {
Process->DefaultHardErrorProcessing = PROCESS_HARDERROR_DEFAULT;
Process->InheritedFromUniqueProcessId = NULL;
}
//
// Section
//
if (ARGUMENT_PRESENT (SectionHandle)) {
// 如果指定了内存区对象句柄参数 SectionHandle,就获取内存区对象
Status = ObReferenceObjectByHandle (SectionHandle,
SECTION_MAP_EXECUTE,
MmSectionObjectType,
PreviousMode,
&SectionObject,
NULL);
if (!NT_SUCCESS (Status)) {
goto exit_and_deref;
}
} else {
// SectionHandle 参数为NULL,要看父进程是不是 System 进程
SectionObject = NULL;
if (Parent != PsInitialSystemProcess) {
// 如果父进程不是 System 进程 ,那么内存区对象继承自父进程
//
// Fetch the section pointer from the parent process
// as we will be cloning. Since the section pointer
// is removed at last thread exit we need to protect against
// process exit here to be safe.
//
if (ExAcquireRundownProtection (&Parent->RundownProtect)) {
SectionObject = Parent->SectionObject;
if (SectionObject != NULL) {
ObReferenceObject (SectionObject);
}
ExReleaseRundownProtection (&Parent->RundownProtect);
}
if (SectionObject == NULL) {
Status = STATUS_PROCESS_IS_TERMINATING;
goto exit_and_deref;
}
}
// 如果父进程是 System 进程,那么 SectionObject 就是 NULL
}
// 内存区对象 SectionObject 初始化完成(如果是NULL则表示父进程是System进程)
Process->SectionObject = SectionObject;
//
// DebugPort
// 调试端口初始化
if (ARGUMENT_PRESENT (DebugPort)) {
Status = ObReferenceObjectByHandle (DebugPort,
DEBUG_PROCESS_ASSIGN,
DbgkDebugObjectType,
PreviousMode,
&DebugPortObject,
NULL);
if (!NT_SUCCESS (Status)) {
goto exit_and_deref;
}
Process->DebugPort = DebugPortObject;
if (Flags&PROCESS_CREATE_FLAGS_NO_DEBUG_INHERIT) {
PS_SET_BITS (&Process->Flags, PS_PROCESS_FLAGS_NO_DEBUG_INHERIT);
}
} else {
if (Parent != NULL) {
DbgkCopyProcessDebugPort (Process, Parent);
}
}
//
// ExceptionPort
// 异常端口初始化
if (ARGUMENT_PRESENT (Exception