生活中如果你不知道什么是对象,那你需要到”非诚勿扰”走一遭,在编程的世界里,如果你不知道,那么你需要将关于”C++”思想的书好好看一看。在windows内核中也有对象的概念,与编程中的对象类似,但是要简洁好理解得多。对象是windows内核中一个非常重要的概念,我认为是一种基石性的东西,windows中的很多操作都可以归结为对对象的操作。而句柄是一个和对象关联密切的概念,为什么要用句柄呢?原因可能是这样滴:因为对象的数据结构在内核层,若想在用户层访问处于内核层的对象,就需要用户层有一个记号,好的,用户层的应用程序要操作某个对象,拿出这个记号就好了,告诉内核层,我要对它进行操作了,内核层回答:收到,完成相应的操作,所以说从某种角度来说他是对象的一个指引。这篇文章我们将谈谈windows中对象与句柄的概念,组织与管理。
一、什么是对象(Object),什么是句柄(Handle)
1、什么是对象
对象,外国名字叫Object,简单的说就是windows内核定义的一组数据结构以及一些操作的组合。在WRK中,共有31种对象,包括我们耳熟能详的进程对象,线程对象,事件对象,驱动对象,设备对象等。
一个对象的结构分为三部分:对象头,对象体和附属部分。对象头是每个对象都具备的,结构一致,对象体根据对象类型的不同而有所区别,有的较长,有的较短,还有的如事件对象就没有对象体。而附属部分是一些与对象有关的额外信息。对象的结构如图1,可以看到,对象头、对象体按照顺序依次排列下来,但是附属部分在对象头的上方,主要包括一些该对象的名称信息,句柄信息等,这样做的考虑是因为一般指向对象的指针都是指向对象头,对象头的长度是固定不变的,0x18字节,因此从这个指针向上可以找到附属部分,向下可以找到对象体,便于操作。
1)对象中的对象头结构
前面已经说过,每种对象的对象头都是一致的,其长度是不变的,为0x18,即24个字节长。其结构如下:
typedefstruct_OBJECT_HEADER{ LONG_PTRPointerCount;// 指针个数 union{ LONG_PTRHandleCount;// PVOIDNextToFree; }; 句柄个数 POBJECT_TYPEType;// 指向类型对象 UCHARNameInfoOffset;//附属信息中名称信息存放地址相对于对象头的偏移 UCHARHandleInfoOffset;//附属信息中句柄信息存放地址相对于对象头的偏移 UCHARQuotaInfoOffset;//附属信息中配额信息存放地址相对于对象头的偏移 UCHARFlags; union{ POBJECT_CREATE_INFORMATIONObjectCreateInfo;//对象创建信息 PVOIDQuotaBlockCharged; }; PSECURITY_DESCRIPTORSecurityDescriptor;//安全描述符 QUADBody;//对象体 }OBJECT_HEADER,*POBJECT_HEADER; 这个结构非常重要,其中PointCount和HandleCount成员涉及到对象的生命周期, Type成员涉及类型对象,NameInfoOffset,HandleInfoOffset,QuotaInfoOffset,是指向该对象附属部分的偏移,我们将在后面的部分进行讲解。
2)对象中的附属部分
在图1中我们看到,对象结构中的附属部分共包括四个内容:
①_OBJECT_HEADER_QUOTA_INFO中包含了该对象换页、非换页内存池的配额,安全描述符的配额,另外还利用ExclusiveProcess成员指示了拥有此对象的进程。
②_OBJECT_HEADER_HANDLE_INFO中存储着对象句柄相关的信息。
③_OBJECT_HEADER_NAME_INFO包含有对象所属的路径和对象名字等信息。
④_OBJECT_HEADER_CREATOR_INFO包含了对象头的创建信息,其中的TypeList成员,将所有同类型的对象串起来,CreatorUniqueProcess指向了对象父进程的ID。需要注意的是,对象中的附属部分是变长的,之所以这么说,是因为上述的四个部分的存在并不是必须的,例如如果对象头中的成员NameInfoOffset,HandleInfoOffset,QuotaInfoOffset为0,则表示没有其对应的结构。因为CreatorInfo紧邻对象头,只需要将对象头的地址减掉一个固定的偏移即可找到,所以在结构_OBJECT_HEADER中,并没有关于CreatorInfo的偏移。
3)对象体
对象结构中的对象体根据对象类型的不同而不同,在这里就不赘述了,将来在讲述进线程管理,设备驱动,内存管理的内容中,我们再具体对象具体讲解。
2、什么是句柄
回忆起我最开始学习编程,第一个项目便是对文件的操作,以下是经典的语句:
HANDLEpHandle=CreateFile(…,….,); ReadFile(pHandle…….); WriteFile(pHandle…….);
对一个文件,通过CreateFile函数打开它,然后获得一个句柄pHandle,接下来对该文件的读写操作都可以通过pHandle来操作。当然句柄的使用不仅局限在文件上,可以说,当一个进程利用名称来创建或打开一个对象时,将获得一个句柄,该句柄指向所创建或打开的对象,以后,该进程无须使用名称来引用该对象,使用此句柄即可访问。在Windows中,句柄是进程范围内的对象引用,换句话说,句柄仅在一个进程范围内才有效。一个进程中的句柄传递给另一个进程后,句柄值将不再有效。一个进程的句柄表包
含了所有已被该进程打开的那些对象的指针,Windows支持的句柄是一个索引,指向该句柄所在进程的句柄表中的一个表项。
每个进程有独立的句柄表,在结构EPROCESS中有成员ObjectTable,这是一个指针,指向一个_HANDLE_TABLE结构,其结构如下:
typedefstruct_HANDLE_TABLE { ULONGTableCode; PEPROCESSQuotaProcess; PVOIDUniqueProcessId; EX_PUSH_LOCKHandleLock; LIST_ENTRYHandleTableList; EX_PUSH_LOCKHandleContentionEvent; PHANDLE_TRACE_DEBUG_INFODebugInfo; LONGExtraInfoPages; ULONGFlags; ULONGStrictFIFO:1; LONGFirstFreeHandle; PHANDLE_TABLE_ENTRYLastFreeHandleEntry; LONGHandleCount; ULONGNextHandleNeedingPool; }HANDLE_TABLE,*PHANDLE_TABLE;
注意TableCode这个成员,我们可以认为它是一个指向句柄表的指针,由于句柄表的地址是以4为单位的,所以TableCode的末两位被windows用作其它用途,用来指明是几级表,如果为00,则为一级表,如为01则为二级表,为10则为三级表。通常将一个句柄表放于一个4K大小的页面中,因为每个句柄表项为8个字节,所以最多存储512个句柄项。
Windowsxp系统中将句柄的组织分为可调整的三级,如果句柄较少,那么直接使用一级表,即直接由句柄项指向对象就可以了,而当句柄增多,超过512个,那么就会启用二级句柄表,即此时句柄项指向的不是对象,而是另外一个句柄表,再由这个句柄表中的句柄项指向对象,三层句柄表的情况依此类推,句柄表的分类如图2所示。
二、Windows中对象的管理
以上我们介绍了windows中有关对象和句柄的一些基本的概念,下面我们将分别针对对象和句柄的管理进行描述。在对象的管理中,我们主要涉及到对象的类型管理,命名对象的管理和对象的周期性问题.
1)类型对象
在31种内核对象类型中有一种比较特别,叫做类型对象,用来描述每种类型的对象。这种对象只有一个实体,该对象实体包含某一种对象的共用的属性和操作,比如有多少个该类型的对象,对该类型的对象的相同的操作。
在每个对象的头部有一个POBJECT_TYPE数据类型的type成员,它指向该类型对象的唯一实体,所以每种对象type成员的内容都是一样的,通过这种方式,每种对象的一些共有属性和操作被该种类型对象所共享,能够有效的节约内存空间。
2)命名对象的管理
对象可以有名字,也可以没有名字,使用命名对象是因为命名的对象是全局性的,因此可以实现对象的跨进程访问。有名字的对象共同组成的一个名字空间,我们利用WinObj可以看到非常类似操作系统中的文件管理,通过目录对象将普通对象分层次的组织起来,如图
类似于windows操作系统中文件的组织,在这里可以完成常见的操作:查询,插入和删除对象。例如,在一个指定的目录中找到一个对象(使用对象名),使用ObpLookupDirectoryEntry函数;把一个对象插入到一个目录中使用ObpInsertDirectoryEntry函数;删除某一项使用ObpDeleteDirectoryEntry函数,上述函数都只在一个目录中进行,而不是递归的操作其下的子目录,为了实现递归查找,Windows提供了ObpLookupObjectName函数,其能够实现在某一目录下递归查询。如果想了解这些函数的实现,就必须了解对命名对象的组织。那么这些函数又是如何实现的呢?我们先从图5说起。
图5:对象的目录组织
让我们学学伟大的福尔摩斯,看看从一幅图中能够猜出些什么,从左向右,ObpRootDirectoryObject好像是一个指针,指向一个DirectoryTable的结构,而该结构应该包含37个有{next,对象指针}成员的结构,{next,对象指针}结构通过next指针连接起来,形成一个链,共有37个链,但是还有一个特殊的地方在于,如果某个对象是目录对象,那么它又包含一个数量为37的链。嗯,从图上我们看到了很多,如果我们将这里的目录对象想象成文件系统中的目录,对象想象成文件,我们很容易得到这是一个形成树状的目录结构,bingo,实际的命名对象的组织就是这样的。
下面我们用专业的语言总结一下,命名对象的组织是通过目录对象作为中枢完成的,其根目录对象由全局变量ObpRootDirectoryObject定义。Windows中的命名对象被组织成树型的结构,目录对象类型是其中的关键,Directory的body的结构如下:
typedef_DIR_ITEM{ PDIR_ITEMNext; PVOIDObject; }DIR_ITEM,*PDIR_ITEM;//就是我们从图5中看到的{next,对象指针}结构; typedefstruct_DIRECTORY{ PDIR_ITEMHashEntries[37]; PDIR_ITEMLastHashAccess; DWORDLastHashResult; }DIRECTORY,*PDIRECTORY;//就是我们在图5中看到的DirectoryTable结构;
系统利用目录对象(注意,这也是一种对象)将所有有名字的对象组织起来,目录对象可以看成是存储对象的容器,可以说它是一种特殊的对象,其利用一定的算法将对象名称处理得出一个HASH值(0--36),对应的放入37个数组元素组成的哈希数组中。举个例子,在一个目录下,针对该目录下所有的对象(包括目录对象),将对象名按照一定算法进行哈希(什么?不知道什么是哈希,那您就需要好好查查数据结构的书,哈希是一种散列的算法),将对象名变为0---36的数字,然后将对象放到其对应的数组中去,如果该数组已经有内容,那么就通过next指针形成一个单链表。这里有个特殊点,如果对象为目录对象,那么该目录对象再指向一个DirectoryTable结构,这个DirectoryTable结构又包括一个长度为37的数组,将这个目录下所有的对象放到37个链表中,如此循环下去。这里使用的HASH算法并不复杂,有兴趣的朋友可以查看WRK或者ReactOS的代码。
3)对象的生命周期
对象的生命周期与对象头中的成员PointerCount(引用计数)直接相关,一旦引用计数为零,则对象的生命周期结束,它所占用的内存也可以被回收。对象的引用计数来源于两个方面。
内核中的指针引用。当内核调用ObReferenceObjectByPointer函数,直接新增加一个对某一对象的引用,则该对象的引用计数加1;而调用ObDereferenceObject函数,不再对某一对象引用,则该对象的引用计数减1;
一个进程,注意这里是进程,类似于前面介绍的,通过CreateFile函数,打开一个对象并获得一个句柄时,那么对象头中的引用计数和句柄计数都加1,该进程以后通过此句柄来引用此对象。而当一个句柄不再被使用时,其句柄计数与引用计数都要减1。通过上面两点,可以得到,对象头中的成员引用计数(PointerCount)和句柄计数(HandleCount)之间的关系可以归结为:PointerCount=HandleCount+直接使用指针引用对象的计数。
三、Windows中句柄的管理
在这里我们只讨论windowsxp中关于句柄的管理,而对windows2000中的句柄管理不做讨论。
1)句柄的定义
还是引用前面的例子吧:
HANDLEpHandle=CreateFile(…,….,);
我们打开调试器,将断点设置在以上代码的下一条语句上,当运行停止,可以看到pHandle被赋予一个16位的值,0x2c,0x120等等类似的数字,如果你仔细观察,这个值应该是4的倍数。实际上,对于X86系统,句柄的结构如图6:
2)句柄的管理
Windowsxp中句柄表分为可调的三层。为了使我们的讲解易于理解,我们先看看一级表的结构。一个句柄表是以4K为单位的,每个条目占用8个字节,因此一个句柄表可以有512个条目,我们称这个条目为_HANDLE_TABLE_ENTRY,其结构为:
typedefstruct_HANDLE_TABLE_ENTRY{ union{ PVOIDObject; ULONGObAttributes; PHANDLE_TABLE_ENTRY_INFOInfoTable; ULONG_PTRValue; }; union{ union{ ACCESS_MASKGrantedAccess; struct{ USHORTGrantedAccessIndex; USHORTCreatorBackTraceIndex; }; }; LONGNextFreeTableEntry; }; }HANDLE_TABLE_ENTRY,*PHANDLE_TABLE_ENTRY;
图7:HANDLE_TABLE_ENTRY的结构
第一个union的关键成员是Object指针,指向句柄所代表的内核对象,它的最低3位有特别含义:
①第0位OBJ_PROTECT_CLOSE,表示调用者是否允许关闭该句柄;
②第1位OBJ_INHERIT,指示该进程所创建的子进程是否可以继承该句柄,即是否将该句柄项拷贝到子进程的句柄表中;
③第2位OBJ_AUDIT_OBJECT_CLOSE。指示关闭该对象时是否产生一个审计事件。在第二个union中,如果句柄表项指向一个有效的对象,那么GrantedAccess成员记录了该句柄的访问掩码;如果这是一个空闲的句柄表项,那么,NextFreeTableEntry成员将加入到句柄表的空闲单链表中。
3)快速查找空闲句柄的方法
为了有效快速的管理句柄项,Windows使用_Handle_Table结构中的FirstFreeHandle成员与_Handle_Table_Entry中的NextFreeTableEntry成员,采用类似链表的管理方法。
我们看图8中左图,FirstFreeHandle指向该句柄表中目前第一个空闲的句柄项,而因为该句柄项为空,因此使用NextFreeTableEntry定义,利用其NextFreeTableEntry成员指向下一个空闲的句柄项,以此类推,这样通过NextFreeTableEntry成员将空闲的句柄项组成一个单链表,当系统需要一个空闲的句柄项时,则依据FirstFreeHandle,定位到其指向的句柄项,而将FirstFreeHandle填充为该句柄项中原来的NextFreeTableEntry内容,如图中中图所示,而当一个在用的句柄项转为空时,则将其地址赋给FirstFreeHandle,而其空出来的NextFreeTableEntry指向原来FirstFreeHandle指向的句柄项,如右图所示。
FirstFreeHandleFirstFreeHandle FirstFreeHandle _Handle_Table_Entry FirstFreeHandle _Handle_Table_Entry NextFreeTableEntry _Handle_Table_Entry NextFreeTableEntry NextFreeTableEntry _Handle_Table_Entry NextFreeTableEntry NextFreeTableEntry NextFreeTableEntry NextFreeTableEntry NextFreeTableEntry
4)一个句柄管理的例子
好了,到这里我们对句柄,句柄表,句柄项都已经有所了解,下面我们利用一个例子将这个概念串联起来。题目是这样滴,目前已知一个进程,如何得到句柄为0x24指向的对象呢?
小结
这篇文章短短的篇幅当然不能包括对象与句柄中所有的内容,比如windows中还有一个特殊的表,PspCidTable,它是一个包含所有进线程句柄的表,通过它可以遍历所有的进线程,又如在命名对象的管理中除了目录对象和其它对象,还允许符号连接的存在。我们讲述的重点是将对象和句柄的概念以及其管理算法的思路描述清楚,在以后文章中还会包括对象和句柄的操作。
-------------------------------------------------------------------------------