首页 > WinDriver > WD-句柄表(ObjTable)(WRK)
2015四月6

WD-句柄表(ObjTable)(WRK)

[隐藏]

1.句柄表

在Windows中,句柄是进程范围内的对象引用,换句话说,句柄仅在一个进程范围内才有效。一个进程中的句柄传递给另一个进程后,句柄值将不再有效.句柄是一个索引,指向该句柄所在进程的句柄表(HANDLE_TABLE)中的一个表项.

EPROCESS 数据结构的ObjectTable域指向进程的句柄表。句柄表的第一个索引为4,第二个为8,依此类推。一个进程的句柄表包含了所有已被该进程打开的那些对象的指针.

kd> dt EPROCESS -y Obj 84a06bf8  
nt!EPROCESS
   +0x0d4 ObjectTable : 0xe1368888 _HANDLE_TABLE
kd> dt 0xe1368888 _HANDLE_TABLE
ntdll!_HANDLE_TABLE
   +0x000 TableCode        : 0xe106d000
   +0x004 QuotaProcess     : 0x84a06bf8 _EPROCESS
   +0x008 UniqueProcessId  : 0x00000ad0 Void
   +0x00c HandleTableLock  : [4] _EX_PUSH_LOCK
   +0x01c HandleTableList  : _LIST_ENTRY [ 0x8089df00 - 0xe14b3dec ]
   +0x024 HandleContentionEvent : _EX_PUSH_LOCK
   +0x028 DebugInfo        : (null) 
   +0x02c ExtraInfoPages   : 0n0
   +0x030 FirstFree        : 0x40
   +0x034 LastFree         : 0
   +0x038 NextHandleNeedingPool : 0x800
   +0x03c HandleCount      : 0n18
   +0x040 Flags            : 0
   +0x040 StrictFIFO       : 0y0

HAND_TABLE的详细定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
typedef struct _HANDLE_TABLE {
    ULONG_PTR TableCode;//  指向句柄表的存储结构
    struct _EPROCESS *QuotaProcess;// 句柄表的内存资源记录在此进程中 
    HANDLE UniqueProcessId;// 创建进程的ID,用于回调函数  
 
#define HANDLE_TABLE_LOCKS 4
    EX_PUSH_LOCK HandleTableLock[HANDLE_TABLE_LOCKS];// 句柄表锁,仅在句柄表扩展时使用 
    LIST_ENTRY HandleTableList;// 所有的句柄表形成一个链表,链表头为全局变量HandleTableListHead  
    EX_PUSH_LOCK HandleContentionEvent;// 若在访问句柄时发生竞争,则在此推锁上等待
 
    PHANDLE_TRACE_DEBUG_INFO DebugInfo;// 调试信息,仅当调试句柄时才有意义  
 
    LONG ExtraInfoPages;// 审计信息所占用的页面数量  
 
    ULONG FirstFree;// 空闲链表表头的句柄索引
    ULONG LastFree;// 最近被释放的句柄索引,用于FIFO 类型空闲链表  
 
    ULONG NextHandleNeedingPool;// 下一次句柄表扩展的起始句柄索引  
 
    LONG HandleCount; // 正在使用的句柄表项的数量  
 
    union {
        ULONG Flags;// 标志域  
        BOOLEAN StrictFIFO : 1;// 是否使用FIFO 风格的重用,即先释放先重用  
    };
} HANDLE_TABLE, *PHANDLE_TABLE;

TableCode域是一个指针,指向句柄表的最高层表项页面,它的低2 位的值代表了当前句柄表的层数。也就是说,如果TableCode 的最低2 位为0,说明句柄表只有一层, 此种情况下该进程最多只能容纳512个句柄( 宏定义LOWLEVEL_THRESHOLD);如果TableCode 的最低2 位为1,则说明句柄表有两层,此种情况下该进程可容纳的句柄数是512×1024(宏定义MIDLEVEL_THRESHOLD),即TableCode 指向一个中间层句柄表页面,该页面包含1024 个指针,每个指向一个最低层句柄表页面;如果TableCode 的最低2位为2,则说明句柄表有三层,此种情况下三层树结构可容纳的句柄数是512×1024×1024(宏定义HIGHLEVEL_THRESHOLD),但Windows 执行体限定每个进程的句柄数不得超过1<<24=16777216(宏定义MAX_HANDLES).

