设为首页收藏本站喵玉殿官方微博

 找回密码
 少女注册中
搜索
查看: 7291|回复: 8

[编程算法] 一个进程的诞生(大补)

[复制链接]
发表于 2010-12-31 17:11:35 | 显示全部楼层 |阅读模式
本帖最后由 十二 于 2010-12-31 17:50 编辑

当你双击一个图标的时候Shell会调用CreateProcess来创建一个进程对象,这个函数被调用,系统会创建一个“进程内核对象”。

但实际创建进程对象的并不是它,MS喜欢玩捉迷藏,在ntdll中导出了两个在Ring3下类似的函数名分别以Zw和Nt开头。Zw在R3下只做了部分处理。真正的创建者是NtCreateProcess。此时进程内核对象的初始使用计数为1。然后系统为该进程创建4GB虚拟线性可寻址范围。也就是DS选择子在GDT中所指定的地址范围。这些用于加载PE文件和任何必要的dll文件的数据和代码。
在32位x86系统上:
从0x00000000到0x7fffffff的空间中存放着 应用程序代码,全局变量,每个线程堆栈,dll代码。
从0x80000000到0xc0000000的空间中存放着 内核和执行体,HAL(硬件抽象层),引导驱动程序。
从0xc0000000到0xc0800000的空间中存放着 进程页表和超空间。
从0xc0800000到0xffffffff的空间中存放着 系统高速缓存,分页缓冲池,非分页缓冲池。
然后CreateProcess把section区段映射到RAM中,加载所需要的DLL函数。

然后用MmCreatePeb创建进程的PEB信息也就是保存在FS:[30]中的PE信息段


进程加载代码和数据完毕后,就开始创建线程来执行进程空间内的代码。进程是静态的,它只是线程的容器。一个进程至少该有一个线程(main thread),其它线程都是主线程通过调用CreateThread函数创建的。线程也是核心对象,他的实际创建者是一个叫NtCreateThread的函数。一个线程其实只是一个线程核心对象和两个栈(一个核心栈,用于线程运行在核心态;一个用户栈,用于线程运行在用户态),线程与进程类似,也拥有线程核心对象计数和线程句柄,然后PE跳到入口执行OEP。



粗略的PE启动过程实际中比这更复杂也更为详细。 在PE启动的过程中能够窥探Windows对EXE文件的处理方式和对系统更深一步的了解。
 楼主| 发表于 2010-12-31 17:36:20 | 显示全部楼层
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 标志,代码如下:
   
 
  1. mov   eax, [ebp+20h]        ;  ebp+20h = dwCreationFlags
  2.    and   eax, 0F7FFFFFFh       ; 屏蔽CREATE_NO_WINDOW标志。
  3.    mov   [ebp+20h], eax
  4.    mov   ecx, eax
  5.    and   ecx, 18h    ;18h = DETACHED_PROCESS | CREATE_NEW_CONSOLE
  6.    cmp   cl, 18h
  7.    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。

  1. mov   eax, [ebp+20h]        ;  ebp+20h = dwCreationFlags
  2. and   eax, 0F7FFFFFFh       ; 屏蔽CREATE_NO_WINDOW标志
  3. mov   [ebp+20h], eax
  4. mov   ecx, eax
  5. and   ecx, 18h
  6. cmp   cl, 18h  ; 判断dwCreationFlags 是否为CREATE_NEW_CONSOLE |DETACHED_PROCESS
  7. jz    loc_7C8427DE          ; 标志组合不合法, 进行错误处理并退出
  8. mov   [ebp+var_6D4], ebx
  9. mov   [ebp+var_6DC], ebx
  10. test  al, 40h
  11. jnz   IsIdlePriority        ; 优先级为IDLE_PRIORITY_CLASS
  12. test  ah, 40h
  13. jnz   loc_7C8427F6
  14. test  al, 20h
  15. jnz   IsNormalPriority      ; 优先级为NORMAL_PRIORITY_CLASS
  16. test  ah, ah
  17. js    loc_7C842802
  18. test  al, al
  19. js    IsHighPriotity        ; 优先级为HIGH_PRIORITY_CLASS
  20. test  ah, 1
  21. jnz   IsRealTimePriority    ; 优先级为REALTIME_PRIORITY_CLASS
复制代码

4.  继续判断dwCreationFlag的情况,首先在dwCreationFlag过滤掉表示优先级的标志位,然后在判断是什么创建标志。

  1. mov   [ebp+var_668], bl
  2. and   word ptr [ebp+dwCreateFlag], 3E1Fh ; 屏蔽权限级表示的位
  3. mov   edi, 800h
  4. mov   esi, 1000h
  5. test  [ebp+dwCreateFlag], edi
  6. jnz   loc_7C842832          ; dwCreationFlag = CREATE_SEPARATE_WOW_VDM
  7. test  [ebp+dwCreateFlag], esi
  8. jnz   short loc_7C819A33    ; dwCreationFlag = CREATE_SHARED_WOW_VDM
  9. mov   eax, _BaseStaticServerData
  10. cmp   [eax+19F4h], bl
  11. jnz   loc_7C84283C
