3.8 对象与句柄

Windows操作系统把一切都作为对象(OBJECT)来管理,进程是对象,线程是对象,本书第1章提到的驱动也是对象,对象可以有名字,也可以没有名字,如果根据是否有名字来划分的话,对象可以分为命名对象与匿名对象,如在应用层创建一个EVENT或MUTEX时,就可以根据需求来决定创建的是命名对象还是匿名对象。

绝大部分对象在内核中产生,在内核中销毁,由内核组织与维护,这些对象被称为内核对象。前面提到的进程对象、线程对象、EVENT对象、MUTEX对象,都是内核对象。在下文中提及的对象,如没有特别说明,全部指内核对象。

不同类型的内核对象,拥有不同的属性内容,如文件类型的对象保存了文件的名字、卷设备信息、文件读写偏移等等;进程对象保存了进程的ID、线程信息、优先级等等。

考虑这样一个场景,用户态进程需要创建一个EVENT对象并对其进行操作,但是这个对象由内核创建,对象所在的内存地址也属于内核态地址空间,用户态进程无法访问这个对象地址怎么办呢?

为了解决这个问题,系统提供了“句柄”(HANDLE),简单来说,每个进程都有一个表,这个表中的每一项保存着需要访问的内核对象信息,系统为用户态应用程序提供一个“句柄”值,这个句柄值实际上是这个表的某种索引,通过这个值,可以在表中定位到具体需要访问的内核对象信息。用户态程序通过API创建或打开一个内核对象时,这个表中的信息会增加一项,用来描述这个内核对象的信息,并产生一个相应的句柄值,用户态程序把这个句柄传递到相应API,API进入内核后,通过这个句柄值定位到需要操作的内核对象,对内核对象进行相应的操作。

句柄就好比是内核对象的凭证,通过这个凭证,用户态程序可以间接操作内核对象。一般来说,把进程中保存内核对象信息的表称为“句柄表”,实际上,句柄表并不是一维的单表结构,而是一个二维甚至三维的多表结构,但不管怎么样,句柄值与句柄表中表项的映射关系是存在的,通过一个句柄值,可以唯一地定位到表中具体的一项。

在用户态中,句柄值只在当前进程中有意义,这是由于不同进程有各自的句柄表,句柄值只针对自身句柄表有意义。在内核态中,这个概念依然适用,但是除了进程各自的句柄表,还存在一个系统的句柄表,这个系统的句柄表存在于SYSTEM进程中。前面曾经介绍过,对于内核态来说,地址空间是共享、非隔离的(绝大部分情况),也就是说,对于内核态的驱动程序来说,系统的句柄表只有一个,所有内核驱动程序都可以使用内核句柄表。为了表述清晰,本书后面把系统句柄表中的句柄,称为“内核句柄”。

多年前笔者刚接触系统句柄表时曾有个疑问,既然每个进程都有一个句柄表,为何系统还额外提供一个系统句柄表?这不是多此一举吗?请读者复习一下本章开头提到的“上下文”概念,并假设存在这样一个场景:驱动程序的一个函数F1在进程P1上下文中执行,创建或打开一个EVENT对象,并获取句柄值,假设句柄值为0x8,这个句柄值是相对于进程P1的句柄表;驱动程序另外一个函数F2在进程P2上下文中执行,F2函数需要通过先前创建的句柄(句柄值为0x8)来操作对象,由于函数F2处于进程P2上下文,通过值为0x8的句柄去操作对象时,实际上操作的是进程P2句柄表中句柄值为0x8的内核对象,出现了张冠李戴现象。对于此类错误,驱动代码虽然可以通过修改函数F2的上下文来规避问题,但这并不是一个最好的解决方法,正确的做法是,函数F1在创建或打开EVENT时,指定句柄类型为“内核句柄”,由于内核句柄存在于系统句柄表中,无论函数F1与函数F2处于哪个进程上下文,访问该句柄都不会出现问题。

在WDK中,句柄的定义为HANDLE,具体为:

最后介绍内核对象的引用计数,与用户态的COM模型类似,每个内核对象存在两个计数,一个称为“句柄计数”,另一个称为“指针计数”,句柄计数是指这个内核对象被多少个句柄值所指向,如在用户态中创建一个命名的EVENT对象,获取到一个句柄,那么这个EVENT的句柄计数就是1,当其他程序通过该EVENT名字打开该EVENT时,会获取到另外一个句柄,这时候,句柄计数等于2。指针计数是在句柄计数基础上递增的计数,在刚才所提的例子中,句柄计数等于2,指针计数也等于2,句柄计数的增加,会相应导致指针计数增加,同理,句柄计数的减少,会相应导致指针计数减少,但指针计数可以独立增加与减少而不影响句柄计数。当一个对象的指针计数等于0的时候,这个对象会被系统释放。请注意,不同操作系统,系统对引用计数值的管理稍有不同,上面只是列举了一个简单的例子,实际情况还需要以具体的操作系统为准。

