这个是武汉科锐 第三阶段 的分析项目,每个同学负责不同的模块。在此我将我所分析的模块发出来。

声明:由于本人能力有限,文中必然有错漏之处,恳请读者不吝赐教。
因为这个函数比较简单,都是直线下来的,所以没有画流程图!

ObCreateObject它的作用是创建指定类型(OBJECT_TYPE)的对象示例。
堆是系统数据动态申请及释放的地方,WinNT把堆叫做pool。根据是否可以换出,分为PagedPool和NonPagedPool。
如果系统频繁的申请和释放内存,开销很大.于是WinNT利用LookasideList做缓冲,内存释放时并不是直接到pool,而是放到LookasideList中,等申请内存时,先检查表中是否有合适大小的快,若有就直接使用.系统定时检查表项的数目,以保持pool有足够的可用内存。
可以将Lookaside对象想像成一个内存容器。在初始的时候,它先向Windows申请了一块比较大的内存。以后程序员每次申请内存的时候,不是直接向Windows申请内存,而是向Lookaside对象申请内存。Lookaside对象会智能地避免产生内存“空洞”。如果Lookaside对象内部的内存不够用时,它会向操作系统申请更多的内存。当Lookaside对象内部有大量的末使用的内存时,它会自动让Windows回收一部分内存。总之,Lookaside是一个自动的内存分配容器,通过对Lookaside对象申请内存,效率要高于直接向Windows申请内存。
看实现代码之前我先大概的描述一下这个函数的工作流程吧!
ObCreateObject内部通过调用 ObpAllocateObject为对象申请空间。在调用ObpAllocateObject调用之前ObCreateObject函数做一些相关信息的收集以及判断,ObpAllocateObject函数返回通过参数返回其起始地址(即&Header->Body),同时函数里面还做一些对象本身的初始化工作,申请成功后,如果要求对象成为“永久对象”,即具有OB_FLAG_PERMANENT_OBJECT标志的话,再检查是否为系统的安全机制所允许,如果不允话的话,还要把刚刚申请的空间释放掉,同时返回一个错误代码STATUS_PRIVILEGE_NOT_HELD。
整个函数就是完成一个目标对象的创建工作。
下面先来看ObCreateObject函数
这个函数有9个参数,但是有一个没有使用,所以IDA里面没有标识出来。参数如下:

代码:
ProbeMode       = byte ptr  8
ObjectType      = dword ptr  0Ch
ObjectAttributes= dword ptr  10h
ObjectHeader    = byte ptr  14h
ObjectBodySize  = dword ptr  1Ch
PagedPoolCharge = dword ptr  20h
NonPagedPoolCharge= dword ptr  24h
arg_Object          = dword ptr  28h
  可能有些人在想,你是如何知道他有几个参数的,第一,看参数调用之前压了多少个参数,然后看里面都用了哪些,还有注意看下有没有通过寄存器传参,最后再看函数结束的堆栈平衡了多少个参数。
凡是创建对像的系统调用,都要提供至少二个输入参数。其中这定是DesiredAccess,说明所创建对象的访问模式,例如"只读"等。另一个是ObjectAttributes,这是一个OBJECT_ATTRIBUTES结构指针,OBJECT_ATTRIBUTES是一个很重要的数据结构。
代码:
0:000> dt _OBJECT_ATTRIBUTES
ntdll!_OBJECT_ATTRIBUTES
+0x000 Length           : Uint4B
+0x004 RootDirectory    : Ptr32 Void
+0x008 ObjectName       : Ptr32 _UNICODE_STRING
+0x00c Attributes       : Uint4B
+0x010 SecurityDescriptor : Ptr32 Void
+0x014 SecurityQualityOfService : Ptr32 Void
对象总是创建在某一目录中,这个目录必须事先已经打开,当前进程必须拥有这个已经打开目录的句柄。这里的RootDirectory就是这个目录节点的句柄。
另一个字段Attributes是一些表示各种属性的标志位,这些标志位连同这里别的字段最后都进入目标对像的OBJECT_CREATE_INFORMATION数据结构。
继续回到ObCreateObject函数上来。
代码:
mov    ebx, large fs:20h ; 取得_KPRCB地址
push    esi
mov    esi, [ebx+5F8h] ; 取得_KPRCB的PPLookasideList[3]中指向后一个节点的值;_PP_LOOKASIDE_LIST
inc      [esi+GENERAL_LOOKASIDE.TotalAllocates] ; _GENERAL_LOOKASIDE.TotalAllocates
push   edi            ; 保存环镜,还不是传参
mov   ecx, esi        ; 通过寄存器ecx传参
call    @ExInterlockedPopEntrySList@8 ; 删除单向链表互锁操作
mov   edi, eax        ; eax的值为指向previously _GENERAL_LOOKASIDE
test    edi, edi
jnz     short FillObjectCreateInformation
inc     [esi+GENERAL_LOOKASIDE.anonymous_0.AllocateHits]
mov   esi, [ebx+5FCh] ; 取得_KPRCB的PPLookasideList[3]中指向前一个节点的值;_PP_LOOKASIDE_LIST
inc    [esi+GENERAL_LOOKASIDE.TotalAllocates]
mov   ecx, esi
call    @ExInterlockedPopEntrySList@8 ; 删除单向链表互锁操作
mov   edi, eax
test    edi, edi
jnz     short FillObjectCreateInformation
push    [esi+GENERAL_LOOKASIDE.Tag] ; ULONG
inc     dword ptr [esi+GENERAL_LOOKASIDE.anonymous_0]
push    [esi+GENERAL_LOOKASIDE.Size] ; SIZE_T
push    [esi+GENERAL_LOOKASIDE.Type] ; POOL_TYPE
call  [esi+GENERAL_LOOKASIDE.Allocate] ;通常放 ExAllocatePoolWithTag
                        ; 还有一个Free释放函数,通常放 ExFreePool