复制代码

6. 判断lpEnvironment是否为空, 如果不为空,将Ansi字符串转换为UNICODE_STRING, 为空的话跳过这一步,接着调用RtlDosPathNameToNtPathName_U函数将DOS路径转换为NT路径,由于用户给定的路径一般都是DOS路径,而内核需要的是NT路径,因此需要转换一下。

7.  接着调用NtOpenFile()得到文件句柄

  1. push  60h                   ;打开选项
  2. push  5                       ;共享模式
  3. lea   eax, [ebp+pIoStatusBlock]
  4. push  eax                   ; I/0状态块
  5. lea   eax, [ebp+pObjAttribute]  
  6. push  eax                   ; 对象属性
  7. push  1000A1h   
  8.                         ;SYNCHRONIZE |FILE_ATTRIBUTE_NORMAL|
  9.                         ;FILE_ATTRIBUTE_ARCHIVE|FILE_ATTRIBUTE_READONLY
  10. lea   eax, [ebp+pHandleOfFile]
  11. push  eax                   ; 文件句柄  
  12. mov   esi, ds:__imp__NtOpenFile@24 ; NtOpenFile(x,x,x,x,x,x)
  13. call  esi ; NtOpenFile(x,x,x,x,x,x) ; NtOpenFile(x,x,x,x,x,x)
复制代码

接着通过NtOpenFile得到的handle接着调用了NtCreateSectiond函数得到内存区对象句柄

  1. push  [ebp+pHandleOfFile]   ; 文件句柄
  2. push  1000000h
  3. push  10h                   ; 内存区页面保护属性
  4. push  ebx                   ; SETION大小
  5. push  ebx                   ; 对象属性
  6. push  0F001Fh               ; 访问掩码
  7. lea   eax, [ebp+pSectionHandle]
  8. push  eax                   ; 指向内存区对象的指针(传出参数)
  9. 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函数,返回后得到节的基本信息(节基地址,大小,属性)

  1. push  ebx                   ; 接受返回的大小
  2. push  30h                   ; 内存区信息的长度
  3. lea   eax, [ebp+OepAddress]
  4. push  eax                   ; 接受内存区信息Buffer
  5. xor   edi, edi
  6. inc   edi
  7. push  edi                   ; edi = 1 = SectionImageInformation
  8. push  [ebp+pSectionHandle]  ; 内存区句柄
  9. call  ds:__imp__NtQuerySection@20 ; 将区对象作为镜像执行文件来查询信息
复制代码

然后判断创建标志中是否包含DEBUG_PROCESS或者DEBUG_ONLY_THIS_PROCESS,如果包含该标志,判断PEB->ReadImageFileExecOptions域是否为0,如果不为0, 调用LdrQueryImageFileExecutionOptions函数查询该信息,如果不包含该标志,也用调用LdrQueryImageFileExecutionOptions函数。


下面检查镜像文件的部分信息的有效性:

  1. mov   ax, [ebp+MachineType]     ;  MachineType为机器类型,
  2. cmp   ax, ds:7FFE002Ch          ; ds:7FFE002Ch中的内容为0x014c
  3. jb    loc_7C84329E
  4. cmp   ax, ds:7FFE002Eh
  5. ja    loc_7C84329E
  6. cmp   dword ptr [ebp+ SubSystemVersion], 2
  7. jz    short loc_7C8191C4    ; 子系统次版本号
  8. cmp   dword ptr [ebp+ SubSystemVersion], 3 ; 子系统的比较版本(PE信息中包含)
  9. jnz   loc_7C842D0C
复制代码

接着调用函数BasepIsImageVersionOk判断镜像文件版本是否合法

  1. movzx eax, [ebp+pImageFileMinorVersion] ; 子系统次版本号
  2. push  eax
  3. movzx eax, [ebp+pImageFileMajorVersion] ; phSection->GpValue
  4. push  eax                   ; 子系统的主版本号
  5. call  _BasepIsImageVersionOk@8 ; 判断镜像文件的版本是否合法。
复制代码

然后加载advapi32.dll并获得CreateProcessAsUserSecure函数的地址:

  1. push  offset LibFileName    ; "advapi32.dll"
  2. call  _LoadLibraryA@4       ; LoadLibraryA(x)
  3. mov   esi, eax
  4. mov   [ebp+phModuleOfAdvapi32.dll], esi ; 保存模块基地址
  5. cmp   esi, ebx
  6. jz    short loc_7C81923C
  7. push  offset ProcName       ; "CreateProcessAsUserSecure"
  8. push  esi                            ; hModule
  9. call  _GetProcAddress@8     ; 获得指定模块中指定函数的地址
复制代码

获得函数CreateProcessAsUserSecure地址,不过后面没有发现调用这个函数。