#define MAX_HANDLES (1<<24)
#define LOWLEVEL_COUNT (0x1000/ sizeof(HANDLE_TABLE_ENTRY))
#define MIDLEVEL_COUNT (0x1000/ sizeof(PHANDLE_TABLE_ENTRY))
#define HIGHLEVEL_COUNT  MAX_HANDLES / (LOWLEVEL_COUNT * MIDLEVEL_COUNT)
#define LOWLEVEL_THRESHOLD LOWLEVEL_COUNT
#define MIDLEVEL_THRESHOLD (MIDLEVEL_COUNT * LOWLEVEL_COUNT)
#define HIGHLEVEL_THRESHOLD (MIDLEVEL_COUNT * MIDLEVEL_COUNT * LOWLEVEL_COUNT)

1.png

执行体在创建进程时,首先为新进程分配一个单层句柄表。句柄表的创建工作是通过调用ExCreateHandleTable函数来完成的,该函数调用ExpAllocateHandleTable来构造初始的句柄表。随着进程中句柄数量的增加,单层句柄表被扩展为二层句柄表,再进一步被扩展为三层句柄表。句柄表的扩展是由函数ExpAllocateHandleTableEntrySlow来实现的.

在HANDLE_TABLE 结构中,FirstFree 域记录了当前句柄表中的空闲句柄链,这是一个单链表,但并非通过指针链接起来,而是用句柄索引值来链接. 句柄索引值按HANDLE_VALUE_INC(4)逐个递增.FirstFree 域指示了链表头的句柄索引值,HANDLE_TABLE_ENTRY 结构中的NextFreeTableEntry 成员等于下一个空闲句柄的句柄索引值.因此,当进程在执行过程中需要创建新的句柄时,执行体可以直接从空闲句柄链表头得到一个句柄, 新的链表头变成原来链表头的NextFreeTableEntry,参见ExpAllocateHandleTableEntry 函数的代码;而当释放句柄时,将待释放的句柄索引赋给FirstFree,且该句柄项的NextFreeTableEntry 赋为原来的FirstFree,参见ExpFreeHandleTableEntry 函数的代码。另外,HANDLE_TABLE 结构的NextHandleNeedingPool域记录了下一次句柄表扩展的起始句柄索引,相当于当前句柄表中所有已分配页面都满了以后下一个页面的起始句柄索引。所以,Windows 进程的句柄表只是简单地线性增长,但只有当确实不够用的时候才会增长.

以下是ExpAllocateHandleTable的部分代码

 #define HANDLE_VALUE_INC 4
 if (DoInit) {
        HandleEntry++;
  
        for (i = 1; i < LOWLEVEL_COUNT - 1; i += 1) {

            HandleEntry->Value = 0;
            HandleEntry->NextFreeTableEntry = (i+1)*HANDLE_VALUE_INC;   // NextFreeTableEntry 每次+4
            HandleEntry++;
        }
        HandleEntry->Value = 0;
        HandleEntry->NextFreeTableEntry = 0;
        HandleTable->FirstFree = HANDLE_VALUE_INC;   // 初始为4
    }    

  

2.句柄表项