mov     edi, eax        ; 这里判断有没有申请空间成功
test    edi, edi
jnz     short FillObjectCreateInformation
mov     ebx, 0C000009Ah ; STATUS_INVALID_PARAMETER
                        ; 如果没有申请成功,跳到函数末
                        ; 尾部有个将ebx给eax的操作,ebx为返回值
jmp     ObCreateObjectEnd
由上的代码中我们可以发现,首先系统先在LookasideList为_OBJECT_CREATE_INFORMATION结构体申请空间,如果申请不成功,再去非分页池中申请。
上段反汇编识别来自ObpAllocateObjectCreateInfoBuffer,但是在编译之后已经内联进来了。
如果申请空间成功的话,这里再通过调用ObpCaptureObjectCreateInformation函数,做一个填充结构体的工作。
代码:
mov     esi, [ebp+ObjectType]
push    0
push    edi             ; ObjectCreateInfo
lea     eax, [ebp+CapturedObjectName]
push    eax
push    [ebp+ObjectAttributes]
push    dword ptr [ebp+ObjectHeader]
push    dword ptr [ebp+ProbeMode]
push    esi             ; ObjectType
call    ObpCaptureObjectCreateInformation 
下面跟进去看下ObpCaptureObjectCreateInformation函数。
ObpCaptureObjectCreateInformation有7个参数,但是第一个忽略了,所以IDA中略过了。
代码:
ProbeMode       = byte ptr  0Ch
CreatorMode     = byte ptr  10h
ObjectAttributes= dword ptr  14h
CapturedObjectName= dword ptr  18h
ObjectCreateInfo= dword ptr  1Ch
arg_18          = dword ptr  20h
下面是ObpCaptureObjectCreateInformation函数内部:
代码:
xor     eax, eax
mov     ebx, [ebp+ObjectCreateInfo]
mov     edi, ebx
rep stosd               ; 将ObjectCreateInformation结构体清零
函数一开始先对ObjectCreateInformation结构体置为0。然后结构成员根据参数给值。
代码:
mov     eax, [ebp+ObjectAttributes]
cmp     eax, esi 
判断ObjectAttributes是否为空,不为空填写ObjectCreateInformaint相关的项,同时里面还有一些检查,比如检查结构体是否可读,是否是内核模式(KernelMode)的操作,然后再给相就的成员赋值。
后面还有判断ObjectName参数是否为空,不为空的话,调用ObpCaptureObjectName函数给相关成员赋值,否则直接赋空值。
在ObpCaptureObjectName中,有一系列的判断,比如Unicode字符串的长度不是偶数倍数或者不是以零结束,最后再通过ObpAllocateObjectNameBuffer分配空间,申请成功再调用RtlCopyMemory将字符串拷备过来。
ObpAllocateObjectNameBuffer函数的第二个参数给申请空间一些建议,如果值为真,首先从Lookaside列表中申请,如果这里申请不成功,再去非分页中申请,如果这个参数为假,或者是申请的长度过大,则直接通过ExAllocatePoolWithTag函数去非分页中申请。
好了,继续回ObCreateObject函数上面来。
当上面的二个操作成功之后,就到了我们真正为对象分配空间的时候了。
首先前面是一些检查。
代码:
mov     eax, [esi+OBJECT_TYPE.TypeInfo.InvalidAttributes]
test    [edi+_OBJECT_CREATE_INFORMATION.Attributes], eax
ObjecrCreateInfo中的Attributes字段来自用户空间的对象属性块,是一些表示属性要求的标志位。但是这些属性要求对于具体的目标对象类型是否可以接受,为此,对象类型的数据结构中有个字段InvalidAttributes,这也是一些标志位,表示对本类型没有意义或不可接受的属性。这样,如果这两个位图有交集,就说明对象属性块中有非分之想,所以加以拒绝,否则就过不了这一关。
此外,还需要在ObjectCreateInfo中补充收集一些附加的信息,其实只是目标对像对于内存的消耗,包括PagedPoolCharge和NonPagedPoolCharge两项。前者是对于可换出页面的消耗,后者是对于不可换出页面的消耗。
代码:
mov     ecx, [ebp+PagedPoolCharge] ;属性没有冲突,就填写Charge
test    ecx, ecx        ; 判断是否为0
jnz     short FillNonPagedPoolCharge
mov     ecx, [esi+OBJECT_TYPE.TypeInfo.DefaultPagedPoolCharge] ; 
FillNonPagedPoolCharge:
mov     eax, [ebp+NonPagedPoolCharge]
test    eax, eax 
jnz     short AllocObjectCreateInformation
mov     eax, [esi+OBJECT_TYPE.TypeInfo.DefaultNonPagedPoolCharge]
AllocObjectCreateInformation:
mov     [edi+_OBJECT_CREATE_INFORMATION.NonPagedPoolCharge], eax 
首先判断PagedPoolCharge的值是否为0,为0的话用默认值代替,再判断NonPagedPoolCharge是否为0,是的话也用默认值代替,最后再将值赋给ObjectCreateInfo相关的成员。
接着调用ObpAllocateObjec为对象真正的分配空间。这里我们先跳过这个函数将着往后面看。
调用完ObpAllocateObjec之后,判断一下是否申请对象成功。
代码:
call    ObpAllocateObject
mov     ebx, eax
test    ebx, ebx
jl      ReleaseObjectCreateInformation
如果对像申请不成功,把之前申请的结构全部释放掉。
目标对象的数据结构分配成功以后,通过参数Object返回其起始地址,即&Header->Body。最后如果要求目标对象成为“永久对象”,即Header->Flags中的标志位OB_FLAG_PERMANENT_OBJECT为1,则还要通过SeSinglePrivilegeCheck()检查是否为系统的安全机制所允许,如果不允许的话就要”退赔”,就是把刚分配的数据结构释放掉。并返回STATUS_PRIVILEGE_NOT_HELD错误代码。
代码:
mov     edi, dword ptr [ebp+ObjectHeader]
mov     esi, [ebp+ arg_Object]
lea     eax, [edi+_OBJECT_CREATE_INFORMATION.SecurityDescriptorCharge]
mov    [esi], eax ;将申请的空间地址给传出参数
test    byte ptr [edi+(_OBJECT_CREATE_INFORMATION+0Fh)], 10h ; ProbeMode是一个char的宽度
jz      short CheckPrivilege
push    dword ptr [ebp+ProbeMode] ;
push    ds:SeCreatePermanentPrivilege.HighPart
push    ds:SeCreatePermanentPrivilege.LowPart ; PrivilegeValue
call    SeSinglePrivilegeCheck ; 如果固定的对像已经创建,则检查是否为系统的安全机制所允许
test    al, al
jnz     short CheckPrivilege
mov     ecx, [esi]
call    ObpFreeObject
mov     ebx, 0C0000061h ; 060000061h == STATUS_PRIVILEGE_NOT_HELD
如果SeSinglePrivilegeCheck()成功之后,比较ObpTraceEnabled标志位,如果为真的话,调用ObpRegisterObject和ObpPushStackInfo。
代码:
cmp     ObpTraceEnabled, 0
jz      short ObCreateObjectEnd
test    ebx, ebx
jl      short ObCreateObjectEnd
push    edi             ; NewIrql
call    ObpRegisterObject ; 如果上面的那个条件都成功,执行下面的二个函数
push    1
push    edi
call    ObpPushStackInfo
jmp     short ObCreateObjectEnd
最后再返回。
整个函数的功能都比较简单。就是单纯的分配空间,然后就是目标对象本身的初始化,就完成目标对象的创建。
我们先来看看对象的内存结构:


 
好了,现在我们来看ObpAllocateObject函数的实现吧!
这个函数有 6个参数,参数列表如下:
代码:
POBJECT_CREATE_INFORMATION ObjectCreateInfo,
KPROCESSOR_MODE OwnershipMode,
POBJECT_TYPE ObjectType OPTIONAL,
PUNICODE_STRING ObjectName,
ULONG ObjectBodySize,
POBJECT_HEADER *ReturnedObjectHeader
在函数体的实现中,有些参数只用了一次,后面被编译器优化用于局部变量。
下面我们来看函数的实现:
代码:
mov     eax, [ebp+arg_ObjectCreateInfo]
push    ebx
push    esi
mov     esi, [ebp+arg_HandleInfoSize] 
xor     ecx, ecx
cmp     eax, ecx        ; 这里判断一下ObjectCreateInfo参数是否为空
                        ; 空的话使用默认值,空的话也说明是一个类型对像
                        ; 默认有OBJECT_HEADER_NAME_INFO、OBJECT_HEADER_NAME_INFO可选头部