下面以EVENT作为例子,为读者展示一段句柄的操作:

代码中首先使用ZwCreateEvent函数创建一个EVENT对象,该函数原型如下:

参数EventHandle用于保存EVENT的句柄;参数DesiredAccess表示EVENT的权限;参数ObjectAttributes表示创建EVENT的属性信息;参数EventType表示EVENT的类型,取值为SynchronizationEvent或者NotificationEvent,分别表示同步类型EVENT以及通告类型的EVENT;参数InitialState表示EVENT的初始状态,TRUE表示EVENT被创建后的状态为“有信号”,否则为“无信号”。

在上面的参数中,ObjectAttributes参数的用法最为重要,ObjectAttributes描述了需要打开或创建的内核对象属性,大部分涉及打开或创建内核对象的API都会有ObjectAttributes参数。内核对象的属性用OBJECT_ATTRIBUTES结构体描述,属性包括对象的名字、根目录句柄、对象的安全描述符等,该结构体定义如下:

Length成员表示结构体的大小,一般等于sizeof(OBJECT_ATTRIBUTES),RootDirectory表示对象的根目录句柄,可以为NULL。ObjectName是一个UNICODE_STRING的指针,表示对象的路径或名字,RootDirectory与ObjectName共同组成了一个完整的对象全路径名字。Attributes表示对象打开或创建时的具体属性,常见的有:

●OBJ_INHERIT 表示内核对象的句柄可以被继承。

●OBJ_CASE_INSENSITIVE 表示内核对象的名字不区分大小写。

●OBJ_KERNEL_HANDLE 表示使用内核句柄,即句柄存在于系统句柄表中。

●SecurityDescriptor以及SecurityQualityOfService与安全性相关,可以暂设置成NULL。

在上述代码中,ZwCreateEvent成功后,句柄会保存在hCreateEvent变量中,这个句柄是一个内核句柄。接着代码中调用ObReferenceObåjectByHandle函数,获取hCreateEvent句柄对应的EVENT对象指针。ObReferenceObåjectByHandle函数原型如下:

这个函数虽然参数较多,但相对简单:

●参数Handle表示句柄值。

●参数DesiredAccess 表示需要获取此对象的权限,针对不同类型的对象,这个权限的值不同,对于本例的EVENT对象来说,这个值可以传递EVENT_ALL_ACCESS,表示EVENT的所有权限。

●参数ObjectType表示对象的类型,不同的对象用不同的“对象类型”来表示,WDK定义了一系列的“对象类型”,如*ExEventObjectType、*ExSemaphoreObjectType、*IoFileObjectType、*PsProcessType、*PsThreadType等等。在本例中参数应使用的类型是*ExEventObjectType,表示EVENT类型的对象。顺便提一下,上面列举的“对象类型”本身也是一种对象,这种对象的类型为“TYPE”类型,有兴趣的读者可以自行研究。

参数AccessMode表示访问模式,可以是KernelMode或UserMode,分别表示用户态与内核态。如果参数Handle是内核句柄,则应该传递KernelMode。

参数Object是返回参数,若函数执行成功则该参数保存对象的指针。

HandleInformation参数暂时没用,可以设置为NULL。

ObReferenceObjectByHandle函数成功则返回STATUS_SUCCESS,失败则返回错误码。

例子代码在调用ObReferenceObjectByHandle函数成功后,使用ZwOpenEvent函数再次打开刚才创建的EVENT并获取一个句柄,保存在hOpenEvent变量中;接着通过hOpenEvent的值,使用ObReferenceObjectByHandle获取EVENT的对象指针,保存在pOpenEventObject变量中。至此,代码中已经存有两个句柄以及两个对象指针,代码通过DbgPrint函数把这些信息输出:

从输出中可以看到,两个句柄的值是不同的,但是对象的指针是同一个,说明这两个句柄指向的是同一个内核对象。

句柄与对象指针使用完毕后,需要将其关闭,关闭句柄使用ZwClose函数;释放对象指针可以使用ObDereferenceObject函数,这两个函数均只有一个参数,表示需要关闭/释放的句柄/对象指针。