关于Windows的进程创建和映像装入的过程,“Microsoft Windows Internals 4e”一书的第六章中有颇为详细的说明。本文就以此为依据,夹译、夹叙、夹议地作一介绍。书中说,创建进程的过程分成六个阶段,发生于操作系统的三个部分中,那就是:Windows客户端即某个应用进程的包括Kernel32.dll在内的动态连接库,Windows的“执行体”、即内核(确切地说是内核的上层),以及Windows子系统的服务进程Csrss中。这六个阶段是:
1. 打开目标映像文件。
2. 创建Windows的“执行体进程对象”,也就是内核中的“进程控制块”数据结构。
3. 创建该进程的初始(第一个)线程,包括其堆栈、上下文、以及“执行体线程对象”,即内核中的“线程控制块”数据结构。
4. 将新建进程通知Windows子系统。
5. 启动初始线程地运行(除非因为参数中的CREATE_SUSPENDED标志位为1而一创建便被挂起)。
6. 在新进程和线程的上下文中完成用户空间的初始化,包括装入所需的DLL,然后开始目标程序的运行。
下面分阶段叙述。
第一阶段:打开目标映像文件
在Win32位API中,创建进程是由CreateProcess()完成的。这实际上是个宏定义,根据不同的情况定义成CreateProcessA ()或CreateProcessW()之一,这两个函数都在kernel32.dll中(可以用工具depends观察)。两个函数的区别仅在于字符串的表达,前者采用ASCII字符,而后者采用“宽字符”、即Unicode。实际上Windows的内部都采用宽字符,所以前者只是把字符串转换成宽字符格式,然后调用后者。
可以在Windows上运行的可执行软件有好几类,处理的方法自然就不一样:
● Windows的32位.exe映像,直接运行。
● Windows的16位.exe映像,启动ntvdm.exe,以原有命令行作为参数。
● DOS的.exe、.com、或.pif映像,启动ntvdm.exe,以原有命令行作为参数。
● DOS的.bat或.cmd批命令文件(脚本),启动cmd.exe,以原有命令行作为参数。
● POSIX可执行映像,启动posix.exe,以原有命令行作为参数。
● OS/2可执行映像,启动os2.exe,以原有命令行作为参数。
这里面最重要的当然是32位的.exe映像,而最后两类现在已经很少见了。从对于除32位.exe以外的各种映像的处理,读者不妨对比一下Wine对.exe映像的处理,看看这里有着什么样的相似性。
当然,我们在这里只关心32位.exe映像。对于这一类映像,CreateProcess()首先打开映像文件,再为其(分配)创建一个 “Section”、即内存区间。创建内存区间的目的当然是要把映像文件影射到这个区间,不过此时还不忙着映射,还要看看。看什么呢?首先是看已经打开的目标文件是否一个合格的.exe映像(万一是DLL映像?)。还要看的事就有点出乎读者意外了,看的是在“注册表”中的这个路径:
HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options
用depends可以看到,ntdll.dll中有个函数LdrQueryImageFileExecutionOption s(),就是专门干这个事的。
如果上述路径下有以目标映像文件的文件名和扩展名为“键”的表项,例如“image.exe”,而表项中又有名为“Debugger”的值,那么这个值 (一个字符串)就替换了原来的目标文件名,变成新的目标映像名,并重新执行上述的第一阶段操作。这样做的目的当然是为调试程序提供方便,但是我们不妨设想:如果黑客或某个木马程序设法在注册表中加上了一个表项?这时候用户以为是启动了程序A,而实际启动的却是B!。
第二阶段:创建内核中的进程对象
我们知道,Linux上的每个进程(线程)都有一个“进程控制块”、即task_struct数据结构,与具体进程/线程有关的绝大部分信息都集中存储在这个数据结构中。而Windows则有所不同。首先,Windows的进程和线程各有不同的“对象”、即数据结构,从概念上把线程和进程分离开来。线程是具体的(执行)上下文,是CPU调度的单位和目标,而进程只是若干共享地址空间和特性(如调度优先级)的线程的集合。于是,进程有进程的数据结构,线程有线程的数据结构。这就好像是对一组task_struct数据结构“提取公因子”所形成的结果,这个举措是很好理解的。进一步,Windows又把本可集中存储的的进程数据结构也拆分成好几个对象,有的在内核中,有的则在用户空间。
内核中与进程有关的对象有:
● EPROCESS。即struct _EPROCESS,在“Internals”书中也称为“Process Block”。它代表着Windows的一个进程,‘E’表示“Executive”,微软把Windows内核中的上层称为“Executive”、以区别于下层的设备驱动和内存管理等成分、一般翻译成“执行体”。“Executive”也有“管理”、“运行”的意思(所以CEO就是“总裁”)。
● KPROCESS。这是EPROCESS内部的一个成分,其名称就叫“Pcb”。
● W32PROCESS。下面将要讲到,在用户空间有个“Windows子系统”的服务进程csrss。这个服务进程为系统中的每个Windows应用进程都维持着一个数据结构,其中包含了一些与窗口和图形界面有关的信息。而对于窗口和图形界面的操作原来也是由csrss在“客户”进程的请求下实现的。但是,为了提高效率,后来把这部分功能移到了内核中。与此相应,有关数据结构的一部分也需要移到内核中,就成了W32PROCESS。
既然KPROCESS是EPROCESS一部分,实际上内核中与进程有关的对象实际上只有两种,就是EPROCESS和W32PROCESS。不过这里没有包括“打开对象表”,那也是每个进程都有的(Linux内核中的“打开文件表”也在进程控制块的外面)。
用户空间与进程有关的对象有:
● 如上所述,把W32PROCESS数据结构移入内核以后,csrss仍需要为每个Windows进程保持一些别的信息,所以csrss内部仍有按进程的相应数据结构。
● PEB(Process Environment Block)、即“进程环境块”。PEB中记录着进程的运行参数、映像装入地址等等信息。PEB在用户空间中的位置是固定的,总是在 0x7ffdf000。在Windows中,用户空间和系统空间的分界线是2GB、即0x80000000,所以PEB在靠近用户空间顶端的地方。
“Internals”书中并未给出有关数据结构的定义,但是通过Debug手段给出了EPROCESS的内部结构:
+0x000 Pcb : _KPROCESS
+0x06c ProcessLock : _EX_PUSH_LOCK
+0x070 CreateTime : _LARGE_INTEGER
+0x078 ExitTime : _LARGE_INTEGER
+0x080 RundownProtect : _EX_RUNDOWN_REF
+0x084 UniqueProcessId : Ptr32Void
+0x088 ActiveProcessLinks : _LIST_ENTRY
+0x090 QuotaUsage : [3] Uint4B
+0x09c QuotaPeak : [3] Uint4B
+0x0a8 CommitCharge : Uint4B
+0x0ac PeakVirtualSize : Uint4B
+0x0b0 VirtualSize : Uint4B
+0x0b4 SessionProcessLinks : _LIST_ENTRY
+0x0bc DebugPort : Ptr32Void
+0x0c0 ExceptionPort : Ptr32Void
+0x0c4 ObjectTable : Ptr32_HANDLE_TABLE
+0x0c8 Token