push    edi
mov     edi, [ebp+CreatorInfoSize] ; 这里原来是ObjectName参数,用来编译器优化,用于它用了,后来用来做变量了
jnz     short cmpDefaultQuota 
push    10h
pop     ebx             ; CreatorInfoSize等于10
mov     [ebp+var_QuotaInfoSize], ecx
mov     [ebp+arg_HandleInfoSize], ecx
mov     [ebp+CreatorInfoSize], ebx
jmp     short ComputerHeaderSize:
这里判断ObjectCreateInfo是否为空,空的话也说明是一个类型对像,然后使用默认值填充,默认是有OBJECT_HEADER_NAME_INFO、OBJECT_HEADER_NAME_
INFO可选头部。
如果不为空的话,我们继续往下看:
代码:
mov     edx, [eax+_OBJECT_CREATE_INFORMATION.PagedPoolCharge] ; 
cmp     edx, [esi+OBJECT_TYPE.TypeInfo.DefaultPagedPoolCharge]
jnz     short cmpPsInitialSystemProcess
mov     edx, [eax+_OBJECT_CREATE_INFORMATION.NonPagedPoolCharge]
cmp     edx, [esi+OBJECT_TYPE.TypeInfo.DefaultNonPagedPoolCharge] jnz     short cmpPsInitialSystemProcess
cmp     [eax+_OBJECT_CREATE_INFORMATION.SecurityDescriptorCharge], 800h ; SE_DEFAULT_SECURITY_QUOTA
jbe     short loc_525BD5 ; OBJ_EXCLUSIVE
                        ; 判断是否允许别的进程打开

cmpPsInitialSystemProcess: ; 
mov     edx, large fs:124h
mov     edx, [edx+38h]
cmp     edx, PsInitialSystemProcess ; 判断是不是系统初始化进程
jnz     short HaveQuotaInfo ; 给QuotaInfo留空间

loc_525BD5:
test    byte ptr [eax+_OBJECT_CREATE_INFORMATION.Attributes], 20h 
                                ; 判断是否允许别的进程打开
