WRK Handle Table技术报告

来源:互联网 发布:剑灵人女捏脸数据图文 编辑:程序博客网 时间:2024/06/06 05:38

(一)        内容介绍

Windows HandleTable 的研究基于微软的 WRK项目和《 Windows Internals》第四版。研究的绝大多数情况适应于 WindowsXP Windows2003操作系统。技术报告首先总体上介绍了 Windows下的 Handle Object HandleTable的作用和相互关系,然后依次介绍了 Handle的数据结构、 HandleTable的数据结构,其中着重介绍了 HandleTable中的 Free Entry List的管理机制。最后介绍了在研究过程中设计的一个 windows 驱动,该驱动的作用是向应用程序开发系统内核中的HadleTable 的部分数据结构。

(二)        Handle Object and Handle Table

Handle Windows API里非常重要的概念,比如 CreateFile的系统调用返回类型是 Handle。在应用程序利用 CreateFile创建或者打开了一个文件的之后,要读取这个文件,需要传给 ReadFile 系统调用这个文件对应的Handle

Windows内核中每个打开的文件是有一个内核对象来控制的,也就是有一块内存区,而 Handle 相当于这个内核对象的一个索引。应用程序虽然不能直接引用内核态的内存区,但是可以通过Haldle 来标记对应的内核对象。

HandleTalbe是内核中数据结构,它的主要作用就是记录 Handle 与内核对象的Pointer 的对应关系。

(三)        Handle

Handle在内存中的数据结构如下:

typedef struct _EXHANDLE {

   union {

       struct {

           ULONG TagBits : 2;

           ULONG Index : 30;

       };

       HANDLE GenericHandleOverlay;

       ULONG_PTR Value;

   };

} EXHANDLE, *PEXHANDLE;

它总共占据32bits ,其中低二位TagBits 位被操作系统完全忽略掉,因此可以被应用程序自由改写。高6 位,作为Tag 来使用,其中最高位等于1 ,代表是系统HandleTable 中的Handle 。中间的24 位作为Handle 的有效值。如下:

32位Handle说明

 

 

因此,得出以下两个结论:

1.         所有的Handle 的值,都应该是4 的倍数

2.         一个HandleTable 中最多有224 Handle

另外需要注意,用户态程序可以通过Handle 操作内核态对象,内核态代码也可以通过Handle 查询内核对象的指针,即Handle 并不是专门给用户态程序准备的。

(四)        HandleTable

HandleTable数据结构的主要字段如下(包含简单注释):

typedef struct _HANDLE_TABLE {

   struct _EPROCESS *QuotaProcess;  //所属进程 PCB 的指针

   HANDLE UniqueProcessId;     //这个进程的 ProcessID

   LIST_ENTRY HandleTableList;   //所有的 HandleTAble 在内核中形成一个List,这是 Entry

   

   LONG HandleCount;     // HandleTable中有多少个有效的 Handle

   ULONG_PTR TableCode;  // HandleTable的树的根指针。

   EX_PUSH_LOCK HandleTableLock[4];  //为管理树中的 Free Entry 而提供的4 把锁

   ULONG FirstFree; // 第一个Free Entry List

   ULONG LastFree; // 第二个Free Entry List

   ……

} HANDLE_TABLE, *PHANDLE_TABLE;

有两点需要注意:

首先每个进程是有一个HandleTable 的,在HandleTable 的数据结构中已经体现出来。

其次操作系统中有一张系统HandleTable 。内核态的程序在建立Handle 的时候,可以把Handle 放到系统HandleTable 中,这样可以保证用户态程序永远不可能访问到该Handle

l  HandleTable中的 Tree

Handle Table保存了进程能够访问的所有的指针,这些指针以 Three Level 树的方式组织,三级的名字依次为LowLevel MidLevel HighLevel 。其中LowLevel 总是被创建,MidLeve/HighLevel 按需创建。

