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 标志,代码如下:
经过屏蔽标志位后,判断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。
4. 继续判断dwCreationFlag的情况,首先在dwCreationFlag过滤掉表示优先级的标志位,然后在判断是什么创建标志。
6. 判断lpEnvironment是否为空, 如果不为空,将Ansi字符串转换为UNICODE_STRING, 为空的话跳过这一步,接着调用RtlDosPathNameToNtPathName_U函数将DOS路径转换为NT路径,由于用户给定的路径一般都是DOS路径,而内核需要的是NT路径,因此需要转换一下。
7. 接着调用NtOpenFile()得到文件句柄
接着通过NtOpenFile得到的handle接着调用了NtCreateSectiond函数得到内存区对象句柄
接着调用BasepIsProcessAllowed函数, 该函数用来判断应用程序名是否在授权文件列表中,函数实现调用了NtOpenKey函数打开了注册表中的[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\ CurrentVersion\Image File Execution Options键, 这个键跟以前流行的镜像劫持技术有关,但操作真正这么做的原因不是很清楚,希望知道的朋友交流一下。
在得到内存区对象句柄后调用了NtQuerySection函数,返回后得到节的基本信息(节基地址,大小,属性)
然后判断创建标志中是否包含DEBUG_PROCESS或者DEBUG_ONLY_THIS_PROCESS,如果包含该标志,判断PEB->ReadImageFileExecOptions域是否为0,如果不为0, 调用LdrQueryImageFileExecutionOptions函数查询该信息,如果不包含该标志,也用调用LdrQueryImageFileExecutionOptions函数。
下面检查镜像文件的部分信息的有效性:
接着调用函数BasepIsImageVersionOk判断镜像文件版本是否合法
然后加载advapi32.dll并获得CreateProcessAsUserSecure函数的地址:
获得函数CreateProcessAsUserSecure地址,不过后面没有发现调用这个函数。
然后调用BaseFormatObjectAttributes将安全属性结构格式为NT对象属性结构(得到了对象属性)。 接着调用了_DbgUiConnectToDbg在实现通过调用NtCreateDebugObject函数来创建调试对象,调用DbgUiGetThreadDebugObject来获得调试对象(作为参数传递到0环)。
最后调用NtCreateProcessEx函数完成3环的创建过程
下面附上CreateProcessW函数的流程图:
三. 0环创建进程的分析:
在NtCreateProcessEx中, 首先是判断父进程是否存在,如果不存在,就返回一个STATUS_INVALID_PARAMETER错误码(即无效参数),否则调用PspCreateProcess, 参数都没有变,直接传递给该函数。
1. 在PspCreateProcess函数中,首先保存当前线程运行的前一个模式(r0/r3), 通过KTHREAD->PreviousMode可以得到前一个模式。
接着判断创建标志是否包含除DEBUG_PROCESS, DEBUG_ONLY_THIS_PROCESS,CREATE_SUSPENDED的标志之外标志, 如果包含其他的标志,也报一个无效参数错误后退出该函数。
2. 通过参数ParentProcess调用ObReferenceObjectByHandle()函数得到父进程对象的指针)
接着判断了参数 JobMemberLevel是否为0, 如果为0,判断父进程的EPROCESS->Job是否为0,如果为0,返回无效参数错误后推出该函数。如果不为0,和JobMemberLevel不为同一流程,将父进程对象中的属性保存到局部变量中。
3. 通过ObCreateObject函数创建新进程对象并将对象内容初始化为0.
从父进程继承配额信息(PspInheritQuot函数完成)和设备位图信息(ObInheritDeviceMap函数完成),然后将父进程对象中的部分域给新进程。
4. 通过参数SectionHandle调用ObReferenceObjectByHandle函数得到区对象指针,当然前提是SectionHandle有效,然后将区对象指针赋值给新进程EPROCESS的相应域中,接着就判断参数DebugPort是否为0, 当不为0时:
当DebugPort为0时,调用DbgkCopyProcessDebugPort函数从父进程拷贝DebugPort给新进程。
以上部分就是通过参数来得到所需要的对象指针。
5. 接着调用PspInitializeProcessSecurity函数来设置新进程的安全属性, 主要是设置新进程的安全令牌对象(从父进程拷贝):
其中PspInitializeProcessSecurity函数里面调用了SeSubProcessToken函数来设置新进程对象的令牌对象,这个函数在XP sp3 下和win2k及WRK中不一样,函数原型为SeSubProcessToken(TOKEN ParentToken, TOKEN NewToken, BOOL MarkAsActive),MarkAsActive参数表示TOKEN是否为活动的,SeSubProcessToken函数比WRK中的少一个参数,比win2k的多一个参数。
6. 接着调用MmCreateProcessAddressSpace为新进程创建地址空间,并构建页目录表,页表 及物理页的关系。
之后调用KeInitializeProcess函数初始化新进程对象中内核对象,优先级,亲和性,页目录表物理地址帧号。
接着调用ObInitProcess函数来初始化新进程对象的表
7. 调用MmInitializeProcessAddressSpace函数初始化进程地址空间,这个函数WRK和XP sp3的不一样,XP sp3的只有4个参数,WRK中有5个参数, 去掉了CreateFlags参数,
该函数的实现中调用了KiAttachProcess函数来实现进程的切换(将当前线程挂靠到新进程中),以及初始化EPROCESS中的部分域和PFN,工作集列表等。
8. 调用PspMapSystemDll函数映射指定进程的区对象(映射第一个DLL):
该函数的实现中调用了MmMapViewOfSection映射节区(调用MiMapViewOfImageSection函数将DLL作为镜像映射)
通过MmGetSessionId函数获得指定进程的会话ID,调用SeSetSessionIdToken函数设置令牌的会话ID,接着调用ExCreateHandle函数在PspCidTable中添加一项。
9. 调用MmCreatePeb为新进程创建PEB(后面是大概是实现过程),首先通过调用KeAttachProcess函数将当前线程切换到新进程对象,然后NLS节区映射到新进程的地址空间中(MmMapViewOfSection),
接着调用MiCreatePebOrTeb创建PEB/TEB,(该函数通过ExAllocatePoolWithTag来申请_MMVAD结构大小的空间,通过MiFindEmptyAddressRangeDownTree函数在VAD树中查找一块未被使用的地址空间范围,并返回该范围的起始地址, 最后通过MiInsertVad函数将申请的地址空间插入到VAD树中。), 在创建PEB结构后,初始化PEB中部分域的值(镜像基地址,操作系统编译号等域),最后调用KeDetachProcess函数使线程回到原来的线程中。到此创建PEB完成。
9. 将新进程对象EPROCESS.ActiveProcessLinks更新为全局的活动进程链表(PsActiveProcessHead), 判断父进程是否为系统进程,调用SeCreateAccessStateEx设置访问状态,调用ObInsertObject函数将进程对象加入到进程对象的句柄表中,(省了几个函数介绍)KeQuerySystemTime()结束PspCreateProcess的调用。
在调用其他几个函数后完成PspCreateProcess 调用。
最后分析下KeAttachProcess:
KeAttachProcess的实现,KeAttachProcess函数实际上是对KiAttachProcess函数的封装,只是在该函数总作了些参数的检查,首先判断当前线程挂接的进程是否为当前进程,如果是退出该函数。接着 判断当前线程的APC与DPC状态是否为活动的, 如果存在上述的情况的话,会系统崩溃然后蓝屏, 我认为这应该为了防止在切换进程时被其他更高级的优先级抢占。
接着调用KeRaiseIrqlToDpcLevel函数将当前线程的IRQL提升为DISPATCH_LEVEL,然后将参数线程对象,进程对象,IRQL和ApcSaveState作为参数调用KiAttachProcess函数:
在KiAttachProcess函数中,首先调用KiMoveApcState保存当前线程的ApcState(ETHREAD->ApcSaveState = ETHREAD->ApcState), 以便在恢复切换进程时返回。
将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函数设置进程的优先级和默认处理模式.
接着调用了BasepSxsCreateProcessCsrMessage函数,该函数的具体作用不是很清楚,不过实现中调用了BasepSxsGetProcessImageBaseAddress函数来获得进程的PEB, 然后通过NtReadVirtualMemory()取得ImageBase. 然后调用BasePushProcessParameters函数, 其中实现中调用了RtlCreateProcessParameters来创建进程参数, 该函数对RTL_USER_PROCESS_PARAMETERS结构中的字符串域的地址改为相对的偏移量。
接下来要做的就是创建线程, 不过在在此之前还要构建线程的环境,调用BaseCreateStack函数创建栈:
接着调用BaseInitializeContext初始化线程上下文, 然后调用BaseFormatObjectAttributes函数格式化对象(以便传递给NtCreateThread)
在BaseInitializeContext函数的实现中会判断参数上下文类型是否为1,如果不为1, 指定用户空间的线程启动函数为BaseProcessStartThunk(进程的第一个线程调用这个),否则为BaseThreadStartThunk(普通线程),然后CALL OEP。其中BaseProcessStartThunk函数会调用BaseProcessStart,这个函数就是调用OEP所在函数的上层函数:
用OD打开应用程序,停在OEP处, 然后看栈窗口, 将栈顶值(即ESP)的值所在的地址反汇编跟随一下:
最后调用NtCreateThread创建线程:
到此3环上的CreateThread就完成了, 流程图结合在0环一起给出。
五. 0环创建线程的分析:
0环中NtCreateThread函数直接调用pspCreateThread函数:
在pspCreateThread函数中,首先检查参数StartRoutine(系统线程)是否为0, 如果为0, 将PreviousMode值为1, 否则值为0。接着调用ObCreateObject函数创建线程对象并初始化为0.
接着将新进程中EPROCESS-> UniqueProcessId拷贝给线程对象ETHREAD-> UniqueProcess, 然后调用ExCreateHandle函数往CID表中添加线程对象。
接着初始化信号量对象和自旋锁对象,之后判断参数进程对象指针ProcessPointer,是否为0,如果不为0,调用函数MmCreateTeb创建TEB.
如果ProcessPointer为0,KeInitThread初始化线程时线程函数为PspUserThreadStartup(用户线程启动函数), 否则为用户线程启动函数:
该函数的实现中主要做了如下操作:创建内核线程栈然后调用KiInitializeContextThread初始化线程上下文
在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,
切换到内核后作部分操作后,再次回到3环,调用用户空间的线程入口函数BaseProcessStartThunk, 该函数在3环中BaseInitializeContext中有指定。
线程创建完成后调用CsrClientCallServer通知CRSS 线程创建成功,其中调用NtRequestWaitReplyPort()来等待回应,最后调用NtResumeThread()函数恢复线程的执行。
在用户空间线程启动时调用BaseProcessStartThunk, 该函数调用了BaseProcessStart函数, 这部分代码在上面已经贴出来了。
附上线程创建的流程图:
到此, 程序分析完毕,谢谢大家看完。
创建进程的过程就是构建一个环境,这个环境包含了很多的机制 (比如自我保护, 与外界通信等等)。 构建这个环境需要两种“人”来协调完成(用户态和内核态),他们各有分工,其中用户态提供原料(提供创建的那些参数), 内核态负责来构建这个环境,由于环境是由内核态构建的,因此他持有这个环境的控制权, 而用户由于提供了原料, 因此他具有使用权。 内核态开始构建环境中的基础设施(进程对象,等等),在构建完基础设施后,内核态通知用户态基础设施构建已经完成,是否需要继续构建其他设施,于是用户态通知内核态继续构建一条通道(既创建线程),方便两边的通信,当用户态接收到线程创建完毕的信息后,便可以开始使用这个环境(投入生产),以后缺啥补啥。
二. 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 ;如果相等,说明创建标志不合法
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
代码:
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
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)
代码:
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)
在得到内存区对象句柄后调用了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 ; 将区对象作为镜像执行文件来查询信息
下面检查镜像文件的部分信息的有效性:
代码:
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
代码:
movzx eax, [ebp+pImageFileMinorVersion] ; 子系统次版本号 push eax movzx eax, [ebp+pImageFileMajorVersion] ; phSection->GpValue push eax ; 子系统的主版本号 call _BasepIsImageVersionOk@8 ; 判断镜像文件的版本是否合法。
代码:
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 ; 获得指定模块中指定函数的地址
然后调用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
代码:
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)
三. 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
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 ; 通过句柄得到父进程对象指针
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
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 ; 通过调试对象句柄得到调试对象指针
以上部分就是通过参数来得到所需要的对象指针。
5. 接着调用PspInitializeProcessSecurity函数来设置新进程的安全属性, 主要是设置新进程的安全令牌对象(从父进程拷贝):
代码:
push ebx ; 新进程对象 push [ebp+ParentObj] ; 父进程对象 call _PspInitializeProcessSecurity@8 ; 初始进程的安全属性(设置新进程对象的令牌对象)
代码:
push 1 ; TOKEN是否为活动 lea eax, [ebp+8] push eax ; eax指向父进程对象的指针 push edi ; 父进程对象中的令牌对象 call _SeSubProcessToken@12 ; 将父进程的令牌对象赋值给新进程对象
代码:
lea eax, [ebp+pDirTableBase] push eax ; 页目录表基地指针 push ebx ; 新进程对象 push [ebp+MinimumWorkingSet] ; 最小工作集 call _MmCreateProcessAddressSpace@12 ; 创建进程地址空间(并构建页目录表,页表 及物理页的关系)
代码:
push eax lea eax, [ebp+pDirTableBase] push eax ; 页目录表基地址 push [ebp+Affinity] ; 亲和性 push 8 ; 进程基本优先级 push ebx ; 新进程对象 call _KeInitializeProcess@20 ;
代码:
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)
该函数的实现中调用了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个参数
代码:
push esi ; 需要映射DLL的基地址 push ebx ; 新进程对象 call _PspMapSystemDll@8 ; 映射新进程对象的系统DLL(NTDLL.DLL)
代码:
lea eax, [ebp+pMapAddress] push eax ; 映射到那个地址 push [ebp+pNewObj] ; 进程对象(映射到那个进程) push ebx ; 系统DLL映射节的地址 call _MmMapViewOfSection@40 ; 映射节区
代码:
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中添加一项
接着调用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节, 在该文件的那个节区,返回该节区对象指针。
代码:
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 ; 插入一个对象到进程的句柄表,并返回该对象的句柄值
最后分析下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状态是否为活动
代码:
push eax push [ebp+pNewProcessObj] push edi ; 进程对象 push esi ; 线程对象 call _KiAttachProcess@16 ; 将当前线程附加到目标进程的地址空间中
代码:
lea edi, [esi+_KTHREAD.ApcState.ApcListHead.Flink] push edi ;原始APC call _KiMoveApcState@8 ; 保存当前线程的APC状态到ApcSaveState
最后附上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 ; 设置进程信息
接下来要做的就是创建线程, 不过在在此之前还要构建线程的环境,调用BaseCreateStack函数创建栈:
代码:
lea ecx, [ebp+InitialTeb] push ecx ; 初始TEB push eax ; 栈的最大值 push [ebp+StackSize] ; 栈大小 push [ebp+pHandleOfProcess] ; 进程句柄 call _BaseCreateStack@16 ; 创建栈
代码:
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
代码:
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)
最后调用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
五. 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
代码:
lea eax, [ebp+pEthread] 50 push eax ; 线程对象 push _PspCidTable ; CID表指针 call _ExCreateHandle@8 ; 在指定的句柄表中添加一个表项
代码:
lea eax, [ebp+pTeb] push eax ; 指向TEB指针(传出参数) push edi ; 客户端ID结构的指针 push [ebp+InitTeb] ; 初始TEB push ebx ; 目标进程对象 call _MmCreateTeb@16 ; 创建TEB
代码:
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, 信号量对象,创建内核线程栈 ;初始化线程上下文)
代码:
push [ebp+pContext] ; pContext 上下文记录 push [ebp+pContextStart] ; 启动线程函数上下文 push [ebp+pThreadStart] ; 启动线程函数地址 push [ebp+pKernelFunStart] ; 系统线程函数 push esi ; 内核线程对象(KTHREAD) call _KiInitializeContextThread@20
然后调用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后,调用该函数继续返回到内核
线程创建完成后调用CsrClientCallServer通知CRSS 线程创建成功,其中调用NtRequestWaitReplyPort()来等待回应,最后调用NtResumeThread()函数恢复线程的执行。
在用户空间线程启动时调用BaseProcessStartThunk, 该函数调用了BaseProcessStart函数, 这部分代码在上面已经贴出来了。
附上线程创建的流程图:
到此, 程序分析完毕,谢谢大家看完。