mov     [ebp+var_QuotaInfoSize], ecx   ; ecx 为零值
jz      short loc_525BE4
HaveQuotaInfo:
mov     [ebp+var_QuotaInfoSize], 10h ; 给QuotaInfo留空间
首先是判断ObjectCreateInfo,PagedPoolCharge是否是使用默认值,如果不是的话,就有必要为OBJECT_HEADER_QUOTA_INFO留空间,因为不是默认值的话,需要空间来保存相关的内存消耗信息,如果是使用认值的话再比较NonPagedPoolCharge与SecurityDescriptorCharge,这三个比较都是与操作,只要一个为真,就成立,这个成立之后跳去cmpPsInitialSystemProcess。
这个判断是不是系统初始化进程,是的话不成立,即不用为QuotaInfoSize留空间。
代码:
mov     al, [esi+OBJECT_TYPE.TypeInfo.MaintainHandleCount] ; 对象的类型决定了是否需要OBJECT_HEADER_HANDLE_INFO结构
mov     ebx, [edi+UNICODE_STRING.Buffer] 
neg     al
sbb     eax, eax
and     eax, 8
neg     ebx
mov     [ebp+arg_HandleInfoSize], eax
mov     al, [esi+OBJECT_TYPE.TypeInfo.MaintainTypeList]
sbb     ebx, ebx
and     ebx, 10h 
neg     al
sbb     eax, eax
and     eax, 10h
mov     [ebp+CreatorInfoSize], eax
这里是二个判断,判断是否需要OBJECT_HEADER_HANDLE_INFO和OBJECT_HEADER_CREATOR_INFO、OBJECT_HEADER_NAME_INFO。
这些有一些很有意思的编译器优化。
代码:
mov     ebx, [edi+UNICODE_STRING.Buffer]
neg     ebx
sbb     ebx, ebx
and     ebx, 10h
  首先取得值给ebx,再对ebx做一个求补的运算,如果ebx为0的话,结果也为0,再一个带符号的减法,如果ebx原来为零,值的话也一直为0,如果原来有值的话,经过这步之后,寄存器的值变成了全F,然后再和10做个与运算(因为结构体的大小是10),这样运算完的结果可以直接用于赋值,而不用跳转了,同时编译器还做了一些流水线的优化。
  好了,我们继续回到代码上面来,现在到了计算头大小的时候了。
代码:
mov     eax, [ebp+CreatorInfoSize]
mov     ecx, [ebp+var_QuotaInfoSize]
add     eax, ebx        ; 这里开始对头的大小进么计算
                        ; ebx为NameInfoSize
add     eax, [ebp+arg_HandleInfoSize]
xor     edx, edx
cmp     esi, edx 
lea     ecx, [eax+ecx+18h]
  这里将CreatorInfoSize、QuotaInfoSize、HandleInfoSize、NameInfoSize与0x18相加(_OBJECT_HEADER除去BODY的size)。这些做了一个小优化,通过lea来进行算术运算。
  接着上面的代码,我们继续分析。
代码:
jz      short loc_525C26
cmp     [esi+OBJECT_TYPE.TypeInfo.PoolType], edx ; NonPagedPool
jz      short loc_525C26
inc     edx             ; edx为分页类型
loc_525C26
test    esi, esi
mov     eax, 546A624Fh  ; 字符串TjbO
jz      short loc_525C35
 ; (ObjectType==NULL?'TjbO':ObjectType->Key)|PROTECTED_POOL
mov     eax, [esi+OBJECT_TYPE.Key]
loc_525C35:
or      eax, 80000000h 
 ; (ObjectType==NULL?'TjbO':ObjectType->Key)|PROTECTED_POOL