句柄表项的类型为HANDLE_TABLE_ENTRY

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
typedef struct _HANDLE_TABLE_ENTRY {  
union {  
PVOID Object; // 指向句柄所代表的对象  
ULONG ObAttributes; // 最低三位有特别含义,参见  
// OBJ_HANDLE_ATTRIBUTES 宏定义  
PHANDLE_TABLE_ENTRY_INFO InfoTable; // 各个句柄表页面的第一个表项  
// 使用此成员指向一张表  
ULONG_PTR Value;  
};  
union {  
union {  
ACCESS_MASK GrantedAccess; // 访问掩码  
struct { // 当NtGlobalFlag 中包含  
// FLG_KERNEL_STACK_TRACE_DB 标记时使用  
USHORT GrantedAccessIndex;  
USHORT CreatorBackTraceIndex;  
};  
};  
LONG NextFreeTableEntry; // 空闲时表示下一个空闲句柄索引  
};  
} HANDLE_TABLE_ENTRY, *PHANDLE_TABLE_ENTRY;

Object指针的最低3 位有特别含义:

第0 位OBJ_PROTECT_CLOSE,表示调用者是否允许关闭该句柄

第1 位OBJ_INHERIT,指示该进程所创建的子进程是否可以继承该句柄,即是否将该句柄项拷贝到它们的句柄表中

第2 位OBJ_AUDIT_OBJECT_CLOSE,指示关闭该对象时是否产生一个审计事件

如果句柄表项指向一个有效的对象,那么,GrantedAccess 成员记录了该句柄的访问掩码

如果这是一个空闲的句柄表项,那么,NextFreeTableEntry 成员将加入到句柄表的空闲单链表中

   一种特殊的情形是,对于每个最低层句柄表页面中的第一个句柄表项,它的NextFreeTableEntry 成员等于EX_ADDITIONAL_INFO_SIGNATURE,并且第一个union 中的InfoTable 成员指向一张HANDLE_TABLE_ENTRY_INFO 表,其中每一项代表了该页面中对应序号处的句柄表项的审计掩码。

另外,在WRK 代码中,关于Object 域的最低位,即第0 位,作为OBJ_PROTECT_CLOSE 来解释有一点微妙。实际上,关闭保护位被转移到了GrantedAccess成员中,而Object 域的第0 位变成了句柄表项的锁标志。如何将关闭保护位转移到GrantedAccess 成员中呢?可以参考宏ObpEncodeProtectClose 和ObpGetHandleAttributes。因此,事实上,当Object 域的第0 位为1 时,表示此句柄表项已被锁住,否则表示该项未被锁住.

  

3.句柄表解析

一个有效的句柄有4种可能:

-1,代表当前进程。
-2,代表当前线程。
负值,其绝对值为内核句柄表中的索引。仅限于内核模式的函数可以引用。
不超过226 的正值,当前进程的句柄表中的索引。

内核句柄表是指系统空间中的全局句柄表, 在WRK中即变量ObpKernelHandleTable,也是System 进程的句柄表.ObpKernelHandleTable 中的句柄只有在内核模式下才可以被引用,但可以位于任何一个进程环境中.所以,这些句柄实际上是跨进程环境的.有些系统组件和设备驱动程序可以利用这样的设施来创建或打开可在任何进程环境下访问的句柄,但是又不让用户模式代码引用它们.

kd> x nt!ObpKernelHandleTable
808a5a0c          nt!ObpKernelHandleTable = 0xe1001de0

解析句柄的基本函数是ObReferenceObjectByHandle

NTSTATUS ObReferenceObjectByHandle ()
{
    if ((LONG)(ULONG_PTR) Handle < 0) {//1.小于0
        if (Handle == NtCurrentProcess()) {// 1.1.当前进程
            // 权限检查
        } else if (Handle == NtCurrentThread()) {//1.2. 当前线程
	   // 权限检查
        } else if (AccessMode == KernelMode) {//1.3.内核,负值
            Handle = DecodeKernelHandle( Handle );// 负值转正
            HandleTable = ObpKernelHandleTable;
        } else {
            return STATUS_INVALID_HANDLE;
        }

    } else {//2.用户层
        HandleTable = PsGetCurrentProcessByThread(Thread)->ObjectTable;
    }
 }

ExMapHandleToPointerEx函数调用xpLookupHandleTableEntry来查找句柄表项,而ExpLookupHandleTableEntry 则直截了当地根据上图结构查找到最低层句柄表页面中的表项.将一个对象插入到句柄表中的函数是ObInsertObject.

  