一个LowLevel 节点是一个页面大小的连续内存,相当于一个数组,数组的每个Entry 占了8 bytes (详见后面对每个Entry 数据结构的描述)。如果页面大小是212 Bytes ,那么总共有 212 /8=29 Entry。每个 Entry所对应的 Handle的值是其索引的 4倍,即这种对应关系是固定的。见下图:

 

 

另外有MidLevel HighLevel 的一些数据:

每个MidLevel 节点保存一组指向LowLevel 的指针,占据一个Page ,有210 Entry ,能检索2**19 Handle

每个HighLevel 节点保存一组指向MidLevel 的指针,含有25 Entry ,能检索224 Handle 。这里的224 即是一个HadleTable 能检索的Handle 的最大数量。

一棵最完整的树可能是以下形态:

 

 

有两个问题着重阐述一下:

1.       HandleTable如何记录 Tree的级数。

        首先保证 TreeNode 分配以4Bytes 对齐,因此根指针TableCode 的低二位就空闲下来,因此就可以利用根指针的低二位标记TableLevel ,如下:

  TableLevel = TableCode & 3

2.       如何根据Handle 来查找Entry

这个过程跟虚拟地址- 物理地址的转换很像,如下:

 

 

l  LowLevel中每个 Entry的数据结构

HighLevel MidLevel中每个 Entry结构很简单,就是指向下一级 Level的指针( 4 byte); Lowlevel中每个 Entry的数据结构要复杂一下,定义如下:

typedef struct _HANDLE_TABLE_ENTRY {

   union {

       PVOID Object;

       ULONG ObAttributes;

       PHANDLE_TABLE_ENTRY_INFO InfoTable;

       ULONG_PTR Value;

   };

   union {

       union {

           ACCESS_MASK GrantedAccess;

           struct {

               USHORT GrantedAccessIndex;

               USHORT CreatorBackTraceIndex;

           };

       };

       LONG NextFreeTableEntry;

   };

} HANDLE_TABLE_ENTRY, *PHANDLE_TABLE_ENTRY;

 

这个数据结构占据了8 个字节,具有复杂的Struct-Union 的嵌套结构。它实现了三种功能,相同的字段在不同的功能下有不同的解释,如下:

1.         用途1 HANDLE_TABLE_ENTRY 包含指向 Kernel Object的指针,这是这个数据结构的主要用途。

        此时, Object是指针字段。由于 KernelObject分配内存的时候保证 8字节对齐,指针的低三位空闲作为 tag ObjectAttributes就是用来操作 tag的。三个 tag位中,有一位是作为 Handle是否被加锁的标记,有一位是作为 Handle是否可以继承给子进程的标记。 GrantedAccess字段是作为 Handle的安全信息的标记。

2.         用途2 :形成空Entry 的链表。

        此时 Object字段一定要是 0 NextFreeTableEntry保存了下一个空 Entry所对应的 Handle值。

3.         用途3 :指向一个Lowlevel 中所有Handle 的统计信息。

        LowLevel 节点的第一个 Entry 永远也不会是有效的Hanlde 。即数值n*29 *4 (n=0,1,2,..)肯定不是有效的 Handle数值。 HANDLE_TABLE_ENTRY此时做如下解释: NextFreeTableEntry保存常量 EX_ADDITIONAL_INFO_SIGNATURE(定义为 -2)作为标记, Object指向了统计信息数组的指针,这个数组总共有 29 Entry ,每个Entry 的内容由统计模块解释。

        统计信息并非总是需要的,因此并没有 Handle_Table_Entry 结构中添加字段,而是在需要的时候,单独分配一个数组,在LowLevel 的第一个Entry 中放一个指向这个数组的指针。

l  Free Entry List

HandleTable Tree LowLevel Free Entry是以 List的方式来管理的,因此在打开新的 Handle的时候,能够迅速查到 Free Entry Handle Table中的维护 Free Entry List用到了无锁同步的方法,但是无锁同步会导致典型的 A-B-A 问题,HandleTable 中提供了一个解决方案。下面依次描述这三项。