push    eax             ; Tag
mov     eax, [ebp+arg_ObjectBodySize]
add     ecx, eax        ; HeaderSize + ObjectBodySize
push    ecx             ; NumberOfBytes
push    edx             ; PoolType
call    ExAllocatePoolWithTag
在之前的代码中已经判断ObjectType是否为空了,如果不为空再判断ObjectType->TypeInfo.PoolType 是否等于 NonPagedPool,等于的话跳过那个inc,因为NonPagedPool等于0,PagedPool等于1,分页类型用于ExAllocatePoolWithTag的最后一个参数。因为ObjectType有可能是空,所以要采用上面的方法传参。
接着下面又再次判断ObjectType是否为空,其实我觉得这里可以优化一下的,因为刚刚才比较过的,如果为空的话把Tag”TjbO”赋给eax,否则将OBJECT_TYPE.Key值给eax,最后再把eax和80000000h(PROTECTED_POOL)做个与的运算。
然后先最后一个参数压进栈之后,再计算一个申请空间的大小,因为我们刚刚计算的头大小中,并没有算上Body的,所以这里他做了一个加法,最后将其它的二个参数压进栈,完成函数的调用。
继续往下看代码:
代码:
test    eax, eax        ; 这里判断有没有申请成功
jnz     short AllocObjectSuccess
mov     eax, 0C000009Ah ; STATUS_INSUFFICIENT_RESOURCES
jmp     FunctionEnd
这里判断有申请空间是否成功,如果不成功的话,返回一个STATUS_INSUFFICIENT_RESOURCES。如果成功了就跳走。我们再看成功的代码。
成功的代码比较长,但是比较简单,都是一些相关结构体内成员的赋值。
代码:
mov     ecx, [ebp+var_QuotaInfoSize]
test    ecx, ecx
jz      short cmpHandleInfoSize
mov     edx, [ebp+arg_ObjectCreateInfo]
mov     edx, [edx+_OBJECT_CREATE_INFORMATION.PagedPoolCharge]
mov     [eax+_OBJECT_HEADER_QUOTA_INFO.PagedPoolCharge], edx
mov     edx, [ebp+arg_ObjectCreateInfo]
mov     edx, [edx+_OBJECT_CREATE_INFORMATION.NonPagedPoolCharge]
mov     [eax+_OBJECT_HEADER_QUOTA_INFO.NonPagedPoolCharge], edx 
mov     edx, [ebp+arg_ObjectCreateInfo]
mov    edx, [edx+_OBJECT_CREATE_INFORMATION.SecurityDescriptorCharge]
and     dword ptr [eax+0Ch], 0 ; QuotaInfo->ExclusiveProcess
mov     [eax+_OBJECT_HEADER_QUOTA_INFO.SecurityDescriptorCharge], edx
add     eax, 10h
这段代码是判断有没有_OBJECT_HEADER_QUOTA_INFO可选头,有的话,把内存消耗的信息复制到_OBJECT_HEADER_QUOTA_INFO中去。
注意后面的add eax, 10h。这有助于我们了解他的内存结构。
代码:
cmp     [ebp+arg_HandleInfoSize], 0
jz      short cmpNameInfoSize
and     [eax+_OBJECT_HEADER_HANDLE_INFO.anonymous_0.SingleEntry.HandleCount], 0 add     eax, 8
这里判断有没有OBJECT_HEADER_HANDLE_INFO可选头,有的话将数据结构中的 HandleCount设置为0。
这里后面是加8了,说明OBJECT_HEADER_HANDLE_INFO的大小为8,排列在_OBJECT_HEADER_QUOTA_INFO头部后面。
代码:
test    ebx, ebx  ; ebx为NameInfoSize,上面一直没有改过
jz      short cmpCreatorInfoSize
mov     edx, [edi]  ; edi指向ObjectName
mov     dword ptr [eax+_OBJECT_HEADER_NAME_INFO.Name.Length], ed 
mov     edx, [edi+_UNICODE_STRING.Buffer] 
and     [eax+_OBJECT_HEADER_NAME_INFO.Directory], 0 
cmp     [ebp+arg_OwnershipMode], 0 ;这里判断是不是内核模式
mov     edi, [ebp+arg_ObjectCreateInfo]
mov     [eax+_OBJECT_HEADER_NAME_INFO.Name.Buffer], edx 
mov     [eax+_OBJECT_HEADER_NAME_INFO.QueryReferences], 1
jnz     short loc_525CBD
test    edi, edi
jz      short loc_525CBD
test    byte ptr [edi+_UNICODE_STRING.MaximumLength], 1 
jz      short loc_525CBD
mov     [eax+_OBJECT_HEADER_NAME_INFO.QueryReferences], 40000001h

loc_525CBD:
add     eax, 10h
jmp     short cmp_CreatorInfoSize
这一段代码的作用就是将对象名复制到OBJECT_HEADER_NAME_INFO数据结构中中去,同时将NameInfo->Directory设为NULL, NameInfo->QueryReferences设为1,然后再判断所属模式(OwnershipMode)是否是内核模式成功的话接着判断ObjectCreateInfo是否为空,不为空再判断属性(Attributes)是否具有采用内核句柄表属性。三个都成立的话,将QueryReferences成员和40000001h(OBP_NAME_KERNEL_
PROTECTED)做个或运算,最后eax再加10h。
代码:
mov     edi, [ebp+arg_ObjectCreateInfo]
cmp_CreatorInfoSize:
cmp     [ebp+CreatorInfoSize], 0
jz      short loc_525CEB ; ecx等于QuotaInfoSize的值
and     word ptr [eax+0Ch], 0; CreatorBackTraceIndex
mov     edx, large fs:124h
mov     edx, [edx+38h];PsGetCurrentProcess函数实现
mov     edx, [edx+94h]; UniqueProcessId
mov     [eax+_OBJECT_HEADER_CREATOR_INFO.CreatorUniqueProcess], edx
mov     [eax+_OBJECT_HEADER_CREATOR_INFO.TypeList.Blink], eax
mov     [eax+_OBJECT_HEADER_CREATOR_INFO.TypeList.Flink], eax
add     eax, 10h
这段代码首先判断是否具有ObjectCreateInfo可选头,然后将CreatorBackTraceIndex置0,再将唯一进程ID给CreatorUniqueProcess,最后调用
InitializeListHead(),这里将这个函数内联进来了。
  接着eax再加10h。
  这里可选头的赋值就结束了,下面是开始计算可选头的偏移了。
  OBJECT_HEADER中的NameInfoOffset、HandleInfoOffset、QuotaInfoOffset是相应可选头结构距OBJECT_HEADER的偏移。
  OBJECT_HEADER的地址减去相应偏移就可以找到对应结构体。
  若偏移为0则说明对应结构不存在。
代码:
test    ecx, ecx        ; ecx等于QuotaInfoSize的值
jz      short setQuotaInfoOffsetZero
add     cl, byte ptr [ebp+arg_HandleInfoSize]
add     cl, bl
add     cl, byte ptr [ebp+CreatorInfoSize]
mov     [eax+0Eh], cl   ; SetQuotaInfoOffset
jmp     short loc_525D00
setQuotaInfoOffsetZero: 
mov     [eax+OBJECT_HEADER.QuotaInfoOffset], 0
  先判断有没有OBJECT_HEADER_QUOTA_INFO头,再将另三个可选头的大小相加,便可得到偏移地址,最后将结果存在域中。如果没有这个可选头,那这个域的值为0。
  后面依次是计算另二个可选头的偏移(OBJECT_HEADER_CREATE_INFO可不是记偏移的哦)。
  跳上那部分计算偏移的代码,我们继续往下看:
