首页
社区
课程
招聘
[原创]Windows内核学习笔记之进程(上)
发表于: 2021-12-13 15:03 16659

[原创]Windows内核学习笔记之进程(上)

2021-12-13 15:03
16659

仅仅有执行程序的指令是无法让一个程序正常运行的,因为程序的运行需要用到各种各样的资源。进程就是各种资源的容器,它定义了一个地址空间作为基本的执行环境。在操作系统的管理规则中,很多资源是针对进程分配的,必须要鲜有一个进程,才能为其分配资源。

在Windows操作系统中,每个进程拥有如下资源:

一个虚拟的地址空间,一般称为进程空间。进程空间是操作系统分配给每个进程的虚拟地址空间,每个进程运行在这个受操作系统保护的虚拟空间之中,它的地址指针指向的都是这个空间中的虚拟地址,根本无法指到另一个进程空间中。也就是每个进程都在操作系统分配给它的虚拟空间中运行,它无法访问其他进程的空间,也不必担心自己的空间会被其他的进程所侵占

全局唯一的进程ID,也就是常说的PID

一个可执行映像,也就是该进程的程序文件(可执行文件)在内存中的表示

一个或多个线程

一个位于内核空间中名为EPROCESS(进程执行块)的数据结构,用以记录该进程的关键信息,包括进程的创建时间,映像文件名称等

一个位于内核空间中的对象句柄表,用以记录和索引该进程所创建/打开的内核对象。操作系统根据该表格将用户模式下的句柄翻译为指向内核对象的指针

一个用于描述内存目录表起始位置的基地址,简称页目录基地址(DirBase),当CPU切换到该进程时,会将该地址加载到页表基地址寄存器(x86之CR3, ARM之TTBR),这样当前进程的虚拟地址就会被翻译为正确的物理地址

一个位于用户空间中的进程环境块(PEB)

一个访问令牌,用于表示该进程的用户,安全组及优先级

为了保存创建的进程的各种信息,同时也为了方便系统管理创建的进程,在用户层和内核层系统都有相应的数据结构。

当成功创建一个进程的时候,Windows内核中就会创建相应的进程内核对象。内核对象保存了进程的各种信息,同时操作系统也通过创建的内核对象来对进程进行管理。

Windows内核中的执行体层负责各自与管理策略相关的功能,而内核层(或微内核)实现了操作系统的核心机制。进程在这两个层上都有对应的数据结构

KPROCESS是保存在微内核层最基本的进程数据结构,每个KPROCESS都代表了一个进程,该结构定义如下:

说明进程是否在内存中,共有六种可能状态

ProcessInMemory

ProcessOutOfMemory

ProcessInTransition

ProcessOutTransition

ProcessInSwap

ProcessOutSwap

EPROCESS是保存在执行体的数据结构,几乎包括了进程的所有关键的信息,它侧重于提供各自管理策略,同时为上层应用程序提供基本的功能接口。所以,在执行层的数据结构中,有些成员直接对应于上层应用程序中所看到的功能,该结构定义如下:

单字节,表明进程的优先级程度,分别有以下几个类别

1:空闲类别

2:普通类别

3:高优先级类别

4:实时类别

5:普通之下类别

6:普通之上类别

0:未知类别

PEB的全程是进程环境块,它包含了进程大多数的用户模式信息。与EPROCESS结构位于内核空间中不同,PEB是在内核模式建立后映射到用户空间的。因此,多个进程的PEB地址可能是同一个指,PEB的结构如下:

通过上面的三个结构分别在用户层和内核层保留了创建的进程的所有的信息,操作系统通过这些信息完成对进程的管理工作。因此,在创建进程的过程中上述的三个结构体是一定要在内存中被申请出来并初始化的。

Windows API提供了很多用于创建进程的函数。其中最简单的是CreateProcess,该函数会尝试使用与创建者相同的访问令牌新建一个进程。如果需要不同的令牌,可以使用CreateProcessAsUser函数或CreateProcessWithTokenW函数来实现。如果系统快速用特定用户凭据登录并使用所获得的令牌创建进程,此时可以使用CreateProcesssWithLoginW来实现。