然后调用BaseFormatObjectAttributes将安全属性结构格式为NT对象属性结构(得到了对象属性)。 接着调用了_DbgUiConnectToDbg在实现通过调用NtCreateDebugObject函数来创建调试对象,调用DbgUiGetThreadDebugObject来获得调试对象(作为参数传递到0环)。

  1. call  _DbgUiConnectToDbg@0  ; 实现调用中NtCreateDebugObject
  2. mov   [ebp+nReturnStatus], eax
  3. cmp   eax, ebx
  4. jl    loc_7C82DF58
  5. call  _DbgUiGetThreadDebugObject@0 ; 获得线程调试对象teb->0xF24,
  6. mov   [ebp+phandleOfDebugObj], eax
复制代码

最后调用NtCreateProcessEx函数完成3环的创建过程

  1. push  [ebp+JobLevel]                  ; 作业级别
  2. push  ebx                                    ; 异常端口对象句柄
  3. push  [ebp+phandleOfDebugObj]   ; 调试对象句柄
  4. push  [ebp+pSectionHandle]      ; 内存区对象句柄
  5. push  [ebp+CreateFlag]             ; 创建标志
  6. or    esi, 0FFFFFFFFh
  7. push  esi                                    ; 父进程句柄(FFFFFFFF)
  8. push  [ebp+ObjAttibute]           ; 对象属性
  9. push  1F0FFFh                            ; (访问掩码)
  10. lea   eax, [ebp+pHandleOfProcess]
  11. push  eax                                 ; 保留进程句柄值(传出参数)
  12. call  ds:__imp__NtCreateProcessEx@36 ; 创建进程(x,x,x,x,x,x,x,x,x)
复制代码
回复

使用道具 举报

 楼主| 发表于 2010-12-31 17:42:30 | 显示全部楼层
0环创建进程的分析:


函数原型:
NTSTATUS
NtCreateProcessEx(
    __out PHANDLE ProcessHandle,
    __in ACCESS_MASK DesiredAccess,
    __in_opt POBJECT_ATTRIBUTES ObjectAttributes,
    __in HANDLE ParentProcess,
    __in ULONG Flags,
    __in_opt HANDLE SectionHandle,
    __in_opt HANDLE DebugPort,
    __in_opt HANDLE ExceptionPort,
    __in ULONG JobMemberLevel
);

函数主要功能:
  函数大致操作分三部分:
1.  创建并初始化进程EPROCESS对象,
2.  创建并初始化进程地址空间,
3.  初始化内核进程块(KPROCESS)
4.  创建进程ID,创建PEB等操作。

函数参数:
  ProcessHandle,输出参数,如果创建成功,则它返回所创建的进程的句柄。
  DesiredAccess, 新进程的访问权限。
  ObjectAttributes,可选参数,指定了新进程的对象属性。
ParentProcess:新进程的父进程句柄。如果这个参数没有设定,即新进程没有父进程,新进程使用系统地址空间创建。
Flags :进程创建的标志。
SectionHandle :内存区域映射句柄,用来创建进程的地址空间,如果这个参数没有设定,新进程的地址空间是一个简单的克隆父进程的地址空间。
DebugPort : 一个端口对象的句柄,被用于进程的调试端口。
ExceptionPort :一个端口对象的句柄,被用于进程的异常端口。
JobMemberLevel :新进程的在 jobset 中的等级。
     
      在NtCreateProcessEx中, 首先是判断父进程是否存在,如果不存在,就返回一个STATUS_INVALID_PARAMETER错误码(即无效参数),否则调用PspCreateProcess, 参数都没有变,直接传递给该函数。

1.  在PspCreateProcess函数中,首先保存当前线程运行的前一个模式(r0/r3), 通过KTHREAD->PreviousMode可以得到前一个模式。

  1. mov     eax, large fs:124h    ; eax保存了指向KTHREAD结构的指针
  2. mov     [ebp+pKthread], eax
  3. mov    cl, [eax+_KTHREAD.PreviousMode] ; 线程的前一个模式
  4. mov     [ebp+PreviousMode], cl
复制代码

接着判断创建标志是否包含除DEBUG_PROCESS, DEBUG_ONLY_THIS_PROCESS,CREATE_SUSPENDED的标志之外标志, 如果包含其他的标志,也报一个无效参数错误后退出该函数。

2.  通过参数ParentProcess调用ObReferenceObjectByHandle()函数得到父进程对象的指针)

  1. push    esi                   ; HandleInformation
  2. lea  eax, [ebp+pParentObject] ; eax 为EPROCESS局部变量的地址(作传出参数)
  3. push    eax                   ; Object
  4. push    dword ptr [ebp+PreviousMode] ; AccessMode
  5. push    _PsProcessType        ; ObjectType
  6. push    80h                   ; DesiredAccess
  7. push    [ebp+ParentProcessHandle] ; Handle
  8. call    _ObReferenceObjectByHandle@24 ; 通过句柄得到父进程对象指针
复制代码

接着判断了参数 JobMemberLevel是否为0, 如果为0,判断父进程的EPROCESS->Job是否为0,如果为0,返回无效参数错误后推出该函数。如果不为0,和JobMemberLevel不为同一流程,将父进程对象中的属性保存到局部变量中。