代码:
cmp     [ebp+CreatorInfoSize], 0
mov     [eax+OBJECT_HEADER.Flags], 1 ; OB_FLAG_NEW_OBJECT
jz      short loc_525D35
mov     [eax+OBJECT_HEADER.Flags], 5 ; OB_FLAG_CREATOR_INFO
这里判断是否有ObjectCreatorInfo可选头,无论有没有,都将Flags域置为1,如果有的话置为5,其实这是一个1与4的或运算。4代表OB_FLAG_CREATOR_INFO,当具有OB_FLAG_CREATOR_INFO标志时说明有ObjectCreatorInfo头,因为ObjectCreatorInfo头固定在OBJECT_HEADER上面的,所以没有记偏移,而是通过Flags的值来判断。
代码:
cmp   [ebp+arg_HandleInfoSize], 0
jz    short loc_525D3F 
or    [eax+OBJECT_HEADER.Flags], 40h ;  OB_FLAG_SINGLE_HANDLE_ENTRY
  接着判断有没有OBJECT_HEADER_HANDLE_INFO头,有的话将Flags加上OB_FLAG_SINGLE_HANDLE_ENTRY属性。
代码:
and     [eax+OBJECT_HEADER.anonymous_0.HandleCount], 0
xor     edx, edx
inc     edx
cmp     [ebp+arg_OwnershipMode], 0
mov     [eax+OBJECT_HEADER.PointerCount], edx
mov     [eax+OBJECT_HEADER.Type], esi    ; esi指向objectType
  这里对HandleCount、PointerCount以及Type域的赋值。
代码:
jnz     short loc_525D55 ; 这里判断是不是内核模式
or      [eax+OBJECT_HEADER.Flags], 2 ;
  这接着上面的,如果所属模式是内核模式的话,Flags再增加一个OB_FLAG_KERNEL_OBJECT标志。
代码:
test    edi, edi        ; 判断ObjectCreateInfo是否为空
jz      short ObjectCreateInfoIsZero
test    byte ptr [edi+_OBJECT_CREATE_INFORMATION.Attributes], 10h
jz      short loc_525D62 ; 
or      byte ptr [eax+OBJECT_HEADER.Flags], 10h
loc_525D62:
test    byte ptr [edi+_OBJECT_CREATE_INFORMATION.Attributes], 20h
jz      short ObjectCreateInfoIsZero
or      [eax+OBJECT_HEADER.Flags], 8
  还是先判断是否为空,这里做了一个小优化,把二次比较优化成一次了,第一个判断是否是永久性对象,是的话在属性(Attributes)域中增加OB_FLAG_PERMANENT_OBJECT属性。
  接着下一个是判断是否不允许别的进程同时打开,是的话再属性(Attributes)域中增加OB_FLAG_EXCLUSIVE_OBJECT属性。
代码:
and     [eax+OBJECT_HEADER.SecurityDescriptor], 0
test    esi, esi        ; 判断ObjectType是否等于0
mov     [eax+OBJECT_HEADER.anonymous_1.ObjectCreateInfo], edi
jz      short loc_525D87
lea     ecx, [esi+OBJECT_TYPE.TotalNumberOfObjects]
lock xadd [ecx], edx
mov     ecx, [ecx]
cmp     ecx, [esi+OBJECT_TYPE.HighWaterNumberOfObjects]
jbe     short loc_525D87 
mov     [esi+OBJECT_TYPE.HighWaterNumberOfObjects], ecx
loc_525D87: j
mov     ecx, [ebp+arg_ObjectHeader]
mov     [ecx], eax      ; 这里将申请的对像地址返回
xor     eax, eax
  上面已经判断ObjectType是否为空了,如果没有值的话跳走,有的话,先算出TotalNumberOfObjects的地址,之后再执行lock xadd [ecx], edx指令,这条指令指目标地址锁定,然后先交换两个操作数的值,再进行算术加法操作,edx的值为1,在上面的代码中执行了xor edx, edx、inc edx。
  接着再判断TotalNumberOfObjects的值是否大于HighWaterNumberOfObjects,是的话将TotalNumberOfObjects的值给HighWaterNumberOfObjects。
  最后将申请到的地址通过参数传出。
  之前提到”质检”不合格的话再将装申请的空间释放。现在我们来看ObFreeObject函数。他跟ObCreateObject很多操作都是对应的。下面我们来看代码吧。
先来看ObCreateObject是如何调用他的,下面这代码还是来自ObCreateObject:
代码:
push    dword ptr [ebp+ProbeMode] ; PreviousMode
push    ds:SeCreatePermanentPrivilege.HighPart
push    ds:SeCreatePermanentPrivilege.LowPart ; PrivilegeValue
call    SeSinglePrivilegeCheck ;检查是否为系统的安全机制所允许
test    al, al
jnz     short CheckPrivilege 
mov     ecx, [esi]
call    ObpFreeObject
  在Call ObpFreeObject上面我们没有看到相关的参数的入栈,只看到一个ecx的赋值,而esi是一个内存地址值,里面指向用来存放刚刚申请空间的起始地址(即&Headfer->Body的地址)。
  接着我们进到ObpFreeObject函数里面。
代码:
push    ebp
mov     ebp, esp
sub     esp, 0Ch
push    esi
lea     esi, [ecx-18h]  ; 通过ecx传参, ecx-18就得到了OBJECT_HEADER
  从这里我们可以确定是通过寄存器传参了,同时这里再做一个减法,就可以得到指向OBJECT_HEADER的地址了,因为传进来的是Body的起始地址。