创建一个进程的主要步骤如下:

验证参数,将Windows子系统标志和选项转换为原生形式,解析,验证并转换属性列表为原生形式

打开要在进程中执行的映像文件(.exe)

创建Windows执行体进程对象

创建初始线程(栈,上下文,Windows执行体线程对象)

执行创建之后需要的,与Windows子系统有关的进程初始化操作

开始执行初始线程(除非指定了CREATE_SUSPENDED标志)

在新进程和线程上下文中完成地址空间的初始化操作(如加载需要的DLL),并开始执行程序的入口点

所有文档化的进程创建函数最终都会调用Kernel32.dll中的CreateProcessInternalW。以常见的CreateProcessW函数为例,可以看到在这个函数中就是将参数压入栈中,然后调用CreateProcessInternalW,因此接下来会以此函数为主进行分析

在函数最开始,首先是将参数赋值到相应的局部变量中,同时也会一部分局部变量初始化为0。

接下来函数会查看hNewToken中是否传入了参数,如果有参数,就将对应地址中的内容赋值为0

接下来函数会对进程的优先级进行设定,而进程优先级是由参数dwCreateFlags标志来决定的。

在windows中,对于参数有如下宏定义

所以如下代码的作用就是从标志中删除CREATE_NO_WINDOWS参数

再根据这两个宏定义

可以直到接下来的代码就是判断标志中是否同时有DETACHED_PROCESS和CREATE_NEW_CONSOLE

是的话就会跳转到下面的代码,此时会设定错误并退出函数

以下宏定义则是用来判别如何设置进程优先级

函数会使用这些宏来判断创建进程的标志中指定了什么样的进程优先级

接着根据指定的优先级来赋值不同的进程优先级

设置完进程优先级,函数就会把创建进程中指定的标志中有关进程优先级的内容删除

接下来的内容则判断标记中和VDM相关的内容

接下去就会判断创建进程时传入的参数lpEnvironment是否有效以及是否有CREATE_PROTECTED_PROCESS标志,满足的话则会将eax指向保存环境字符串的地址的末尾

然后把传入的Unicode字符串转换为Ansi字符串后保存

接着函数会继续初始化局部变量,并且将传入的StartupInfo内容保存到局部变量中

接下来的内容最主要的就是传入的将要启动的文件名通过RtlDosPathNameToNtPathName转换为NT内部名称(比如:c:\temp\1.exe会转换为\device\harddiskvolumn1\temp\1.exe),因为在内核中需要使用这样的名字来打开文件。

将经过转换以后的文件名作为参数调用NtOpenFile来获得文件句柄。

而第二个参数指定了句柄的权限,而根据以下的宏定义可以知道要获得的权限。

如果第一次调用NtOpenFile失败,则函数会再次调用该函数打开文件,只是此时要获得的权限发生改变。

根据宏定义可以知道,这次是以只要获得执行权限的方式打开

如果这次打开还是失败,那么函数接下来就会判断失败原因。首先通过RtlIsDosDeviceName判断是否是因为正在执行设备导致失败,是的话会设置相应的错误码。

如果不是则会将NtOpenFile函数执行失败的结果作为error code返回。

接下来函数会判断调用方是否指定了桌面,如果没有的话就会将本进程PEB中的桌面内容赋值过去

随后函数就通过NtCreateSection来对打开的文件句柄创建内存映射

打开成功以后,将会调用BasepIsProcessAllowed函数来得知是否可以加载进程

根据宏定义

可以知道,接下来会判断创建进程的标志中是否带有CREATE_FORCEDOS

如果没有,则会设置错误码,释放句柄等等

如果内存映射成功,函数会进行一系列验证,然后通过NtQuerySection来获得文件的信息

根据宏定义

函数会判断传入的参数中是否带有调试相关的标志

