这个是武汉科锐 第三阶段 的分析项目,每个同学负责不同的模块。在此我将我所分析的模块发出来。
声明:由于本人能力有限,文中必然有错漏之处,恳请读者不吝赐教。
ObInsertObject它的作用是将所创建的对象插入对象目录和本进程句柄表(或内核句柄表),并返回句柄。插入对象目录的目的是使得以后可以按对象名及期路径找到对象,而句柄表的插入则使当前进程在创建之余同时打开了这个对象。
我们先来了解一下ObInsertObject的大概流程:
首先是一些常规检查,如果对象名没有指定的话,创建一个无名对象句柄,同时将ObpCreateUnnamedHandle的返回值返回。
然后再判断一个访问状态(AccessState)是否为空,如果为空的话就调用SeCreateAccessState创建一个,如果创建出错了,返回错误码。
接着再判断对象名是否为空,如果不为空的话,调用ObpLookupObjectName查找一下目录下面是否有同名的存在, 如果就通过指针返回对象,否则就返回STATUS_OBJECT_NAME_NOT_FOUND,如果没有找到就把这个对象挂入给定的目录,并通过指针返回这个对象的指针。
接着再判断是不是要创建符号连接,是的话通过ObpCreateSymbolicLinkName创建。
下面再判断是不是成功插入对象目录,再根据所在目录修正目标对象的访问控制描述。
最后就是句柄表中创建一个新的句柄。
现在我们开始分析ObInsertObject函数。
首先函数完成对一些局部变量的赋值,比如取得ObjectType等操作。
接着先判断一下是不是要创建无名对象句柄。
代码:
cmp [ecx+OBJECT_TYPE.TypeInfo.SecurityRequired], 0 jnz loc_527F99 cmp [ebp+var_ObjectName], eax ; 判断一下对象名有没有指定 jnz loc_527F99
之后再检查一下_OBJECT_CREATE_INFORMATION.SecurityDescriptor这个域是否有值,有值的话调用SeReleaseSecurityDescriptor()将安全描述描给释放。
接着再判断是否有_OBJECT_HEADER_NAME_INF可选头,有的话也将空间释放,这里有一个检测,检测是从哪里申请到的空间,是LookasideList还是在堆里,然后调后相应的函数释放空间。
接着再判断_OBJECT_HEADER_NAME_INFO.Directory是否有值。
代码:
mov eax, [edi+_OBJECT_HEADER_NAME_INFO.Directory] cmp eax, ebx
接着再调用ObfDereferenceObject函数。
基本上后面都是一些善后操作。
最后返回ObpCreateUnnamedHandle()函数的返回值,结束整个函数的调用。
如果不是创建无名对象,函数接着往下走。
如果名字指定了,或者需要完整的安全检查,是需要访问状态的,所以首先判断AccessState是否有值,这个参数是ObInsertObject函数的参数之一。
代码:
cmp [ebp+arg_AccessState], eax ; eax为零值,前面进行了xor eax,eax jnz short NoCreateAccessState
代码:
lea eax, [ebp+var_LocalAccessState] mov [ebp+arg_AccessState], eax mov eax, [ebp+var_ObjectType] add eax, 68h ; 'h' ; 这里做个加法就指到GenericMapping push eax push [ebp+arg_DesiredAccess] ; lea eax, [ebp+var_AuxData] push eax lea eax, [ebp+var_LocalAccessState] push eax call SeCreateAccessState ; 创建一个访问状态
我们继续再往下看。
这里有一个调用ObpValidateAccessMask函数,同时后面还有针对函数成功与否做的一些判断以及相关的操作,失败依旧是释放相关的资源,但是跟进这个函数发现,如论怎样,他都返回STATUS_SUCCESS。所以这里我们跳过了。
代码:
mov eax, [ebp+arg_AccessState] mov ecx, [esi+_OBJECT_CREATE_INFORMATION.SecurityDescriptor] push eax mov [eax+ACCESS_STATE.SecurityDescriptor], ecx call ObpValidateAccessMask ;无论怎么样,他都返回STATUS_SUCCESS
下面先是判断ObjectName是否为空,不为空的话调用ObpLookupObjectName函数,这个函数的作用是通过ObjectName去对象目录中查找,如果没有找到的话将ObjectName插入,同时通过最后一个参数返回结果,原来最后一个参数是指向我们的对象本身的,如果对象目录中找到有这个对象的话,将会将会改写这个值,通过这个值我们可以判断有没有将我们的对象插入进去,还是说找到同名的。
代码:
lea eax, [ebp+var_InsertObject] push eax ; int lea eax, [ebp+var_34] ; _OBP_LOOKUP_CONTEXT类型的变量的首地址 push eax push [ebp+arg_AccessState] xor eax, eax mov al, [ebx+0Fh] push ecx ; int push [esi+_OBJECT_CREATE_INFORMATION.SecurityQos] ; int shr al, 1 push [esi+_OBJECT_CREATE_INFORMATION.ParseContext] ; int not al and eax, 0FFFFFF01h push eax ; AccessMode push [ebp+var_ObjectType] push [esi+_OBJECT_CREATE_INFORMATION.Attributes] push [ebp+var_ObjectName] push [esi+_OBJECT_CREATE_INFORMATION.RootDirectory] call ObpLookupObjectNam
发现他没有直接单纯的判断函数有没有成功,而是做了一个检查,判断一下是不是找到同名的。
代码:
test eax, eax ; 判断函数是否执行成功 mov [ebp+Status], eax jl loc_5281BD mov ecx, [ebp+var_InsertObject] test ecx, ecx ; 判断var_InsertObject是否为零 jz loc_5281B3 cmp ecx, [ebp+arg_Object] ; 判断var_InsertObject是不是等于Object ; 如果相等的话,说明已经把Object插进对象目录 ; 否则的话说明在里面找到同名的
先判断属性中是否有OBJ_OPENIF属性,有的话再判断类型是否相同,不同的话将Status置为0C0000024h(STATUS_OBJECT_TYPE_MISMATCH),相同的话置Status为40000000h (STATUS_OBJECT_NAME_EXISTS),然后再跳去下面判断ObjectType是否等于ObpSymbolicLinkObjectType,接着做相应的操作。
如果属性中没有OBJ_OPENIF属性的话,再判断一下类型是否等于ObpSymbolicLinkObjectType,是的话调用ObfDereferenceObject函数。最后再将Status置为0C0000035h(STATUS_OBJECT_NAME_COLLISION)。
之后下面再判断一下Status是否成功(注意上面有修改Statue的操作),不成功的话做一些资源的释放,最后再返回,结束函数,成功的话再判断类型是否是ObpSymbolicLinkObjectType,是的话调用ObpCreateSymbolicLinkName。
这个函数比较大,而且跳来跳去也比较麻烦,为了方便大家理解,我画了一张流程图出来。
接着我们往下分析。首先判断是否将我们的对象插入到了对象目录里面去了。
代码:
mov eax, [ebp+var_InsertObject] cmp eax, [ebp+arg_Object] ; jz loc_5282D4 ; 如果相等,说明将我们的对象插入到对象目录中去了
代码:
test edi, edi ; 先判断ObjectName是否为空 jz short loc_5282EE mov eax, [edi+_OBJECT_HEADER_NAME_INFO.Directory] test eax, eax jz short loc_5282EE
接着在下面调用ObAssignSecurity函数, 根据所在目录修正目标对象的访问控制描述。接着很自然的在后面又做了一些函数调用成功以及返回值的判断,然后是相关的操作,比如失败了,一些资源的释放。
接着下面这个估计大家都比较感兴趣的了。
先是判断Handle是否有值。
代码:
cmp [ebp+arg_Handle], 0 mov [ebp+var_ReturnStatus], eax jz HandleIsZero ; 如果arg_Handle等于零的话跳走
代码:
cmp eax, ebx ; 这里判断函数有没有执行成功了 mov [ebp+Status], eax jge loc_528491 ; 成立就跳走 cmp [ebp+var_ObjectName], ebx jz short loc_528189 push [ebp+arg_Object] call ObpDeleteNameCheck ; 将对象从对象目录里删除
接着后面是对我们之前申请的一些资源的释放工作。
好了,我们现在开始分析ObpLookupObjectName函数。
在我们开始分析ObpLookupObjectName之前,先来看二个数据结构。
代码:
kd> dt _OBJECT_DIRECTORY ntdll!_OBJECT_DIRECTORY +0x000 HashBuckets : [37] Ptr32 _OBJECT_DIRECTORY_ENTRY +0x094 Lock : _EX_PUSH_LOCK +0x098 DeviceMap : Ptr32 _DEVICE_MAP +0x09c SessionId : Uint4B
代码:
kd> dt _OBJECT_DIRECTORY_ENTRY ntdll!_OBJECT_DIRECTORY_ENTRY +0x000 ChainLink : Ptr32 _OBJECT_DIRECTORY_ENTRY +0x004 Object : Ptr32 Void +0x008 HashValue : Uint4B
基本上从上面的二个结构体中可以大致的知道对象目录是怎么组织起来的了。下面我们看对象目录的图表。
在看代码之前,我大概的说下这个函数的工作流程:
首先是一大堆的检查,比如必须是以\开头的,是否区分大小写之类的。
函数最重要的部份将对象名一个一个的分离出来,然后调用ObpLookupDirectory
Entry去查找,如果没有找到,则调用ObpInsertDirectoryEntry。
后面就是一些资源的释放工作了。
其实这个函数的实现不难,只是太繁琐了。
ObpLookupDirectory、ObpInsertDirectoryEntry这二个函数都相对简单,我们等下再分析。
我们结合上面的图,用一个实例来演示一下如查找\aaa\bbb\ccc的。
ObpLookupDirectory参数中有一个是指定查找的目录,刚开始值一定为空的,然后做个检查,是否为空,空的话说明是要根目录下查找,就把根目录赋给他。
然后函数先分离出第一个对象名,第一次先把第一个’\’给跳过,第二次再去找后面第一个’\’,这二个长度一减,就可以得到当前第一个对象名的长度了,然后通过对象名再去目录下面找,第一次我们要找的是AAA,先算出他的哈希值,再将哈希值与37取下模,即可得到在数组中的哪一项了,接着再在链表中一个一个的查找,第一个就是AAA,我们很快的就可以找到了,找到了ObpLookupDirectory当然会提示的,接着我们再分离出第二个对象名BBB,我们可以看到AAA下面有三个对象,对象A、对象B、以及一个对象目录BBB,好了,BBB就是我们要找的,接着再在BBB目录下面找CCC,OK,可以找到。
但是我们找\AAA\BBB\DDD呢,怎么处理。步骤和上面的一样,当然我们在BBB目录中是找不到DDD的,然后函数调用ObpInsertDirectoryEntry将对象插到对象目录中去。一般也没问题的。
但是假如我们要找\AAA\DDD\EEE,可是DDD不存在,很显然,如果将对象插进去就会出错。
其实我觉得这个也挺好的理解,因为我们经常在用,平时打开磁盘,是不是也是这个结构呀!
可能刚开始看那个图有点郁闷,怎么这个指的是对象,那个指的又是目录呢,更奇怪的是居然还有一个符号连接呢!而上面给的二个结构中却没有什么标志呀!其实在Object中,有一个成员是不是ObjectType呢!这下明白了吧!
现在我们来看函数吧,因为这个函数太多检查了,我们直接跳到最骨干的部分看吧!
首先是分离对象名。
下面是步过第一个’\’,比如我们要查找\aaa\bbb\ccc,现在这里就得到aaa\bbb\ccc。
代码:
cmp word ptr [ebp+var_RemainingName], bx mov [ebp+var_Object], ebx ; object = NULL jz short loc_523F9D cmp word ptr [eax], 5Ch ; '\' ; 判断第一个字符是不是'\' jnz short loc_523F9D inc eax inc eax ; 因为是UNICODE_STRING,所以要加二次 add word ptr [ebp+var_RemainingName], 0FFFEh ; 这里减1 mov [ebp+var_UnicodeBuffer], eax
代码:
cmp word ptr [eax], 5Ch ; '\' ; 这里找到下一个'\' ; 这样就可以分离出第一个对象名了 jz short loc_523FBE inc eax inc eax add word ptr [ebp+var_RemainingName], 0FFFEh mov [ebp+var_UnicodeBuffer], eax jnz short loc_523FAB ; 这里找到下一个'\'
之后调用一个很重要的函数。
代码:
cmp [ebp+arg_InsertObject], ebx push esi ; int setz al push eax ; int push [ebp+arg_Attributes] ; int lea eax, [ebp+String2] push eax ; String1 push edi ; int call ObpLookupDirectoryEntry ; 在指定目录中查找
如果调用ObpLookupdirectoryEntry没有找到的话,我们就尝试将对象插入到对象目录中去。
代码:
cmp eax, ebx mov [ebp+P], eax jz short loc_524400 push esi push [ebp+arg_LookupContext] push [ebp+var_Directory] call ObpInsertDirectoryEntry
如果ObpLookupDirectoryEntry成功,再继续重复上面的操作。
最后还是要做一大堆的检查,比如是否将我们的对象插入了,还是在里面找到了之类的。是否还要继续查找等等。
然后再判断标志位,是否再进行下一轮的查找。
下面我们进去分析一下ObpLookupDirectoryEntry函数是如何实现的。
上面我们看到结构体_OBJECT_DIRECTORY_ENTRY中有一个HashValue,理所当然,这个函数一开始将计算对象名的哈希值,我们这里直接跳过。
计算出哈希值之后,和37(25h)做个除法,因为我们是取模,所以去dx中取结果。
接着我们再链表中一个一个的查找。
代码:
LoopFind: mov eax, [ebp+var_HashValue] cmp [esi+_OBP_LOOKUP_CONTEXT.HashValue], eax ;
代码:
jnz short Condition ; 这里取链表的下一个结点 mov eax, [esi+_OBP_LOOKUP_CONTEXT.Object] movzx ecx, byte ptr [eax-0Ch] push dword ptr [ebp+CaseInSensitive] ; CaseInSensitive sub eax, 18h sub eax, ecx add eax, 4 push eax ; String2 push [ebp+arg_Name] ; String1 call RtlEqualUnicodeString ;
代码:
test al, al jnz short loc_5236C2 Condition: mov ebx, esi ; 这里取链表的下一个结点 mov esi, [esi+_OBP_LOOKUP_CONTEXT.Directory] test esi, esi jnz short LoopFind
eString函数来比较二个对象名是否相同。这样大大的提高了查找的速度。
同时下面还有一个很重要的操作。
代码:
mov eax, [esi] ; esi指向的是当前已经找到的对象名的结点 mov [ebx], eax ; ebx是指向上一个的,现在上一个节点的ChainLink就等于当前结点的ChainLink mov eax, [edi] ; edi是链表的第一个结点 mov [esi], eax mov [edi], esi ; 经过这些操作之后,就把找到的对象移到链表头
好了,这个函数的分析就到此结束了!
虽然程序中有用到ObpInsertDirectoryEntry函数我们没有分析,但是我想大家看完上面的分析报告,已经可以想得到怎么做了吧!申请一个链表节点,把哈希值一算,往链表头部一插就完事了!
平台环镜:
Windows XP SP2 + VM6.5 + WRK1.2 + Windbg + IDA5.5
参考文献
[1] 毛德操 著 《Windows 内核情景分析--采用开源代码ReactOS》 [M]电子工业出版社 2009 年5月
[2] 张帆 史彩成 著《Windows驱动开发技术详解》 [M]电子工业出版社 2008年7月
马上就要毕业了,在此多谢钱方师,方老师,岳老师,赵老师,在这将近一年的时间里,对我们的教导,不仅仅是技术上,还有为人处事等等,同时还有多谢科锐的同学,是你们的陪伴,让我在这期间过得很充实,同时还有在专业学习上的交流以及指导。非常感谢。