代码:
test    [esi+OBJECT_HEADER.Flags], 4 
  这里通过Flags来判断有没有OBJECT_CREATE_INFO,因为这个头一定在OBJECT_HEADER上面,所以没有记录偏移,而是置了一个标志位。
mov     ecx, [esi+OBJECT_HEADER.Type]
mov     [ebp+var_ObjectType], ecx ; 取得ObjectType
mov     [ebp+P], esi    ; 现在esi指向已知的空间最顶处
  [ebp+P]始终保存着指向对象数据结构已知的起始地址。这部分代码也就是算他的起始地址的,得到OBJECT_HEADER的地址以后,我们还要判断有没有别的可选头,然后减去相关头的大长,最后就可以得到对象数据结构的起始地址,关于对象在内存中的布局,我们前面也已经讲过了,我相信读者对那图已熟记于心了。
代码:
jz      short NoObjectCreateInfo
lea     eax, [esi-10h]  ; 减去ObjectCreateInfo结构的长度
jmp     short loc_525DBA
NoObjectCreateInfo: 
xor     eax, eax。
loc_525DBA:
test    eax, eax        ; 判断有没有ObjectCreateInfo头
jz      short loc_525DC1
mov     [ebp+P], eax ;再次将已知的起始地址保存起来。
loc_525DC1:
mov     al, [esi+OBJECT_HEADER.NameInfoOffset]
test    al, al          ; 判断有没有OBJECT_HEADER_NAME_INFO头
  除了OBJECT_CREATE_INFO以外,别的几个选头有的话,是记录偏移量,所以我们可以能过判断偏移量是否为0值来判断是否存在那个可选头,这里就是判断有没有OBJECT_HEADER_NAME_INFO。
代码:
movzx   edx, al
mov     eax, esi
sub     eax, edx        ; 减去OBJECT_HEADER_NAME_INFO的长度
mov     [ebp+var_NameInfo], eax。
jmp     short loc_525DD8
NoOBJECT_HEADER_NAME_INFO:
and     [ebp+var_NameInfo], 0 ; 将var_NameInfo置0
loc_525DD8:
mov     eax, [ebp+var_NameInfo]
test    eax, eax        ; 判断是否有OBJECT_HEADER_NAME_INFO头
jz      short loc_525DE2
mov     [ebp+P], eax
loc_525DE2:
mov     al, [esi+OBJECT_HEADER.HandleInfoOffset]
test    al, al          ; 判断有没有OBJECT_HEADER_HANDLE_INFO头
push    ebx
jz      short loc_525DF3
movzx   eax, al
mov     ebx, esi
sub     ebx, eax
  esi指向OBJECT_HEADER, 将esi的值给ebx再减去OBJECT_HEADER_HAND
LE_INFO头偏移,得到HANDLE_INFO的起始地址。同时也是已知的数据结构的起始地址。
代码:
xor     ebx, ebx
  如果没有OBJECT_HEADER_HANDLE_INFO的话,将ebx置0
代码:
test    ebx, ebx
jz      short loc_525DFC
mov     [ebp+P], ebx 
loc_525DFC:j
mov     al, [esi+OBJECT_HEADER.QuotaInfoOffset]
test    al, al
  判断有没有OBJECT_HEADER_QUOTA_INFO头,有的话减去体构体大小,就可以得到对象真正的起始地址
  这里基本完成了计算了,因为这个头是在最上面的了!
代码:
test    dl, 1
  通过判断Flags的标志位来判断是不是新建的对象。其实按理来说,这里是判断是处于初始化哪个阶段,这个标记只用于检测是否有对该对象进行配额管理,这是因为对象创建信息和负责配额块并在一起了。
代码:
jz      short NotNewObject
mov     eax, [esi+OBJECT_HEADER.anonymous_1.ObjectCreateInfo]
test    eax, eax
jz      loc_525ECB
mov     ecx, [eax+_OBJECT_CREATE_INFORMATION.SecurityDescriptor]
test    ecx, ecx 
jz      short loc_525E4E 
movsx   eax, [eax+_OBJECT_CREATE_INFORMATION.ProbeMode]
push    1               ; char
push    eax             ; char
push    ecx             ; P
call    SeReleaseSecurityDescriptor ; 将SecurityDescriptor释放
  由字面上可以理解成释放安全描述描,在ObCreateObject中我们也看到的相对应的操作。
代码:
mov     eax, [esi+OBJECT_HEADER.anonymous_1.ObjectCreateInfo]
and     [eax+_OBJECT_CREATE_INFORMATION.SecurityDescriptor], 0
  释放掉了,再将那个域的值清零,相当于我们释放了堆空间,同时也要将指针清零,防止野指针。
代码:
mov     eax, large fs:20h ; 取得_KPRCB地址
                        ; 这里的这些操作和ObCreateObect相对应