如果有,则会进一步验证PEB中的ReadImageFileExecOptions字段是否为0

如果不为0,接下来就会通过LdrQueryImageFileExecutionOptions来查询调试器信息

随后函数会再次判断参数中是否有调试标志

有的话就会调用DbgUiConnectToDbg来建立调试关系

至此已经打开了有效的Windows可执行文件并将其映射到相应的地址空间,接下来就需要通过系统调用NtCreateProcessEx来创建内核对象,由于这个部分内容比较多,后面再详细说。

创建完成以后函数会判断是否设置了进程的优先级

如果设置了,接着会判断进程优先级是否为实时优先级

是的话就会调用函数来判断是否权限是否足够

如果符合条件,接下来就会调用函数来设置进程的优先级

根据以下的宏定义就可以知道

设置完进程优先级以后,函数会继续判断传入的标志中是否有

如果有,接下来就会通过函数来设置错误模式

接下来会调用函数从创建的进程的内存中读出数据

接下来会分别对输入输出以及错误句柄进行判断,如果不是控制台句柄就会通过StuffStdHandle进行复制

现在进程执行体已经创建完成,但是想要执行指令,还需要线程才可以完成。因此,要继续创建再进程中运行的线程。

至此,Windows执行体进程对象已经创建完成,然而其中依然不具备线程,因此还无法开始工作。接下来的工作就需要创建线程,而创建线程之前需要初始化创建线程需要的资源。

首先需要调用BaseCreateStack来创建线程栈

接着需要调用BaseInitializeContext来初始化线程的CONTEXT

最后通过系统调用NtCreateThread来创建内核线程对象,但是此时是以挂起的方式创建的线程

当创建了进程与线程对象以后,接下来函数就会使用CsrClientCallServer来给Windows子系统(Csrss)发送消息

该消息包含以下信息:

路径名称和SxS路径名称

进程和线程句柄

区域句柄

访问令牌句柄

媒体信息

AppCompat和填充数据

沉浸式进程信息

PEB地址

各种标志,如是否为受保护进程,是否需要再提权后运行等

代表进程是否属于某个Windows应用程序(Csrss借此决定是否显示启动光标)的标志

UI语言信息

DLL重定向和.local标志

清单文件信息

收到该消息后,Windows子系统将执行下列操作:

CsrCreateProcess复制进程和线程句柄。在这一步中,进程和线程的使用计数会从1增加到2

分配Csrss进程结构(CSR_PROCESS)

新进程的异常端口设置为Windows子系统的通用功能端口,借此Windows子系统能再进程中发送二次异常

如果要新建进程组,且该新进程称为进程组的根(在CreateProcess中使用了CREATE_NEW_PROCESS_GROUP标志),则在CSR_PROCESS中设置。进程组可用于将控制事件发送给一组共享了同一控制台进程

分配并初始化Csrss线程结构(CSR_THREAD)

CsrCreateThread将该线程插入到进程的线程列表

递增会话中的进程计数

进程的关机级别(shutdown level)设置为0x280,这是默认的进程关机级别

新建的Csrss进程结构被插入到Windows子系统范围内的进程列表

由于上面创建线程对象的时候是以挂起的形式创建的,所以现在需要通过NtResumeThread来恢复线程的执行

此时新创建的进程中的初始线程正式开始工作,由于进程中还有一些资源需要初始化以后才可以保证线程的正常运行。所以,线程最开始的工作依然是对进程的初始化,这部分内容较多,之后再详细说。

上面说到,创建进程过程中会通过系统调用NtCreateProcess来创建进程的内核对象,而再该函数中则是对几个参数进行检验并赋值相应的标记以后就调用NtCreateProcessEx来完成操作。其中参数InherObjectTable说明新创建的进程是否要继承父进程的句柄表,如果要,则参数ParentProcess就是父进程的句柄,一般这就是当前进程,但也可以不是。换言之,当前进程可以为别的进程创建子进程,让新创建的进程从别的进程继承句柄表,实际上还包括一些资源的配额和有关的属性。另一个参数SectionHandle是一个文件映射区的句柄,这个映射区代表目标映像文件。最后两个进程间的通信端口DebugPort和ExceptionPort的句柄。顾名思义,这两个端口是供新创进程发送调试信息和异常处理信息的端口,是可以为所有进程共用的系统资源。