3.  通过ObCreateObject函数创建新进程对象并将对象内容初始化为0.

  1. lea     eax, [ebp+pNewProcessObj]
  2. push    eax                   ; 新对象指针
  3. push    esi                   ; int
  4. push    esi                   ; 对象的大小
  5. push    260h                  ; paseContext
  6. push    esi                   ; int
  7. push    dword ptr [ebp+PreviousMode] ; int
  8. push    [ebp+ObjectAttributes] ; 对象属性
  9. push    _PsProcessType        ; 对象类型
  10. push    dword ptr [ebp+PreviousMode] ; 处理器模式
  11. call    _ObCreateObject@36    ; 创建进程对象
  12. mov     edi, eax
  13. cmp     edi, esi
  14. jl      loc_4AAB6B            ; 判断ObCreateObject函数调用是否成功
  15. mov     ecx, 98h
  16. xor     eax, eax
  17. mov     ebx, [ebp+pNewProcessObj]
  18. mov     edi, ebx
  19. rep stosd                     ; 将新建对象初始化为0
复制代码

从父进程继承配额信息(PspInheritQuot函数完成)和设备位图信息(ObInheritDeviceMap函数完成),然后将父进程对象中的部分域给新进程。

4.  通过参数SectionHandle调用ObReferenceObjectByHandle函数得到区对象指针,当然前提是SectionHandle有效,然后将区对象指针赋值给新进程EPROCESS的相应域中,接着就判断参数DebugPort是否为0, 当不为0时:

  1. cmp     [ebp+SectionHandle], esi
  2. jz      loc_4F449F            ; 判断区对象句柄是否为0
  3. push    esi                   ; HandleInformation
  4. eax, [ebp+pSectionObj]
  5. push    eax                   ; 内存区对象指针
  6. push    dword ptr [ebp+PreviousMode] ; 处理器模式
  7. push    _MmSectionObjectType  ; 对象类型
  8. push    8                     ; 访问掩码
  9. push    [ebp+SectionHandle]   ; 内存去句柄
  10. call    _ObReferenceObjectByHandle@24 ; 通过区对象句柄得到区对象指针

  11. mov     eax, [ebp+HandleInformation]
  12. mov     [ebx+_EPROCESS.SectionObject], eax ; pNewProcessObject->SectionObject = 内存区对象指针
  13. cmp     [ebp+DebugPort], esi
  14. jnz     IsDebugger            ; 判断参数DebugPort是否为0

  15. IsDebugger:(DebugPort不为0时)
  16. push    esi                   ; HandleInformation
  17. lea     eax, [ebp+pDebugObj]
  18. push    eax                   ; 新调试对象指针
  19. push    dword ptr [ebp+PreviousMode] ; 处理器模式
  20. push    _DbgkDebugObjectType  ; 对象类型
  21. push    2                     ; 访问模式
  22. push    [ebp+DebugPort]       ; 调试端口句柄
  23. call    _ObReferenceObjectByHandle@24 ; 通过调试对象句柄得到调试对象指针
复制代码

当DebugPort为0时,调用DbgkCopyProcessDebugPort函数从父进程拷贝DebugPort给新进程。

以上部分就是通过参数来得到所需要的对象指针。

5.  接着调用PspInitializeProcessSecurity函数来设置新进程的安全属性, 主要是设置新进程的安全令牌对象(从父进程拷贝):

  1. push    ebx                   ; 新进程对象
  2. push    [ebp+ParentObj]       ; 父进程对象
  3. call    _PspInitializeProcessSecurity@8 ; 初始进程的安全属性(设置新进程对象的令牌对象)
复制代码

其中PspInitializeProcessSecurity函数里面调用了SeSubProcessToken函数来设置新进程对象的令牌对象,这个函数在XP sp3 下和win2k及WRK中不一样,函数原型为SeSubProcessToken(TOKEN ParentToken, TOKEN NewToken, BOOL MarkAsActive),MarkAsActive参数表示TOKEN是否为活动的,SeSubProcessToken函数比WRK中的少一个参数,比win2k的多一个参数。

  1. push    1                     ; TOKEN是否为活动
  2. lea     eax, [ebp+8]
  3. push    eax                   ; eax指向父进程对象的指针
  4. push    edi                   ; 父进程对象中的令牌对象
  5. call    _SeSubProcessToken@12 ; 将父进程的令牌对象赋值给新进程对象
复制代码

6.     接着调用MmCreateProcessAddressSpace为新进程创建地址空间,并构建页目录表,页表 及物理页的关系。

  1. lea     eax, [ebp+pDirTableBase]
  2. push    eax                   ; 页目录表基地指针
  3. push    ebx                   ; 新进程对象
  4. push    [ebp+MinimumWorkingSet] ; 最小工作集
  5. call    _MmCreateProcessAddressSpace@12 ; 创建进程地址空间(并构建页目录表,页表 及物理页的关系)
复制代码

之后调用KeInitializeProcess函数初始化新进程对象中内核对象,优先级,亲和性,页目录表物理地址帧号。

  1. push    eax
  2. lea     eax, [ebp+pDirTableBase]
  3. push    eax                   ; 页目录表基地址
  4. push    [ebp+Affinity]        ; 亲和性
  5. push    8                     ; 进程基本优先级
  6. push    ebx                   ; 新进程对象
  7. call    _KeInitializeProcess@20 ;
