Windows进程创建的流程分析
. 创建进程的大体流程:
创建进程的过程就是构建一个环境,这个环境包含了很多的机制 (比如自我保护, 与外界通信等等)。 构建这个环境需要两种“人”来协调完成(用户态和内核态),他们各有分工,其中用户态提供原料(提供创建的那些参数), 内核态负责来构建这个环境,由于环境是由内核态构建的,因此他持有这个环境的控制权, 而用户由于提供了原料, 因此他具有使用权。 内核态开始构建环境中的基础设施(进程对象,等等),在构建完基础设施后,内核态通知用户态基础设施构建已经完成,是否需要继续构建其他设施,于是用户态通知内核态继续构建一条通道(既创建线程),方便两边的通信,当用户态接收到线程创建完毕的信息后,便可以开始使用这个环境(投入生产),以后缺啥补啥。
二. 3环进程的创建分析:
CreateProcesssA的分析:
在CreateProcessA函数中调用了CreateProcessInternalA,调用该函数时,增加了两个参数(一头一尾加了个零参数),在该函数里面也没看到对这两个参数的引用,而是直接传递给CreateProcessInternalW函数。
在CreateProcessInternalA中,首先检查了参数 lpCmdLine, 判断是否为0,不为0则将lpCmdLine初始化为UNICODE字符串, 为0则做了几个局部变量赋值后跳过初始化为UNICODE字符串。接着继续检查参数,先将lpStartupInfor的内容拷贝到一个局部变量, 然后判断参数lpApplicationName是否为0,不为0则参数转换为UNICODE字符串,为0则跳过转换继续判断参数lpCurrentDirectory是否为0,这个和参数lpAppLicationName的判断逻辑是一样的。接着判断STARTUPINFOA.lpReserved域是否为0(MSDN上说在调用CreateProcess前,要把lpReserved设置为NULL ,不过我试了下,不为0也没问题。),该域不为0则和其他字符串参数一样做UNICODE的转换,后面还判断STARTUPINFOA中的几个字符串的域,不为0的都作了下转换。最后调用了CreateProcesssW函数, 看上面的步骤大家应该知道了其实CreateProcessInternalA函数只是对字符串参数或者结构体中包含字符串类型的域的作了检查和转换工作,然后就调用了下层函数。(附图)
分析CreateProcesssW的大概流程:
1. 将参数 保存到局部变量中。
2. dwCreationFlags 的值至少由一个标志组合成(一些创建标志和优先级类型),首先屏蔽 CREATE_NO_WINDOW 标志,代码如下:
mov eax, [ebp+20h] ; ebp+20h = dwCreationFlags
and eax, 0F7FFFFFFh ; 屏蔽CREATE_NO_WINDOW标志。
mov [ebp+20h], eax
mov ecx, eax
and ecx, 18h ;18h = DETACHED_PROCESS | CREATE_NEW_CONSOLE
cmp cl, 18h
jz loc_7C8427DE ;如果相等,说明创建标志不合法
经过屏蔽标志位后,判断dwCreationFlags中是否包含CREATE_NEW_CONSOLE | DETACHED_PROCESS的组合, 如果包含它们的组合,参考MSDN上, 存在这种组合是不合法的, 因此跳转到错误处理中, 该错误处理例程中,将57h与teb-> LastErrorValue想比较,如果不相等,就更新LastErrorValue的值为57h, 实际上GetLastError() 函数的返回的错误码就是从teb-> LastErrorValue获取的。
3. 在2中说到dwCreationFlags中也包含优先级类型的组合, 接着就判断优先级,判断的顺序依次是IDLE_PRIORITY_CLASS, NORMAL_PRIORITY_CLASS, HIGH_PRIORITY_CLASS, REALTIME_PRIORITY_CLASS, 只要满足其中一个优先级,就跳过其他优先级的判断,如果都不满足, 将权限级置为0.
(当满足IDLE_PRIORITY_CLASS时),置优先级标志为1.
(当满足NORMAL_PRIORITY_CLASS时),置优先级标志为2.
(当满足HIGH_PRIORITY_CLASS时),置优先级标志3.
(当满足REALTIME_PRIORITY_CLASS时),由于该优先级别很高,比操作系统优先级还高(参考MSDN),作了如下操作, 申请堆空间 -->打开一个令牌对象与线程关联,并返回一个句柄可用于访问该令牌。-->调整优先级令牌。 置优先级标志4。
mov eax, [ebp+20h] ; ebp+20h = dwCreationFlags
and eax, 0F7FFFFFFh ; 屏蔽CREATE_NO_WINDOW标志
mov [ebp+20h], eax
mov ecx, eax
and ecx, 18h
cmp cl, 18h ; 判断dwCreationFlags 是否为CREATE_NEW_CONSOLE |DETACHED_PROCESS
jz loc_7C8427DE ; 标志组合不合法, 进行错误处理并退出
mov [ebp+var_6D4], ebx
mov [ebp+var_6DC], ebx
test al, 40h
jnz IsIdlePriority ; 优先级为IDLE_PRIORITY_CLASS
test ah, 40h
jnz loc_7C8427F6
test al, 20h
jnz IsNormalPriority ; 优先级为NORMAL_PRIORITY_CLASS
test ah, ah
js loc_7C842802
test al, al
js IsHighPriotity ; 优先级为HIGH_PRIORITY_CLASS
test ah, 1
jnz IsRealTimePriority ; 优先级为REALTIME_PRIORITY_CLASS
4. 继续判断dwCreationFlag的情况,首先在dwCreationFlag过滤掉表示优先级的标志位,然后在判断是什么创建标志。
mov [ebp+var_668], bl
and word ptr [ebp+dwCreateFlag], 3E1Fh ; 屏蔽权限级表示的位
mov edi, 800h
mov esi, 1000h
test [ebp+dwCreateFlag], edi
jnz loc_7C842832 ; dwCreationFlag = CREATE_SEPARATE_WOW_VDM
test [ebp+dwCreateFlag], esi
jnz short loc_7C819A33 ; dwCreationFlag = CREATE_SHARED_WOW_VDM
mov eax, _BaseStaticServerData
cmp [eax+19F4h], bl
jnz loc_7C84283C
6. 判断lpEnvironment是否为空, 如果不为空,将Ansi字符串转换为UNICODE_STRING, 为空的话跳过这一步,接着调用RtlDosPathNameToNtPathName_U函数将DOS路径转换为NT路径,由于用户给定的路径一般都是DOS路径,而内核需要的是NT路径,因此需要转换一下。
7. 接着调用NtOpenFile()得到文件句柄
push 60h ;打开选项
push 5 ;共享模式
lea eax, [ebp+pIoStatusBlock]
push eax ; I/0状态块
lea eax, [ebp+pObjAttribute]
push eax ; 对象属性
push 1000A1h
;SYNCHRONIZE |FILE_ATTRIBUTE_NORMAL|
;FILE_ATTRIBUTE_ARCHIVE|FILE_ATTRIBUTE_READONLY
lea eax, [ebp+pHandleOfFile]
push eax ; 文件句柄
mov esi, ds:__imp__NtOpenFile@24 ; NtOpenFile(x,x,x,x,x,x)
call esi ; NtOpenFile(x,x,x,x,x,x) ; NtOpenFile(x,x,x,x,x,x)
接着通过NtOpenFile得到的handle接着调用了NtCreateSectiond函数得到内存区对象句柄
push [ebp+pHandleOfFile] ; 文件句柄
push 1000000h
push 10h ; 内存区页面保护属性
push ebx ; SETION大小
push ebx ; 对象属性
push 0F001Fh ; 访问掩码
lea eax, [ebp+pSectionHandle]
push eax ; 指向内存区对象的指针(传出参数)
call ds:__imp__NtCreateSection@28 ; NtCreateSection(x,x,x,x,x,x,x)
接着调用BasepIsProcessAllowed函数, 该函数用来判断应用程序名是否在授权文件列表中,函数实现调用了NtOpenKey函数打开了注册表中的[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\ CurrentVersion\Image File Execution Options键, 这个键跟以前流行的镜像劫持技术有关,但操作真正这么做的原因不是很清楚,希望知道的朋友交流一下。
在得到内存区对象句柄后调用了NtQuerySection函数,返回后得到节的基本信息(节基地址,大小,属性)
push ebx ; 接受返回的大小
push 30h ; 内存区信息的长度
lea eax, [ebp+OepAddress]
push eax ; 接受内存区信息Buffer
xor edi, edi
inc edi
push edi ; edi = 1 = SectionImageInformation
push [ebp+pSectionHandle] ; 内存区句柄
call ds:__imp__NtQuerySection@20 ; 将区对象作为镜像执行文件来查询信息
然后判断创建标志中是否包含DEBUG_PROCESS或者DEBUG_ONLY_THIS_PROCESS,如果包含该标志,判断PEB->ReadImageFileExecOptions域是否为0,如果不为0, 调用LdrQueryImageFileExecutionOptions函数查询该信息,如果不包含该标志,也用调用LdrQueryImageFileExecutionOptions函数。
下面检查镜像文件的部分信息的有效性:
mov ax, [ebp+MachineType] ; MachineType为机器类型,
cmp ax, ds:7FFE002Ch ; ds:7FFE002Ch中的内容为0x014c
jb loc_7C84329E
cmp ax, ds:7FFE002Eh
ja loc_7C84329E
cmp dword ptr [ebp+ SubSystemVersion], 2
jz short loc_7C8191C4 ; 子系统次版本号
cmp dword ptr [ebp+ SubSystemVersion], 3 ; 子系统的比较版本(PE信息中包含)
jnz loc_7C842D0C
接着调用函数BasepIsImageVersionOk判断镜像文件版本是否合法
movzx eax, [ebp+pImageFileMinorVersion] ; 子系统次版本号
push eax
movzx eax, [ebp+pImageFileMajorVersion] ; phSection->GpValue
push eax ; 子系统的主版本号
call _BasepIsImageVersionOk@8 ; 判断镜像文件的版本是否合法。
然后加载advapi32.dll并获得CreateProcessAsUserSecure函数的地址:
push offset LibFileName ; "advapi32.dll"
call _LoadLibraryA@4 ; LoadLibraryA(x)
mov esi, eax
mov [ebp+phModuleOfAdvapi32.dll], esi ; 保存模块基地址
cmp esi, ebx
jz short loc_7C81923C
push offset ProcName ; "CreateProcessAsUserSecure"
push esi ; hModule
call _GetProcAddress@8 ; 获得指定模块中指定函数的地址
获得函数CreateProcessAsUserSecure地址,不过后面没有发现调用这个函数。
然后调用BaseFormatObjectAttributes将安全属性结构格式为NT对象属性结构(得到了对象属性)。 接着调用了_DbgUiConnectToDbg在实现通过调用NtCreateDebugObject函数来创建调试对象,调用DbgUiGetThreadDebugObject来获得调试对象(作为参数传递到0环)。
call _DbgUiConnectToDbg@0 ; 实现调用中NtCreateDebugObject
mov [ebp+nReturnStatus], eax
cmp eax, ebx
jl loc_7C82DF58
call _DbgUiGetThreadDebugObject@0 ; 获得线程调试对象teb->0xF24,
mov [ebp+phandleOfDebugObj], eax
最后调用NtCreateProcessEx函数完成3环的创建过程
push [ebp+JobLevel] ; 作业级别
push ebx ; 异常端口对象句柄
push [ebp+phandleOfDebugObj] ; 调试对象句柄
push [ebp+pSectionHandle] ; 内存区对象句柄
push [ebp+CreateFlag] ; 创建标志
or esi, 0FFFFFFFFh
push esi ; 父进程句柄(FFFFFFFF)
push [ebp+ObjAttibute] ; 对象属性
push 1F0FFFh ; (访问掩码)
lea eax, [ebp+pHandleOfProcess]
push eax ; 保留进程句柄值(传出参数)
call ds:__imp__NtCreateProcessEx@36 ; 创建进程(x,x,x,x,x,x,x,x,x)
下面附上CreateProcessW函数的流程图:
三. 0环创建进程的分析:
在NtCreateProcessEx中, 首先是判断父进程是否存在,如果不存在,就返回一个STATUS_INVALID_PARAMETER错误码(即无效参数),否则调用PspCreateProcess, 参数都没有变,直接传递给该函数。
1. 在PspCreateProcess函数中,首先保存当前线程运行的前一个模式(r0/r3), 通过KTHREAD->PreviousMode可以得到前一个模式。
mov eax, large fs:124h ; eax保存了指向KTHREAD结构的指针
mov [ebp+pKthread], eax
mov cl, [eax+_KTHREAD.PreviousMode] ; 线程的前一个模式
mov [ebp+PreviousMode], cl
接着判断创建标志是否包含除DEBUG_PROCESS, DEBUG_ONLY_THIS_PROCESS,CREATE_SUSPENDED的标志之外标志, 如果包含其他的标志,也报一个无效参数错误后退出该函数。
2. 通过参数ParentProcess调用ObReferenceObjectByHandle()函数得到父进程对象的指针)
push esi ; HandleInformation
lea eax, [ebp+pParentObject] ; eax 为EPROCESS局部变量的地址(作传出参数)
push eax ; Object
push dword ptr [ebp+PreviousMode] ; AccessMode
push _PsProcessType ; ObjectType
push 80h ; DesiredAccess
push [ebp+ParentProcessHandle] ; Handle
call _ObReferenceObjectByHandle@24 ; 通过句柄得到父进程对象指针
接着判断了参数 JobMemberLevel是否为0, 如果为0,判断父进程的EPROCESS->Job是否为0,如果为0,返回无效参数错误后推出该函数。如果不为0,和JobMemberLevel不为同一流程,将父进程对象中的属性保存到局部变量中。
3. 通过ObCreateObject函数创建新进程对象并将对象内容初始化为0.
lea eax, [ebp+pNewProcessObj]
push eax ; 新对象指针
push esi ; int
push esi ; 对象的大小
push 260h ; paseContext
push esi ; int
push dword ptr [ebp+PreviousMode] ; int
push [ebp+ObjectAttributes] ; 对象属性
push _PsProcessType ; 对象类型
push dword ptr [ebp+PreviousMode] ; 处理器模式
call _ObCreateObject@36 ; 创建进程对象
mov edi, eax
cmp edi, esi
jl loc_4AAB6B ; 判断ObCreateObject函数调用是否成功
mov ecx, 98h
xor eax, eax
mov ebx, [ebp+pNewProcessObj]
mov edi, ebx
rep stosd ; 将新建对象初始化为0
从父进程继承配额信息(PspInheritQuot函数完成)和设备位图信息(ObInheritDeviceMap函数完成),然后将父进程对象中的部分域给新进程。
4. 通过参数SectionHandle调用ObReferenceObjectByHandle函数得到区对象指针,当然前提是SectionHandle有效,然后将区对象指针赋值给新进程EPROCESS的相应域中,接着就判断参数DebugPort是否为0, 当不为0时:
cmp [ebp+SectionHandle], esi
jz loc_4F449F ; 判断区对象句柄是否为0
push esi ; HandleInformation
eax, [ebp+pSectionObj]
push eax ; 内存区对象指针
push dword ptr [ebp+PreviousMode] ; 处理器模式
push _MmSectionObjectType ; 对象类型
push 8 ; 访问掩码
push [ebp+SectionHandle] ; 内存去句柄
call _ObReferenceObjectByHandle@24 ; 通过区对象句柄得到区对象指针 mov eax, [ebp+HandleInformation]
mov [ebx+_EPROCESS.SectionObject], eax ; pNewProcessObject->SectionObject = 内存区对象指针
cmp [ebp+DebugPort], esi
jnz IsDebugger ; 判断参数DebugPort是否为0 IsDebugger:(DebugPort不为0时)
push esi ; HandleInformation
lea eax, [ebp+pDebugObj]
push eax ; 新调试对象指针
push dword ptr [ebp+PreviousMode] ; 处理器模式
push _DbgkDebugObjectType ; 对象类型
push 2 ; 访问模式
push [ebp+DebugPort] ; 调试端口句柄
call _ObReferenceObjectByHandle@24 ; 通过调试对象句柄得到调试对象指针
当DebugPort为0时,调用DbgkCopyProcessDebugPort函数从父进程拷贝DebugPort给新进程。
以上部分就是通过参数来得到所需要的对象指针。
5. 接着调用PspInitializeProcessSecurity函数来设置新进程的安全属性, 主要是设置新进程的安全令牌对象(从父进程拷贝):
push ebx ; 新进程对象
push [ebp+ParentObj] ; 父进程对象
call _PspInitializeProcessSecurity@8 ; 初始进程的安全属性(设置新进程对象的令牌对象)
其中PspInitializeProcessSecurity函数里面调用了SeSubProcessToken函数来设置新进程对象的令牌对象,这个函数在XP sp3 下和win2k及WRK中不一样,函数原型为SeSubProcessToken(TOKEN ParentToken, TOKEN NewToken, BOOL MarkAsActive),MarkAsActive参数表示TOKEN是否为活动的,SeSubProcessToken函数比WRK中的少一个参数,比win2k的多一个参数。
push 1 ; TOKEN是否为活动
lea eax, [ebp+8]
push eax ; eax指向父进程对象的指针
push edi ; 父进程对象中的令牌对象
call _SeSubProcessToken@12 ; 将父进程的令牌对象赋值给新进程对象
6. 接着调用MmCreateProcessAddressSpace为新进程创建地址空间,并构建页目录表,页表 及物理页的关系。
lea eax, [ebp+pDirTableBase]
push eax ; 页目录表基地指针
push ebx ; 新进程对象
push [ebp+MinimumWorkingSet] ; 最小工作集
call _MmCreateProcessAddressSpace@12 ; 创建进程地址空间(并构建页目录表,页表 及物理页的关系)
之后调用KeInitializeProcess函数初始化新进程对象中内核对象,优先级,亲和性,页目录表物理地址帧号。
push eax
lea eax, [ebp+pDirTableBase]
push eax ; 页目录表基地址
push [ebp+Affinity] ; 亲和性
push 8 ; 进程基本优先级
push ebx ; 新进程对象
call _KeInitializeProcess@20 ;
接着调用ObInitProcess函数来初始化新进程对象的表
push ebx ; 新进程对象
mov eax, [ebp+CreateFlag]
and al, 4
neg al
sbb eax, eax
and eax, [ebp+ParentObj]
push eax ; 父进程对象
call _ObInitProcess@8 ; 初始化新进程对象的对象表(如果父进程被指定,父进程的对象表拷贝到新进程中, 对象表 的每个对象中HandleCount域都+1)
7. 调用MmInitializeProcessAddressSpace函数初始化进程地址空间,这个函数WRK和XP sp3的不一样,XP sp3的只有4个参数,WRK中有5个参数, 去掉了CreateFlags参数,
该函数的实现中调用了KiAttachProcess函数来实现进程的切换(将当前线程挂靠到新进程中),以及初始化EPROCESS中的部分域和PFN,工作集列表等。
lea eax, [ebx+_EPROCESS.SeAuditProcessCreationInfo]
push eax ; 对象名称信息
push [ebp+HandleInformation] ; 映射节信息
push esi ; 克隆进程
push ebx ; 需要被初始化的新进程对象
call _MmInitializeProcessAddressSpace@16 ; 这个函数WRK和XP的不一样,XP的只有4个参数,WRK中有5个参数
8. 调用PspMapSystemDll函数映射指定进程的区对象(映射第一个DLL):
push esi ; 需要映射DLL的基地址
push ebx ; 新进程对象
call _PspMapSystemDll@8 ; 映射新进程对象的系统DLL(NTDLL.DLL)
该函数的实现中调用了MmMapViewOfSection映射节区(调用MiMapViewOfImageSection函数将DLL作为镜像映射)
lea eax, [ebp+pMapAddress]
push eax ; 映射到那个地址
push [ebp+pNewObj] ; 进程对象(映射到那个进程)
push ebx ; 系统DLL映射节的地址
call _MmMapViewOfSection@40 ; 映射节区
通过MmGetSessionId函数获得指定进程的会话ID,调用SeSetSessionIdToken函数设置令牌的会话ID,接着调用ExCreateHandle函数在PspCidTable中添加一项。
mov edi, [ebx+_EPROCESS.Token] ; 取得pNewProcessObject->Token
and edi, 0FFFFFFF8h ; 取消Token中的低3位
push ebx ; 新进程对象
call _MmGetSessionId@4 ; 返回指定进程的会话ID
push eax ; 新进程对象的会话ID
push edi ; 新进程令牌对象
call _SeSetSessionIdToken@8 ; 设置指定Token->SessionId
mov [ebp+var_70], ebx
mov [ebp+var_6C], esi
lea eax, [ebp+var_70]
push eax ; eax = pNewProcessObject
push _PspCidTable ; 该表保存了所有进程和线程对象的指针
call _ExCreateHandle@8 ; 在PspCidTable中添加一项
9. 调用MmCreatePeb为新进程创建PEB(后面是大概是实现过程),首先通过调用KeAttachProcess函数将当前线程切换到新进程对象,然后NLS节区映射到新进程的地址空间中(MmMapViewOfSection),
接着调用MiCreatePebOrTeb创建PEB/TEB,(该函数通过ExAllocatePoolWithTag来申请_MMVAD结构大小的空间,通过MiFindEmptyAddressRangeDownTree函数在VAD树中查找一块未被使用的地址空间范围,并返回该范围的起始地址, 最后通过MiInsertVad函数将申请的地址空间插入到VAD树中。), 在创建PEB结构后,初始化PEB中部分域的值(镜像基地址,操作系统编译号等域),最后调用KeDetachProcess函数使线程回到原来的线程中。到此创建PEB完成。
lea eax, [ebx+_EPROCESS.Peb]
push eax ; 指向PEB结构的指针(传出参数)
lea eax, [ebp+pInitPeb]
push eax ; 初始PEB
push ebx ; 新进程对象
call MmCreatePeb@12; 创建PEB(将线程切换到目标进程后,创建PEB结构及初始化部分域,
mov esi, [ebp+pNewObj]
push esi ; 新进程对象(附加到那个进程)
call _KeAttachProcess@4 ; KeAttachProcess(x) lea eax, [ebp+pPeb]
push eax ; 指向PEB 对象的地址(传出参数)
push 210h ; 创建PEB/TEB的大小
push esi ; 进程对象结构EPROCESS的指针
call _MiCreatePebOrTeb@12 ; 创建 lea eax, [ebp+Size]
push eax ; 数据目录大小
push 0Ah ; 目录表项索引
push 1 ; 如果不为0, 作为镜像映射
mov eax, [ebp+pPeb]
push dword ptr [eax+8] ; ImageBase(镜像文件或者数据文件的基地址)
call _RtlImageDirectoryEntryToData@16 ; 将一个镜像文件中IMAGE_DIRECTORY_LOAD_CONFIG节, 在该文件的那个节区,返回该节区对象指针。
9. 将新进程对象EPROCESS.ActiveProcessLinks更新为全局的活动进程链表(PsActiveProcessHead), 判断父进程是否为系统进程,调用SeCreateAccessStateEx设置访问状态,调用ObInsertObject函数将进程对象加入到进程对象的句柄表中,(省了几个函数介绍)KeQuerySystemTime()结束PspCreateProcess的调用。
mov ecx, _PsProcessType
add ecx, 68h
push ecx ; GenericMapping
push [ebp+AccessMask] ; 访问掩码
lea ecx, [ebp+var_B8]
push ecx ; 访问辅助结构指针PAUX_ACCESS_DATA
lea ecx, [ebp+var_12C]
push ecx ; 访问状态(传出参数)
push eax ; 进程对象
push esi ; 线程对象
call _SeCreateAccessStateEx@24 lea eax, [ebp+pHandle]
push eax ; 接受返回新对象的句柄
push esi ; 接受返回新对象的的指针
push 1 ; 对象指针计数
push [ebp+AccessMask] ; 访问掩码
lea eax, [ebp+AccessState]
push eax ; 访问状态
push ebx ; 新进程对象
call _ObInsertObject@24 ; 插入一个对象到进程的句柄表,并返回该对象的句柄值
在调用其他几个函数后完成PspCreateProcess 调用。
最后分析下KeAttachProcess:
KeAttachProcess的实现,KeAttachProcess函数实际上是对KiAttachProcess函数的封装,只是在该函数总作了些参数的检查,首先判断当前线程挂接的进程是否为当前进程,如果是退出该函数。接着 判断当前线程的APC与DPC状态是否为活动的, 如果存在上述的情况的话,会系统崩溃然后蓝屏, 我认为这应该为了防止在切换进程时被其他更高级的优先级抢占。
mov eax, large fs:124h
mov edi, [ebp+pNewProcessObj]
mov esi, eax ; KTHREAD
cmp [esi+_KTHREAD.ApcState.Process], edi ; 判断是否为当前进程
jz short Exit
cmp [esi+_KTHREAD.ApcStateIndex], 0
jnz loc_445F94 ; 判断APC是否为活动
mov eax, large fs:994h ; _KPRCB->DpcRoutineActive
test eax, eax
jnz loc_445F94 ; 判断DPC状态是否为活动
接着调用KeRaiseIrqlToDpcLevel函数将当前线程的IRQL提升为DISPATCH_LEVEL,然后将参数线程对象,进程对象,IRQL和ApcSaveState作为参数调用KiAttachProcess函数:
push eax
push [ebp+pNewProcessObj]
push edi ; 进程对象
push esi ; 线程对象
call _KiAttachProcess@16 ; 将当前线程附加到目标进程的地址空间中
在KiAttachProcess函数中,首先调用KiMoveApcState保存当前线程的ApcState(ETHREAD->ApcSaveState = ETHREAD->ApcState), 以便在恢复切换进程时返回。
lea edi, [esi+_KTHREAD.ApcState.ApcListHead.Flink]
push edi ;原始APC
call _KiMoveApcState@8 ; 保存当前线程的APC状态到ApcSaveState
将ApcState中的KernelApcInProgress,KernelApcPending,UserApcPending的值都置为0, 接着判断当前线程ApcSaveState 是否为保存的ApcSaveState相等,判断挂接的目标进程是否在内存中, 如果不在进程就调用KiSwapProcess函数切换进程。在KiSwapProcess中,首先判断LDT是否存在,如果不存在, 获得KPCR->TSS的指针, 然后将当前进程对象的CR3(KPROCESS->DirectoryTableBase)拷贝到当前处理器的KPCR –>TSS->CR3中, 以及将IopmOffset给到TSS结构中的相应域中。到此进程切换完成了。
最后附上PspCreateProcess函数的执行流程图:
四. 3环创建线程的分析:
在创建进程返回后,此时EPROCESS,PEB结构已经创建,进程地址空间也已经创建并初始化了。接下处理下创建进程后的残余工作,调用NtSetInformationProcess函数设置进程的优先级和默认处理模式.
push 2 ; 进程信息的的大小
lea eax, [ebp+pProcessInforBuffer]
push eax ; 进程信息
push 12h ; 进程信息类型索引(12 = ProcessPriorityClass)
push [ebp+pHandleOfProcess] ; 进程句柄
call ds:__imp__NtSetInformationProcess@16 ; 设置进程信息 mov [ebp+pInforBuffer], edi
push 4 ; 进程信息的长度
lea eax, [ebp+pInforBuffer]
push eax ; 进程信息的Buffer
push 0Ch ; 设置那个进程信息的索引(0c = ProcessDefaultHardErrorMode)
push [ebp+pHandleOfProcess] ; 进程句柄
call ds:__imp__NtSetInformationProcess@16 ; 设置进程信息
接着调用了BasepSxsCreateProcessCsrMessage函数,该函数的具体作用不是很清楚,不过实现中调用了BasepSxsGetProcessImageBaseAddress函数来获得进程的PEB, 然后通过NtReadVirtualMemory()取得ImageBase. 然后调用BasePushProcessParameters函数, 其中实现中调用了RtlCreateProcessParameters来创建进程参数, 该函数对RTL_USER_PROCESS_PARAMETERS结构中的字符串域的地址改为相对的偏移量。
接下来要做的就是创建线程, 不过在在此之前还要构建线程的环境,调用BaseCreateStack函数创建栈:
lea ecx, [ebp+InitialTeb]
push ecx ; 初始TEB
push eax ; 栈的最大值
push [ebp+StackSize] ; 栈大小
push [ebp+pHandleOfProcess] ; 进程句柄
call _BaseCreateStack@16 ; 创建栈
接着调用BaseInitializeContext初始化线程上下文, 然后调用BaseFormatObjectAttributes函数格式化对象(以便传递给NtCreateThread)
push ebx ; 基本上下文类型
push [ebp+InitSp] ; 初始化栈指针值
push dword ptr [ebp-834h] ; [ebp-834] = InitPc(初始化计数值)
push [ebp+Param] ; teb
lea eax, [ebp+pThreadContext]
push eax ; eax为指向CONTEXT的指针
call _BaseInitializeContext@20
在BaseInitializeContext函数的实现中会判断参数上下文类型是否为1,如果不为1, 指定用户空间的线程启动函数为BaseProcessStartThunk(进程的第一个线程调用这个),否则为BaseThreadStartThunk(普通线程),然后CALL OEP。其中BaseProcessStartThunk函数会调用BaseProcessStart,这个函数就是调用OEP所在函数的上层函数:
BaseProcessStart@4: ; CODE XREF: BaseProcessStartThunk(x,x)+5 j
7C817054 push 0Ch
7C817056 push offset stru_7C817080
7C81705B call __SEH_prolog ;安装异常注册项
7C817060 and dword ptr [ebp-4], 0
7C817064 push 4
7C817066 lea eax, [ebp+8]
7C817069 push eax
7C81706A push 9
7C81706C push 0FFFFFFFEh
7C81706E call ds:__imp__NtSetInformationThread@16 ;
7C817074 call dword ptr [ebp+8] ; 调用启动函数(CALL OEP)
7C817077 push eax ; dwExitCode
7C817078 call _ExitThread@4 ; ExitThread(x)
用OD打开应用程序,停在OEP处, 然后看栈窗口, 将栈顶值(即ESP)的值所在的地址反汇编跟随一下:
最后调用NtCreateThread创建线程:
push 1 ; 是否创建后挂起
lea eax, [ebp+InitialTeb]
push eax ; 初始化TEB
lea eax, [ebp+pThreadContext]
push eax ; 线程上下文结构指针
lea eax, [ebp+pClientId]
push eax ; 客户端ID结构指针
push [ebp+pHandleOfProcess] ; 进程句柄
push [ebp+ObjAttibute] ; 对象属性
push 1F03FFh ; 访问掩码
lea eax, [ebp+pThreadHandle]
push eax ; 线程句柄指针
call ds:__imp__NtCreateThread
到此3环上的CreateThread就完成了, 流程图结合在0环一起给出。
五. 0环创建线程的分析:
0环中NtCreateThread函数直接调用pspCreateThread函数:
在pspCreateThread函数中,首先检查参数StartRoutine(系统线程)是否为0, 如果为0, 将PreviousMode值为1, 否则值为0。接着调用ObCreateObject函数创建线程对象并初始化为0.
lea eax, [ebp+pThreadObj]
push eax ; 新对象指针
push esi ; 不可分页面大小
push esi ; 可分页面的大小
push 258h ; 对象体的大小
push esi ; int
push dword ptr [ebp+PreviousMode] ; int
push [ebp+ObjAttribute] ; 对象属性
push _PsThreadType ; 对象类型
push dword ptr [ebp+PreviousMode] ; 处理器模式
call _ObCreateObject@36 ; 创建线程对象 mov ecx, 96h
xor eax, eax
mov esi, [ebp+pThreadObj]
mov edi, esi
rep stosd ; 初始化线程对象为0
接着将新进程中EPROCESS-> UniqueProcessId拷贝给线程对象ETHREAD-> UniqueProcess, 然后调用ExCreateHandle函数往CID表中添加线程对象。
lea eax, [ebp+pEthread]
50 push eax ; 线程对象
push _PspCidTable ; CID表指针
call _ExCreateHandle@8 ; 在指定的句柄表中添加一个表项
接着初始化信号量对象和自旋锁对象,之后判断参数进程对象指针ProcessPointer,是否为0,如果不为0,调用函数MmCreateTeb创建TEB.
lea eax, [ebp+pTeb]
push eax ; 指向TEB指针(传出参数)
push edi ; 客户端ID结构的指针
push [ebp+InitTeb] ; 初始TEB
push ebx ; 目标进程对象
call _MmCreateTeb@16 ; 创建TEB
如果ProcessPointer为0,KeInitThread初始化线程时线程函数为PspUserThreadStartup(用户线程启动函数), 否则为用户线程启动函数:
push ebx ; 进程对象(EPROCESS)
push ecx ; teb
push ecx ; 线程上下文指针
push [ebp+pStartContext] ; 启动线程上下文
push eax ; 启动线程函数指针
push offset _PspSystemThreadStartup@8 ; 系统线程函数指针 , 当创建用户线程时,参数为PspUserThreadStartup
push ecx ; 内核栈指针
push esi ; 线程对象(ETHREAD)
call _KeInitThread@32 ; 初始化线程对象(初始化APC, 信号量对象,创建内核线程栈
;初始化线程上下文)
该函数的实现中主要做了如下操作:创建内核线程栈然后调用KiInitializeContextThread初始化线程上下文
push [ebp+pContext] ; pContext 上下文记录
push [ebp+pContextStart] ; 启动线程函数上下文
push [ebp+pThreadStart] ; 启动线程函数地址
push [ebp+pKernelFunStart] ; 系统线程函数
push esi ; 内核线程对象(KTHREAD)
call _KiInitializeContextThread@20
在KiInitializeContextThread函数的实现中,在堆栈上构建了KTRAP_FRAME(陷井框架, 用来系统调用时保存用户空间堆栈的信息,或者在为了返回到用户空间时构建的上线文环境), 并指定了线程的启动函数KiThreadStartup。
然后调用KeStartThread函数,该函数除初始化线程对象的部分域外,还将进程对象中的线程列表中的线程函数放到线程对象中的线程队列中。接着调用了SeCreateAccessStateEx函数来创建线程的访问状态,ObInsertObject函数来将线程对象添加到进程中的对象表中.
然后调用ObGetObjectSecurity,PsReferencePrimaryToken设置进程对象的安全属性和令牌对象,以及调用SeAccessCheck函数设置线程的访问权限, 最后调用KeReadyThread函数将线程加入到进程对象中的线程就绪队列中(kprocess->ReadyListHead)
在以上调用完后,会调用内核中线程的启动函数KiThreadStartup, 该函数的实现中调用了PspUserThreadStartup,该函数初始化用户APC,将LdrInitializeThunk函数作为APC函数挂入APC队列中,最后调用KiDeliverApc发生APC, 通过中断返回3环。
切换到3环KiUserApcDispatcher,实现中调用LdrInitializeThunk来加载需要的DLL,
KiUserApcDispatcher
lea edi, [esp+arg_C]
pop eax
call eax //LdrInitializeThunk
push 1
push edi
call _ZwContinue@8 ;加载完DLL后,调用该函数继续返回到内核
切换到内核后作部分操作后,再次回到3环,调用用户空间的线程入口函数BaseProcessStartThunk, 该函数在3环中BaseInitializeContext中有指定。
线程创建完成后调用CsrClientCallServer通知CRSS 线程创建成功,其中调用NtRequestWaitReplyPort()来等待回应,最后调用NtResumeThread()函数恢复线程的执行。
在用户空间线程启动时调用BaseProcessStartThunk, 该函数调用了BaseProcessStart函数, 这部分代码在上面已经贴出来了。
附上线程创建的流程图:
到此, 程序分析完毕,谢谢大家看完。
Windows进程创建的流程分析的更多相关文章
- php守护进程创建和简要分析
守护进程可 由系统启动脚本 /etc/rc.local crontab任务, 用户shell 方式运行 具体概念可参考c的 进程守护化基本步骤 1.创建子进程,终止父进程 (pcntl_fork,ex ...
- windows编程 进程的创建销毁和分析
Windows程序设计:进程 进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动,在Windows编程环境下,主要由两大元素组成: • 一个是操作系统用来管理进程的内核对象.操作系统使用内 ...
- windows进程/线程创建过程 --- windows操作系统学习
有了之前的对进程和线程对象的学习的铺垫后,我们现在可以开始学习windows下的进程创建过程了,我将尝试着从源代码的层次来分析在windows下创建一个进程都要涉及到哪些步骤,都要涉及到哪些数据结构. ...
- 64位CreateProcess逆向:(二)0环下参数的整合即创建进程的整体流程
转载:https://bbs.pediy.com/thread-207683.htm 点击下面进入总目录: 64位Windows创建64位进程逆向分析(总目录) 在上一篇文章中,我们介绍了Create ...
- Windows 进程的创建和终止
创建一个进程 总述 如图,创建一个进程主要分为两部分,用户态部分和内核部分. 既然我们想看看一个进程是怎么被创建的,那我们就用 WinDbg 来看看从用户态到内核态都调用了什么: 第一步:我们先看看 ...
- 理解Android进程创建流程(转)
/frameworks/base/core/java/com/android/internal/os/ - ZygoteInit.java - ZygoteConnection.java - Runt ...
- windows进程的创建方法
1.WinExec(LPCSTR lpCmdLine,UINT uCmdShow) >>参数: lpCmdLine:指定程序的相对路径或绝对路径,命令行参数 uCmdShow:指定窗口的显 ...
- php里进程创建和分析
pcntl_fork()函数创建一个子进程,这个子进程仅PID(进程号) 和PPID(父进程号)与其父进程不同成功时,在父进程执行线程内返回产生的子进程的PID,在子进程执行线程内返回0.失败时,在 ...
- Linux内核分析--进程创建,执行,切换
学号:351 原创作品转载请注明出处本实验来源 https://github.com/mengning/linuxkernel/ 实验要求 从整理上理解进程创建.可执行文件的加载和进程执行进程切换,重 ...
随机推荐
- 【ARC072E】Alice in linear land
题目 瑟瑟发抖,这竟然只是个蓝题 题意大概就是初始在\(0\),要到坐标为\(D\)的地方去,有\(n\)条指令,第\(i\)条为\(d_i\).当收到一条指令\(x\)后,如果向\(D\)方向走\( ...
- 请求参数MD5加密---函数助手
- pandas中axis的含义
定义一个dataframe: >>> df a b0 1 31 2 4 现在看两种用法: 1.求行的均值 >>> df.mean(axis=1)0 2.01 3.0 ...
- java OOP第03章_继承、抽象类和抽象方法
一. 为什么需要继承: 若多个类中都需要一些属性和方法,那么就可以将属性和方法抽取到一个父类中,需要的子类可以通过extends关键字去继承这个父类后拥有相应的属性和方法. 类.数组.方法----引用 ...
- Mysql优化-索引
1. 索引的本质 MySQL官方对索引的定义为:索引是帮助MySQL高效获取数据的数据结构. 数据库查询是数据库的最主要功能之一.我们都希望查询数据的速度尽可能的快,因此 数据库系统的设计者会从查询算 ...
- Ubuntu16.04下安装Visual Studio Code
sudo add-apt-repository ppa:ubuntu-desktop/ubuntu-make sudo apt-get update sudo apt-get install ubun ...
- ES5-call,apply,bind的用法
区别bind()与call()和apply()? 1. Function.prototype.bind(obj) : * 作用: 将函数内的this绑定为obj, 并将函数返回2. 面试题: 区别bi ...
- 【JZOJ6370】基础 fake 练习题
description 装饰者坐在树荫下听着长者讲述以前的故事: 大神 yk 非常喜欢树,便钦点班里的 n 个小蒟蒻站在一棵 n 个点以 1 为根的树上,并且每个点上恰好有 1 个小蒟蒻. 大神 yk ...
- redis和ehcache的区别,存储方式(各属于内存还是外存)
ehcache属于内存存储,redis的缓存属于内存存储,redis的持久文件属于外存存储: redis是通过socket访问到缓存服务,效率比ecache低,比数据库要快很多,处理集群和分布式缓存方 ...
- 基础数据类型补充 set集合 深浅拷贝
一.基础数据类型补充 1. "拼接字符串".join(可迭代对象) 可迭代对象为列表时,输出列表元素与拼接字符串的拼接 li = ['张三', '李四', '王五', '赵四'] ...