NtCreateProcessEx函数首先就是对PreviousMode进行判断

如果不为0,说明是从用户层模式调用的函数,此时就需要对句柄是否可读进行验证

接着判断父进程句柄是否为空,如果是则报错

如果不是则将参数入栈以后调用PspCreateProcess

在PspCreateProcess函数中首先会对局部变量进行赋值,然后会判断创建标志中是否有相关标志

如果没有,则会返回错误

接下来会判断是否有父进程句柄

如果没有,则会对会父进程的EPROCESS赋值为空且使用KeActiveProcessors来赋值eax

否则会调用ObReferenceObjectByHandle来获得父进程的对象地址,并且使用父进程的Affinity来赋值eax

接着在对局部变量进行赋值

接下来就会通过ObCreateObject来在内核中创建进程对象并将创建的进程对象赋值到局部变量中

创建好了内核对象以后,就要开始完成对对象的初始化。要注意此时得到的地址指向的是内核对象的对象体,也就是EPROCESS结构,此时ebx的值保存的就是创建的进程对象的EPROCESS地址,所以接下来的代码执行的就是对其成员的赋值,根据上面的EPROCESS结构不难知道要赋值的内容

初始化过程也包含了继承父进程资源和属性的过程。这里首先通过PspInheritQuota继承父进程的资源配额,在通过ObInheritDeviceMap继承父进程的(磁盘)设备位图DeviceMap

接下来判断是否存在父进程的对象

如果不存在,则会为创建的进程初始化为默认数值

否则就用父进程的数据来初始化创建的进程

接着判断传入的SectionHandle是否为NULL

如果不为空,则会通过函数来获得想要的对象地址并将其赋值到局部变量SectionObj中

如果为空会对父进程进行判断

如果父进程为PsInitialSystemProcess则会对SectionObj赋值为空,否则会将父进程的SectionObject赋给局部变量SectionObj,注意此时的edi指向的是父进程对象

随后将局部变量赋值到创建的进程的SectionObject中

接下来函数会判断是否有DebugPort句柄,有的话获取对象地址然后赋值到当前进程中

接着在判断ExceptionPort句柄是否存在,存在的话依然是获取对象地址后赋值到当前进程中

随后初始化创建的进程的退出状态及调用函数来设置父进程的安全属性

接下来会判断是否具有父进程对象

如果没有的话,就会将当前进程的句柄表赋值给创建的进程并调用函数来完成进程地址空间的初始化

如果有的话,就会直接通过函数为新进程创建地址空间,并构建页目录表,页表及物理页的关系

在对创建的进程成员赋值以后继续调用函数来初始化进程的优先级,页面映射表等等

调用函数来初始化创建的进程的句柄表,如果此时父进程被指定了,父进程的句柄表就会被拷贝到新进程中,句柄表中的每个对象的引用计数都加1

调用函数初始化新创建进程地址空间

调用函数将进程对象的系统DLL(NTDLL.DLL)映射到创建的进程空间中

调用函数获得新进程的会话ID以后在调用函数来设置令牌的会话ID

调用函数在全局句柄表PspCidTable中增加进程的句柄并获得进程PID,将返回值赋给新建进程

对句柄表所指向对象中的UniqueProcessId进程赋值,随后调用函数设置访问状态

初始化局部变量为0以后,调用函数将新建对象的PEB赋值为0

将新建进程的ActiveProcessLinks插入到全局链表PsActiveProcessHead尾部中

调用SetCreateAccessStateEx设置状态访问以后,调用ObInsertObject将创建的对象插入到句柄表中


[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

最后于 2021-12-21 18:22 被1900编辑 ,原因:
收藏
免费 7
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回
//