复制代码

接着调用ObInitProcess函数来初始化新进程对象的表

  1. push    ebx                   ; 新进程对象
  2. mov     eax, [ebp+CreateFlag]
  3. and     al, 4
  4. neg     al
  5. sbb     eax, eax
  6. and     eax, [ebp+ParentObj]
  7. push    eax                   ; 父进程对象
  8. call    _ObInitProcess@8      ; 初始化新进程对象的对象表(如果父进程被指定,父进程的对象表拷贝到新进程中, 对象表 的每个对象中HandleCount域都+1)
复制代码

7.  调用MmInitializeProcessAddressSpace函数初始化进程地址空间,这个函数WRK和XP sp3的不一样,XP sp3的只有4个参数,WRK中有5个参数, 去掉了CreateFlags参数,
该函数的实现中调用了KiAttachProcess函数来实现进程的切换(将当前线程挂靠到新进程中),以及初始化EPROCESS中的部分域和PFN,工作集列表等。

  1. lea     eax, [ebx+_EPROCESS.SeAuditProcessCreationInfo]
  2. push    eax               ; 对象名称信息
  3. push    [ebp+HandleInformation] ; 映射节信息
  4. push    esi                   ; 克隆进程
  5. push    ebx                   ; 需要被初始化的新进程对象
  6. call    _MmInitializeProcessAddressSpace@16 ; 这个函数WRK和XP的不一样,XP的只有4个参数,WRK中有5个参数
复制代码

8.  调用PspMapSystemDll函数映射指定进程的区对象(映射第一个DLL):

  1. push    esi                   ; 需要映射DLL的基地址
  2. push    ebx                   ; 新进程对象
  3. call    _PspMapSystemDll@8    ; 映射新进程对象的系统DLL(NTDLL.DLL)
复制代码

该函数的实现中调用了MmMapViewOfSection映射节区(调用MiMapViewOfImageSection函数将DLL作为镜像映射)

  1. lea     eax, [ebp+pMapAddress]
  2. push    eax                   ; 映射到那个地址
  3. push    [ebp+pNewObj]         ; 进程对象(映射到那个进程)
  4. push    ebx                   ; 系统DLL映射节的地址
  5. call    _MmMapViewOfSection@40 ; 映射节区
复制代码

通过MmGetSessionId函数获得指定进程的会话ID,调用SeSetSessionIdToken函数设置令牌的会话ID,接着调用ExCreateHandle函数在PspCidTable中添加一项。

  1. mov     edi, [ebx+_EPROCESS.Token] ; 取得pNewProcessObject->Token
  2. and     edi, 0FFFFFFF8h       ; 取消Token中的低3位
  3. push    ebx                   ; 新进程对象
  4. call    _MmGetSessionId@4     ; 返回指定进程的会话ID
  5. push    eax                   ; 新进程对象的会话ID
  6. push    edi                   ; 新进程令牌对象
  7. call    _SeSetSessionIdToken@8 ; 设置指定Token->SessionId
  8. mov     [ebp+var_70], ebx
  9. mov     [ebp+var_6C], esi
  10. lea     eax, [ebp+var_70]
  11. push    eax                   ; eax = pNewProcessObject
  12. push    _PspCidTable          ; 该表保存了所有进程和线程对象的指针
  13. call    _ExCreateHandle@8     ; 在PspCidTable中添加一项
复制代码