1.         无锁同步:

如果要修改一个共享内存的内容,比如int * des ,可以用下面的方式进行:

while( 1 ){

  oldValue = *dest; // Step1 读取老值

  newValue = oldValule * 100; // Step2 根据老值计算新值

  if(InterlockedCompareExchange(&dest, newValue, oldValue)) // Step3 设置新值

     break;

}

InterlockedCompareExchange是一个原子操作,它的功能是判断 dest内存中当前的内容和 oldValue是否相等。如果不相等,则认为从 Step1-Step2之间, dest内存已经被别的进程改写,因此本次写入失败,进入下次 while 循环;如果相等,则为dest 内存没有被改写,本次写入成功,退出while 循环。

可以看到,对于一个孤立的内存地址,上面的代码是正确的。

2.         A-B-A问题:

要用无锁操作来实现一个List ,主要是要用上面的代码对push pop 操作中的list head 进行保护,即要通过上面的while 循环来改写head 地址。这在大部分情况下是成功的,但是在以下情况下会出现错误:

 

上面是一个list ,下面是操作这个list 的两个进程P1 P2 。其中P1 pop 代码展开了,P2 pop push 的代码没有展开。注意的是,P1 的执行过程中发生了进程切换,P2 插入后执行了一段代码。

 

P2在执行结束之后, list变成了

 

当重新切换到P1 的时候,P1 堆栈中的old 变量的值和head 指针指向的值是相等的,因此head 指针会指向已经被删除的节点B ,此时就发生了严重错误。

错误发生的原因是,P2 pop 了两次之后,重新push 了老的头节点;而P1 是通过head 的指针内容来判断写入head 指针是否成功。即内存空间从A 改成了B ,又改成了A ,绕过了InterlockedCompareExchange的判断机制导致了错误,所以这种错误通常被称为 A-B-A 错误。

3.         HandleTable中的解决方案:

这个是从WRK 代码中整理出来的伪代码:

#define LOCK_IDX(handle) ((handle) >> 2) % 4)

void push( handle ) {

   if(Locks[LOCK_IDX(handle)].IsSharedLock) // 这个handle FirstFree 使用相同的锁

       push to LastFree List;

   else

       push to FirstFree List;

}

void pop( ) {

   while( 1 ) {

     if(FirstFree List is empty)

         Move LastFree list to FirstFree list // 这里有可能发生阻塞

     Locks[LOCK_IDX(FirstFree)].lockshared(); // 这里是不会互相阻塞的,因为是共享锁

     if(InterlockedCompareExchange(&FirstFree, Next,FirstFree))

         break;

     Locks[LOCK_IDX(FirstFree)].unlockshared();

   }

}

 

需要注意的是,在HandleTable 的数据结构中有EX_PUSH_LOCK HandleTableLock[4]字段,这就是上述伪代码中操作的锁。

做两点解释:

1.         基本思路是保证在Pop 的过程中,不会push Head 相同的Handle ,这样就避免了前面描述的ABA 问题。

2.         这里分配了两个List push 的时候,按照共享锁的分配情况判断push 到哪个list 中;在pop 的时候,如果第一个list 是空的,则把第二个List 转移到第一个List 中。

(五)        利用驱动程序读取HandleTable

下面介绍一下写的一个工具。这个工具包含一个Windows 驱动程序和一个读取该驱动程序的应用程序。工具的主要功能是读取当前运行的Windows 系统中的所有进程的HandleTable ,列出有效handle 的数量和Tree 的级数。该驱动的功能代码由研究者完成,驱动的框架借鉴了《Windows 驱动开发详解》中的代码。

下面是运行的截图:

 

从结果可以看出,系统中大部分的Handle 的数量比较少,对应的HandleTable 的级数是0 (即一级LowLevel )。从HandleTable 的设计来看,在只有一级LowLeve 的时候,HandleTable Tree 就退化成为一个数组,因此在保证所有的情况都可用的条件下,最大程度的提高了大多数情况的运行效率。

0 0