Windows 进程创建完整过程(除去细节)
当前流程是分析WinXP x86得到的,在最新版本Windows上不一定正确,但是可以做一个参考,
由于我这里符号并不全,所以导致我这里有些东西看到的可能是错误的,误导了我,然后我就做了个错误的记录,
有缘人如果看到的话,可以帮我指正一下,我会很高兴。
工作挺忙的,三天的业余时间,哎,个人水平问题吧,还是没有办法详细地分析出完整套路,算是个简要分析吧。
差点忘记了,我这里分析的文件是 kernel32.dll 和 ntoskrnl.exe
1:入口 CreateProcessW
当我们在应用层调用CreateProcessW的时候,参数稍作整理,会直接调用到 CreateProcessInternalW
(CreateProcessA,会调用 CreateProcessInternalA,然后整理了参数也会调用到CreateProcessInternalW)
2:CreateProcessInternalW
首先,开场是大概是2000来行的汇编指令,判断进程创建参数,以及文件安全属性等等乱七八糟没用的,
然后是 NtOpenFile NtCreateSection 一系列的函数,获取文件句柄和section句柄
中间经过一系列的文件类型判断,有效性判断,及属性判断,
调用 NtQuerySection 获取段属性,
判断是否需要Debug方式启动,并且做对应的设置,
加载 advapi32.dll ,然后获取 CreateProcessAsUserSecure 函数地址,但是并没有使用它,
很可能是根据这个函数是否存在,来判断当前操作系统版本,后面直接调用 NtQuerySystemInformation 来获取操作系统信息,参数很奇怪,0x47,可能就是特殊的情况,
(有空再看)
中间判断了一个位,然后调用了DBG相关的函数,(可能是判断DBG相关吧)
最后就是一个Nt函数,NtCreateProcessEx,进入内核了,(也可能是个Zw函数)
3:NtCreateProcessEx
上一部分,调用ntdll 的ZwCreateProcessEx/NtCreateProcessEx 之后,
进入内核,走SSDT表,第48项,进入NtCreateProcessEx,
判断当前执行模式,内核模式的话,就往下走,否则,改了模式往下走
调用PspCreateProcessEx,
1)获取父进程信息,
并且继承父进程的执行位置
2)给子进程创建一个EPROCESS,然后初步设置它
3)初始化子进程的线程列表
4)增加引用计数,并且继承父进程的QuotaBlock,如果父进程存在的话,否则用系统默认的
5)继承父进程的DeviceMap,如果有父进程,否则用系统默认的
6)如果父进程存在,那么再继承父进程的属性
7)如果参数Section句柄存在,那么就获取对象,并且保存,后面需要用,保存到进程中
这个Section,实际上是上面应用层CreateProcessInternalW函数创建的那个Section
8)如果Debug端口存在,那么就获取它的对象,如果不存在,则从父进程继承
上面的第一个jnz跳转到下面,下面的最后一个jmp回到上面的cmp处
9)初始化PrimaryToken
10)中间继续初始化各种乱七八糟东西,包括
初始化进程地址空间
如果当前进程EProcess不存在,就用另外一种方法,初始化进程空间
PCB
优先级组,因为后面直接就用EPROCESS了,所以我推断这里可能同时也初始化了EPROCESS剩下的部分
11)如果 section存在,那么attach到子进程,
在很深的位置先循环调用了N次 MiMapViewOfPhysicalSection 函数来映射物理内存地址,然后
初始化section,根据section展开文件到子进程
这里有else,但是一般来说,不会走到else里面,因为如果是有效PE,那么section肯定存在,这是应用层已经找到的,然后转化成的内核对象,
然后向自己内部再影射一个子进程模块的内存,映射了之后,就释放掉,只是判断是否成功,如果成功,什么都不干,不成功就返回到label_48
(走到else里面也是同样的操作,只不过目标section不存在,那么就只能映射自己的)
///中间有一块这段代码,个人能力问题,无法看出这里是做什么的,因为结构体识别可能是有一定的问题
///
12)初始化PEB
PEB
实际上这里有点问题,v74在前面的时候,是必须 hSection 存在的时候,才会 ==1,也就是说必须hSection存在,才会进入外层if
但是外层if进入了之后,反过来说,就是hSection肯定存在,肯定不会走else,但是为什么它这么写
13)初始化APC?不知道是否真的是这样,但是清理APC的功能,这里是第一次使用
然后判断APC对列里面有没有APC,如果有的话,触发一个软中断,去运行它
14)做一个 AccessState ,然后使用它把子进程EPROCESS放到进程句柄表中,返回一个句柄
然后AccessState就没用了,释放掉
最后设置一下进程优先级组
15)接近尾声,这里获取当前进程允许的访问权限位
16)收尾
设置进程创建时间,然后返回进程句柄,减掉自己的引用计数,防止泄露
4:CreateProcessInternalW,回到应用层了
沿着第二部分的NtCreateProcessEx继续来看,这里已经回到应用层了,
继续往下,并且这里拿到了一个 新进程的句柄,也就是第一个参数
一大片代码,设置进程优先级和处理模式相关
NtSetInformationProcess
NtAllocateVirtualMemory
申请了一块内存,然后好像也没用过,
创建命令行参数,
初始化stdin stdout stderr
BasePushProcessParameters
就是直接dup出来,然后给对面进程用
给目标进程创建一个栈
给线程构建上下文
然后,主线程就可以跑了
(上下文构建中,其实有个小细节)
其实在 BaseInitializeContext 函数中,根据第五个参数,会判断走哪个启动函数
如上,进程启动,最后一个参数,写死为0,则会走最下面的 BaseProcessStartThunk 函数
而 BaseProcessStartThunk 内部还调用了。。。
再往里看,就是这样
通过对比另外两个函数,可以推断,这里应该就是走主线程 _tmainCRTStartup / wWinMainCRTStartup 位置的了
5:NtCreateThread 开始主线程部分
应用层陷入内核,走SSDT,到了驱动里面的NtCreateThread,
进来还是模式校验,
然后走
1)获取进程对象
2)创建ETHREAD
3)在进程句柄表中创建线程的handle
4)创建TEB
初始化TEB
5)中间使用了超级大篇幅来初始化这个ETHREAD
初始化了之后,这一块,就是准备开跑了,
进程的活动线程数++,然后插入列表,然后启动线程,
6)直到这里,这里是进程创建回调,注意哦,这里是在父进程里面调用的
7)判断作业是否在工作,所在进程是否在作业中,如果在的话,特殊处理,让他去完成,并且清理APC
8)压轴戏,线程创建回调
9)两个回调结束之后,似乎就没什么好做得了,把线程对象插入句柄表
这里的插入和前面的创建不是一个东西,那个ExCreateHandle 是创建全局句柄表,
这里是插入进程句柄表
10)后面就是枯燥乏味的收尾工作
写时间戳,写访问权限,解对象引用,
再清理一次APC,
然后把线程句柄返回,
11)补充一下吧
正常结束时,这里实际上是有个收尾的小工作的,
这里面KeReadyThread 是关键操作,它把 ETHREAD 放到了进程 KPROCESS 的 ReadyListHead 里面,
这样应该就可以swapcontext了,
其实它内部还有众多关键函数,如 KiSetSwapEvent ,看名字和内部实现,似乎就是抢抢占时间片去了。
我所关心的整个部分,实际上就是,
进程、线程创建回调的触发时机,实际上触发的进程上下文都在父进程中,触发时机都在PspCreateThread中,
因为实际工作中,我们能用到的部分,也就这里了
6:回到应用层
这里直接判断返回值,有问题的话,直接错误返回,
没有问题的情况下,那么继续往下做判断
1)通知Csrss
由于我这里符号不全,我想下面的ExitStatus 应该是 Csrss返回的吧。
一旦有问题,立刻退出
2)指派进程到一个作业中
3)后面直到最后,通篇都是整理内存,释放空间了,
基本上就没有干别的活,
最后才是函数返回。
这样,整体流程结束。
补充一下,镜像加载回调的位置吧,
这个,实际上,镜像加载回调被调用的时候,已经和镜像没什么关系了,
而且镜像早早已经被铺到内存中了
在InitThread之前,是初始化TEB部分,这里初始化了TEB之后,设置了一个回调函数
创建应用层线程PspUserThreadStartup
进入这个函数里面,可以看到
有这样一个判断,由于我实在没有找到那个6代表什么,可能是2|4,但是我没找到,所以无法知道它是什么,
进入函数之后,里面会进行一次镜像加载回调状态的判断,如果非隐藏状态,那么再经过一个函数,
就会到这里,联系外面的函数,可以清晰地看出,这里就是镜像加载回调的调用位置,
总地来说,由于镜像加载回调有可能是异步调用的,所以无法确切地知道它的位置,
但是它的位置实际上是在前两个回调之后的(其实已知,这些都是废话)
重点:
在CreateProcess 函数中,会开辟子进程的进程空间,
然后同时会map子进程的主模块到进程空间中,这时不会调用镜像加载回调和进程创建回调,
到了CreateThread 里面,整理了线程各种信息之后,
会先调用进程创建回调,
然后调用线程创建回调,
这时,当前进程上下文还是在父进程中,
最后,当线程跑起来之后,
第一个回调会被触发,就是主模块的镜像加载回调,
这时剩下的就是其他模块的镜像加载回调了,
后面就不是很重要了,前面这个流程应该是最重要的。