mov     ecx, [eax+5F8h] ; 取得_KPRCB的PPLookasideList[3]中指向后一个节点的值;_PP_LOOKASIDE_LIST
mov     edx, [esi+OBJECT_HEADER.anonymous_1.ObjectCreateInfo]
mov     di, [ecx+GENERAL_LOOKASIDE.ListHead.anonymous_0.Depth]
inc     [ecx+GENERAL_LOOKASIDE.TotalFrees]
cmp     di, [ecx+GENERAL_LOOKASIDE.Depth]
jb      short loc_525E89
inc     [ecx+_PAGED_LOOKASIDE_LIST.L.anonymous_1.FreeHits]
mov     ecx, [eax+5FCh]
mov     ax, [ecx+GENERAL_LOOKASIDE.ListHead.anonymous_0.Depth]
inc     [ecx+GENERAL_LOOKASIDE.TotalFrees]
cmp     ax, [ecx+GENERAL_LOOKASIDE.Depth]
jb      short loc_525E89
inc     [ecx+_PAGED_LOOKASIDE_LIST.L.anonymous_1.FreeHits]
push    edx             ; PVOID
call    [ecx+_PAGED_LOOKASIDE_LIST.L.Free]
jmp     short loc_525EC7
loc_525E89:
call    @InterlockedPushEntrySList@8 
jmp     short loc_525EC7
NotNewObject:
mov     edi, [esi+OBJECT_HEADER.anonymous_1.QuotaBlockCharged]
     这上面的代码是不是很眼熟呢,和我们申请空间中的一一对应!怎么样申请的,就怎么样释放。
这下面都是在为PsReturnSharedPoolQuota准备参数的一些判断。
代码:
test    edi, edi        ;判断一下QuotaBlockCharged是否有值,
jz      short loc_525ECB 
test    eax, eax 
jz      short loc_525EA5
mov     ecx, [eax+_OBJECT_HEADER_QUOTA_INFO.SecurityDescriptorCharge]
add     ecx, [eax+_OBJECT_HEADER_QUOTA_INFO.PagedPoolCharge] 
mov     eax, [eax+_OBJECT_HEADER_QUOTA_INFO.NonPagedPoolCharge]
jmp     short loc_525EBF
loc_525EA5:
test    dl, 20h    ; dl是flags;OB_FLAG_DEFAULT_SECURITY_QUOTA
mov     ecx, [ecx+OBJECT_TYPE.TypeInfo.DefaultPagedPoolCharge] ; ecx指向ObjectType
jz      short loc_525EB6
add     ecx, 800h
loc_525EB6:
mov     eax, [ebp+var_ObjectType]
mov     eax, [eax+OBJECT_TYPE.TypeInfo.DefaultNonPagedPoolCharge]
loc_525EBF:
push    eax             ; int
push    ecx             ; int
push    edi             ; P
call    PsReturnSharedPoolQuota ; 
    这段代码将指定池类型的池配额返回给指定进程。
代码:
and     [esi+OBJECT_HEADER.anonymous_1.ObjectCreateInfo], 0
loc_525ECB: 
test    ebx, ebx        ; ebx指向OBJECT_HEADER_HANDLE_INFO
pop     edi
jz      short loc_525EE2
test    [esi+OBJECT_HEADER.Flags], 40h ; OB_FLAG_SINGLE_HANDLE_ENTRY
jnz     short loc_525EE2
push    0               ; Tag
push [ebx+_OBJECT_HEADER_HANDLE_INFO.anonymous_0.HandleCountDataBase]
call    ExFreePoolWithTag
  这里将申请的堆空间释放。
代码:
And    [ebx+_OBJECT_HEADER_HANDLE_INFO.anonymous_0.HandleCountDataBase], 0
loc_525EE2: j
mov     eax, [ebp+var_NameInfo]
test    eax, eax
pop     ebx
jz      short loc_525F00
mov     eax, [eax+_OBJECT_HEADER_NAME_INFO.Name.Buffer]
  有OBJECT_HEADER_NAME_INFO头的话再判断一个Buffer,看下是否有值,有值的话代表申请了空间,要释放之。
代码:
test    eax, eax
jz      short loc_525F00
push    0               ; Tag
push    eax             ; P
call    ExFreePoolWithTag 
mov     eax, [ebp+var_NameInfo]
and     [eax+_OBJECT_HEADER_NAME_INFO.Name.Buffer], 0 
loc_525F00:
mov     eax, [ebp+var_ObjectType] ;  看下有没有OBJECT_YTPE结构。
test    eax, eax
mov     [esi+OBJECT_HEADER.Type], 0BAD0B0B0h
  ExFreePoolWithTag函数的第二个参数是一个Tag,将上面的转成字符串,可以知道是” TjbO”,这个我们在申请的时候已经见到过了。
代码:
pop     esi
jnz     short loc_525F16
mov     eax, 546A624Fh
jmp     short loc_525F1C
loc_525F16:
mov     eax, [eax+OBJECT_TYPE.Key]
  如果Type不为空的话,将key的值给eax,eax将作为ExFreePoolWithTag的第二个参数,也就是Tag。
代码:
or      eax, 80000000h
  最后这二个参数都要与PROTECTED_POOL做一个或的运算。在申请的时候也做了这一步。
代码:
push    eax             ; Tag
push    [ebp+P]         ; P
call    ExFreePoolWithTag
       我们知道[ebp+P]一直保持的是对象的起始地址,现在真正的把对象的空间给释放了。

平台环镜:
  Windows XP SP2 + VM6.5 + WRK1.2 + Windbg + IDA5.5

参考文献
[1] 毛德操 著 《Windows 内核情景分析--采用开源代码ReactOS》  [M]电子工业出版社 2009 年5月
[2]  张帆 史彩成 著《Windows驱动开发技术详解》 [M]电子工业出版社  2008年7月

      还有几天就要毕业了,在此多谢钱方师,方老师,岳老师,赵老师,在这将近一年的时间里,对我们的教导,不仅仅是技术上,还有为人处事等等,同时还有多谢科锐的同学,是你们的陪伴,让我在这期间过得很充实,同时还有在专业学习上的交流以及指导。非常感谢。
上传的附件 ObCreateObject.pdf