4.引用计数

对象的引用有两种来源:

1.在内核中直接通过对象地址来引用,这是通过ObReferenceObjectByPointer来记录一次新的引用.

2.通过句柄来引用对象,这是由ObpIncrementHandleCount 函数来检查并记录一次句柄引用.

对于一个句柄,它的生命周期从被插入到句柄表中开始,一直到它被关闭,在此过程中,对象的引用计数中包含有它的一份引用.

ObpCreateHandle 调用了ObpIncrementHandleCount,标志着新创建的句柄引用了该对象,ObpCloseHandle 函数调用了ObpCloseHandleTableEntry,而后者又进一步调用了ObpDecrementHandleCount,标志着一个句柄结束了相应对象的引用。另外,在一个句柄上调用了ObReferenceObjectByHandle 函数以后,若该对象指针不再使用,则必须调用ObDereferenceObject 函数.

 

5.CID句柄表

线程有一个CLIENT_ID 成员Cid,其中包含了所属进程的唯一ID 和线程自身的唯一ID。这些唯一ID 是怎么生成的呢?它们是通过调用ExCreateHandle 函数,在一个全局的句柄表PspCidTable 中创建的句柄索引值。

 Process->UniqueProcessId = ExCreateHandle (PspCidTable, &CidEntry);  //PspCreateProcess 中代码
 
 Thread->Cid.UniqueProcess = Process->UniqueProcessId;//PspCreateThread 中代码
 Thread->Cid.UniqueThread = ExCreateHandle (PspCidTable, &CidEntry);//PspCreateThread 中代码

此句柄表也称为CID 句柄表(Client ID handle table),它没有被加入到系统的句柄表链表中。CID 句柄表中的每个句柄表项都包含了进程线程的对象地址。

kd> x nt!*PspCi*
808a5e28          nt!PspCidTable = 0xe1003c98
kd> dt _HANDLE_TABLE 0xe1003c98
nt!_HANDLE_TABLE
   +0x000 TableCode        : 0xe1c42001
   +0x004 QuotaProcess     : (null) 
   +0x008 UniqueProcessId  : (null) 
   +0x00c HandleTableLock  : [4] _EX_PUSH_LOCK
   +0x01c HandleTableList  : _LIST_ENTRY [ 0xe1003cb4 - 0xe1003cb4 ]
   +0x024 HandleContentionEvent : _EX_PUSH_LOCK
   +0x028 DebugInfo        : (null) 
   +0x02c ExtraInfoPages   : 0n0
   +0x030 FirstFree        : 0xc88
   +0x034 LastFree         : 0xc84
   +0x038 NextHandleNeedingPool : 0x1000
   +0x03c HandleCount      : 0n450
   +0x040 Flags            : 1
   +0x040 StrictFIFO       : 0y1

2.png

因此,进程和线程的唯一ID 值都是4 的倍数,其中4 是第一个句柄索引值,它被分配给System 进程的唯一ID(因为System 进程是第一个通过PspCreateProcess 函数创建的进程)。0 是专门保留给空闲进程的,它并非通过ExCreateHandle 函数而获得。CID 句柄表严格按照FIFO 来重用句柄表项,所以,一个句柄表项被释放以后,要等到其他的空闲句柄表项都被重用一遍以后才会被再次使用。由于CID 句柄表项保存了进程或线程的对象地址,所以,在内核中,根据进程或线程的唯一ID 值,总是可以很方便地找到相应的对象地址,函数PsLookupProcessThreadByCid、PsLookupProcessByProcessId 和PsLookupThreadByThreadId 正是利用了CID 句柄表的这一能力.

PsLookupProcessThreadByCid

文章作者:hgy413
本文地址:http://hgy413.com/2914.html
版权所有 © 转载时必须以链接形式注明作者和原始出处!

本文目前尚无任何评论.

发表评论