9.  调用MmCreatePeb为新进程创建PEB(后面是大概是实现过程),首先通过调用KeAttachProcess函数将当前线程切换到新进程对象,然后NLS节区映射到新进程的地址空间中(MmMapViewOfSection),
接着调用MiCreatePebOrTeb创建PEB/TEB,(该函数通过ExAllocatePoolWithTag来申请_MMVAD结构大小的空间,通过MiFindEmptyAddressRangeDownTree函数在VAD树中查找一块未被使用的地址空间范围,并返回该范围的起始地址, 最后通过MiInsertVad函数将申请的地址空间插入到VAD树中。), 在创建PEB结构后,初始化PEB中部分域的值(镜像基地址,操作系统编译号等域),最后调用KeDetachProcess函数使线程回到原来的线程中。到此创建PEB完成。

  1. lea     eax, [ebx+_EPROCESS.Peb]
  2. push    eax                   ; 指向PEB结构的指针(传出参数)
  3. lea     eax, [ebp+pInitPeb]
  4. push    eax                   ; 初始PEB
  5. push    ebx                   ; 新进程对象
  6. call    MmCreatePeb@12; 创建PEB(将线程切换到目标进程后,创建PEB结构及初始化部分域,
  7.                                    
  8. mov     esi, [ebp+pNewObj]
  9. push    esi                   ; 新进程对象(附加到那个进程)
  10. call    _KeAttachProcess@4    ; KeAttachProcess(x)

  11. lea     eax, [ebp+pPeb]
  12. push    eax                   ; 指向PEB 对象的地址(传出参数)
  13. push    210h                  ; 创建PEB/TEB的大小
  14. push    esi                   ; 进程对象结构EPROCESS的指针
  15. call    _MiCreatePebOrTeb@12  ; 创建

  16. lea     eax, [ebp+Size]
  17. push    eax                   ; 数据目录大小
  18. push    0Ah                   ; 目录表项索引
  19. push    1                     ; 如果不为0, 作为镜像映射
  20. mov     eax, [ebp+pPeb]
  21. push    dword ptr [eax+8]     ; ImageBase(镜像文件或者数据文件的基地址)
  22. call  _RtlImageDirectoryEntryToData@16 ; 将一个镜像文件中IMAGE_DIRECTORY_LOAD_CONFIG节, 在该文件的那个节区,返回该节区对象指针。
复制代码

9.  将新进程对象EPROCESS.ActiveProcessLinks更新为全局的活动进程链表(PsActiveProcessHead), 判断父进程是否为系统进程,调用SeCreateAccessStateEx设置访问状态,调用ObInsertObject函数将进程对象加入到进程对象的句柄表中,(省了几个函数介绍)KeQuerySystemTime()结束PspCreateProcess的调用。

  1. mov     ecx, _PsProcessType
  2. add     ecx, 68h
  3. push    ecx                   ; GenericMapping
  4. push    [ebp+AccessMask]      ; 访问掩码
  5. lea     ecx, [ebp+var_B8]
  6. push    ecx                   ; 访问辅助结构指针PAUX_ACCESS_DATA
  7. lea     ecx, [ebp+var_12C]
  8. push    ecx                   ; 访问状态(传出参数)
  9. push    eax                   ; 进程对象
  10. push    esi                   ; 线程对象
  11. call    _SeCreateAccessStateEx@24

  12. lea     eax, [ebp+pHandle]
  13. push    eax                   ; 接受返回新对象的句柄
  14. push    esi                   ; 接受返回新对象的的指针
  15. push    1                     ; 对象指针计数
  16. push    [ebp+AccessMask]      ; 访问掩码
  17. lea     eax, [ebp+AccessState]
  18. push    eax                   ; 访问状态
  19. push    ebx                   ; 新进程对象
  20. call    _ObInsertObject@24    ; 插入一个对象到进程的句柄表,并返回该对象的句柄值
  21. 在调用其他几个函数后完成PspCreateProcess 调用。
复制代码


最后分析下KeAttachProcess:

KeAttachProcess的实现,KeAttachProcess函数实际上是对KiAttachProcess函数的封装,只是在该函数总作了些参数的检查,首先判断当前线程挂接的进程是否为当前进程,如果是退出该函数。接着 判断当前线程的APC与DPC状态是否为活动的, 如果存在上述的情况的话,会系统崩溃然后蓝屏, 我认为这应该为了防止在切换进程时被其他更高级的优先级抢占。

  1. mov     eax, large fs:124h
  2. mov     edi, [ebp+pNewProcessObj]
  3. mov     esi, eax              ; KTHREAD
  4. cmp     [esi+_KTHREAD.ApcState.Process], edi ; 判断是否为当前进程
  5. jz      short Exit
  6. cmp     [esi+_KTHREAD.ApcStateIndex], 0
  7. jnz     loc_445F94            ; 判断APC是否为活动
  8. mov     eax, large fs:994h    ; _KPRCB->DpcRoutineActive
  9. test    eax, eax
  10. jnz     loc_445F94            ; 判断DPC状态是否为活动
复制代码

接着调用KeRaiseIrqlToDpcLevel函数将当前线程的IRQL提升为DISPATCH_LEVEL,然后将参数线程对象,进程对象,IRQL和ApcSaveState作为参数调用KiAttachProcess函数:

  1. push    eax
  2. push    [ebp+pNewProcessObj]
  3. push    edi                   ; 进程对象
  4. push    esi                   ; 线程对象
  5. call    _KiAttachProcess@16   ; 将当前线程附加到目标进程的地址空间中
复制代码

在KiAttachProcess函数中,首先调用KiMoveApcState保存当前线程的ApcState(ETHREAD->ApcSaveState = ETHREAD->ApcState), 以便在恢复切换进程时返回。

  1. lea     edi, [esi+_KTHREAD.ApcState.ApcListHead.Flink]
  2. push    edi          ;原始APC
  3. 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结构中的相应域中。到此进程切换完成了。
回复

使用道具 举报

 楼主| 发表于 2010-12-31 17:44:48 | 显示全部楼层
3环创建线程的分析:

      在创建进程返回后,此时EPROCESS,PEB结构已经创建,进程地址空间也已经创建并初始化了。接下处理下创建进程后的残余工作,调用NtSetInformationProcess函数设置进程的优先级和默认处理模式.

  1. push  2                     ; 进程信息的的大小
  2. lea   eax, [ebp+pProcessInforBuffer]
  3. push  eax                   ; 进程信息
  4. push  12h                   ; 进程信息类型索引(12 = ProcessPriorityClass)
  5. push  [ebp+pHandleOfProcess] ; 进程句柄
  6. call  ds:__imp__NtSetInformationProcess@16 ; 设置进程信息

  7. mov   [ebp+pInforBuffer], edi
  8. push  4                     ; 进程信息的长度
  9. lea   eax, [ebp+pInforBuffer]
  10. push  eax                   ; 进程信息的Buffer
  11. push  0Ch                   ; 设置那个进程信息的索引(0c = ProcessDefaultHardErrorMode)
  12. push  [ebp+pHandleOfProcess] ; 进程句柄
  13. call  ds:__imp__NtSetInformationProcess@16 ; 设置进程信息
复制代码

接着调用了BasepSxsCreateProcessCsrMessage函数,该函数的具体作用不是很清楚,不过实现中调用了BasepSxsGetProcessImageBaseAddress函数来获得进程的PEB, 然后通过NtReadVirtualMemory()取得ImageBase. 然后调用BasePushProcessParameters函数, 其中实现中调用了RtlCreateProcessParameters来创建进程参数, 该函数对RTL_USER_PROCESS_PARAMETERS结构中的字符串域的地址改为相对的偏移量。

接下来要做的就是创建线程, 不过在在此之前还要构建线程的环境,调用BaseCreateStack函数创建栈:

  1. lea   ecx, [ebp+InitialTeb]
  2. push  ecx                   ; 初始TEB
  3. push  eax                   ; 栈的最大值
  4. push  [ebp+StackSize]         ; 栈大小
  5. push  [ebp+pHandleOfProcess]  ; 进程句柄
  6. call  _BaseCreateStack@16    ; 创建栈
复制代码

接着调用BaseInitializeContext初始化线程上下文, 然后调用BaseFormatObjectAttributes函数格式化对象(以便传递给NtCreateThread)

  1. push  ebx                   ; 基本上下文类型
  2. push  [ebp+InitSp]            ; 初始化栈指针值
  3. push  dword ptr [ebp-834h]     ; [ebp-834] = InitPc(初始化计数值)
  4. push  [ebp+Param]           ; teb
  5. lea   eax, [ebp+pThreadContext]
  6. push  eax                   ; eax为指向CONTEXT的指针
  7. call  _BaseInitializeContext@20
复制代码

在BaseInitializeContext函数的实现中会判断参数上下文类型是否为1,如果不为1, 指定用户空间的线程启动函数为BaseProcessStartThunk(进程的第一个线程调用这个),否则为BaseThreadStartThunk(普通线程),然后CALL OEP。其中BaseProcessStartThunk函数会调用BaseProcessStart,这个函数就是调用OEP所在函数的上层函数:

  1. BaseProcessStart@4:          ; CODE XREF: BaseProcessStartThunk(x,x)+5 j
  2. 7C817054   push  0Ch
  3. 7C817056   push  offset stru_7C817080
  4. 7C81705B   call  __SEH_prolog                ;安装异常注册项
  5. 7C817060   and   dword ptr [ebp-4], 0
  6. 7C817064   push  4
  7. 7C817066   lea   eax, [ebp+8]
  8. 7C817069   push  eax
  9. 7C81706A   push  9
  10. 7C81706C   push  0FFFFFFFEh
  11. 7C81706E   call  ds:__imp__NtSetInformationThread@16 ;
  12. 7C817074   call  dword ptr [ebp+8]     ; 调用启动函数(CALL OEP)
  13. 7C817077   push  eax                   ; dwExitCode
  14. 7C817078   call  _ExitThread@4         ; ExitThread(x)
复制代码

用OD打开应用程序,停在OEP处, 然后看栈窗口, 将栈顶值(即ESP)的值所在的地址反汇编跟随一下:


最后调用NtCreateThread创建线程:

  1. push  1                     ; 是否创建后挂起
  2. lea   eax, [ebp+InitialTeb]
  3. push  eax                   ; 初始化TEB
  4. lea   eax, [ebp+pThreadContext]
  5. push  eax                   ; 线程上下文结构指针
  6. lea   eax, [ebp+pClientId]
  7. push  eax                   ; 客户端ID结构指针
  8. push  [ebp+pHandleOfProcess] ; 进程句柄
  9. push  [ebp+ObjAttibute]     ; 对象属性
  10. push  1F03FFh               ; 访问掩码
  11. lea   eax, [ebp+pThreadHandle]
  12. push  eax                   ; 线程句柄指针
  13. call  ds:__imp__NtCreateThread
复制代码

到此3环上的CreateThread就完成了
回复

使用道具 举报

 楼主| 发表于 2010-12-31 17:46:41 | 显示全部楼层
本帖最后由 十二 于 2010-12-31 17:49 编辑

0环创建线程的分析:

     0环中NtCreateThread函数直接调用pspCreateThread函数:
在pspCreateThread函数中,首先检查参数StartRoutine(系统线程)是否为0, 如果为0, 将PreviousMode值为1, 否则值为0。接着调用ObCreateObject函数创建线程对象并初始化为0.

  1. lea     eax, [ebp+pThreadObj]
  2. push    eax                   ; 新对象指针
  3. push    esi                   ; 不可分页面大小
  4. push    esi                   ; 可分页面的大小
  5. push    258h                  ; 对象体的大小
  6. push    esi                   ; int
  7. push    dword ptr [ebp+PreviousMode] ; int
  8. push    [ebp+ObjAttribute]    ; 对象属性
  9. push    _PsThreadType         ; 对象类型
  10. push    dword ptr [ebp+PreviousMode] ; 处理器模式
  11. call    _ObCreateObject@36    ; 创建线程对象

  12. mov     ecx, 96h
  13. xor     eax, eax
  14. mov     esi, [ebp+pThreadObj]
  15. mov     edi, esi
  16. rep stosd                     ; 初始化线程对象为0
复制代码

接着将新进程中EPROCESS-> UniqueProcessId拷贝给线程对象ETHREAD-> UniqueProcess, 然后调用ExCreateHandle函数往CID表中添加线程对象。

  1. lea     eax, [ebp+pEthread]
  2. 50                push    eax                   ; 线程对象
  3. push    _PspCidTable          ; CID表指针
  4. call    _ExCreateHandle@8     ; 在指定的句柄表中添加一个表项
复制代码

接着初始化信号量对象和自旋锁对象,之后判断参数进程对象指针ProcessPointer,是否为0,如果不为0,调用函数MmCreateTeb创建TEB.

  1. lea     eax, [ebp+pTeb]
  2. push    eax                   ; 指向TEB指针(传出参数)
  3. push    edi                   ; 客户端ID结构的指针
  4. push    [ebp+InitTeb]         ; 初始TEB
  5. push    ebx                   ; 目标进程对象
  6. call    _MmCreateTeb@16       ; 创建TEB
复制代码

如果ProcessPointer为0,KeInitThread初始化线程时线程函数为PspUserThreadStartup(用户线程启动函数), 否则为用户线程启动函数:

  1. push    ebx                   ; 进程对象(EPROCESS)
  2. push    ecx                   ; teb
  3. push    ecx                   ; 线程上下文指针
  4. push    [ebp+pStartContext]   ; 启动线程上下文
  5. push    eax                   ; 启动线程函数指针
  6. push    offset _PspSystemThreadStartup@8 ; 系统线程函数指针   , 当创建用户线程时,参数为PspUserThreadStartup
  7. push    ecx                   ; 内核栈指针
  8. push    esi                   ; 线程对象(ETHREAD)
  9. call    _KeInitThread@32      ; 初始化线程对象(初始化APC, 信号量对象,创建内核线程栈
  10.                                              ;初始化线程上下文)
复制代码

该函数的实现中主要做了如下操作:创建内核线程栈然后调用KiInitializeContextThread初始化线程上下文

  1. push    [ebp+pContext]        ; pContext 上下文记录
  2. push    [ebp+pContextStart]   ; 启动线程函数上下文
  3. push    [ebp+pThreadStart]    ; 启动线程函数地址
  4. push    [ebp+pKernelFunStart] ; 系统线程函数
  5. push    esi                   ; 内核线程对象(KTHREAD)
  6. 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,

  1. KiUserApcDispatcher
  2. lea     edi, [esp+arg_C]
  3. pop     eax
  4. call    eax            //LdrInitializeThunk
  5. push    1
  6. push    edi
  7. call    _ZwContinue@8         ;加载完DLL后,调用该函数继续返回到内核
复制代码

切换到内核后作部分操作后,再次回到3环,调用用户空间的线程入口函数BaseProcessStartThunk, 该函数在3环中BaseInitializeContext中有指定。

线程创建完成后调用CsrClientCallServer通知CRSS 线程创建成功,其中调用NtRequestWaitReplyPort()来等待回应,最后调用NtResumeThread()函数恢复线程的执行。

在用户空间线程启动时调用BaseProcessStartThunk, 该函数调用了BaseProcessStart函数, 这部分代码在上面已经贴出来了。
回复

使用道具 举报

 楼主| 发表于 2010-12-31 17:48:09 | 显示全部楼层
以上转载自pediy- pyq逍遥

http://bbs.pediy.com/showthread.php?t=114611
回复

使用道具 举报

发表于 2010-12-31 17:53:59 | 显示全部楼层
- -
…………真勤奋啊
回复

使用道具 举报

发表于 2010-12-31 19:37:52 | 显示全部楼层
本帖最后由 漆黑之翼 于 2010-12-31 19:38 编辑

对我来说太艰深了,再加上我没学过汇编虚不受补啊
回复

使用道具 举报

 楼主| 发表于 2010-12-31 19:41:36 | 显示全部楼层
大部分地方充斥呢API  找找原型还是比较好消化的,Nt系列的函数可以去找找System Call Table那帖。
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 少女注册中

本版积分规则

合作与事务联系|无图版|手机版|小黑屋|喵玉殿

GMT+8, 2025-10-31 07:16

Powered by Discuz! X3.5

© 2001-2025 Discuz! Team.

快速回复 返回顶部 返回列表