文章链接:
1> Windows内核情景分析 3.4.1 Windows 进程的句柄表
4> 浅谈Windows句柄表
5> 句柄啊,3层表啊,ExpLookupHandleTableEntry啊... 5楼
--------------------------------------------------------------------------------------------
<ExpLookupHandleTableEntry笔记>
wrk1.2中
ExpLookupHandleTableEntry的内部流程
1) 取 Handle(EXHANDLE类型) 值为tHandle,并将TagBit(低两位)置0
2) 取 HandleTable->NextHandleNeedingPool为MaxHandle ,
如果 tHandle大于等于MaxHandle,则返回NULL,查询失败
(由此可见, NextHandleNeedingPool 应该是当前句柄表的最大句柄值+1
或者说是下一个可用的句柄值)
3) 取 CapturedTable(ULONG_PTR类型)为HandleTable->TableCode
取 TableLevel(ULONG)为(CapturedTable & LEVEL_CODE_MASK);
(LEVEL_CODE_MASK=3,即由HandleTable->TableCode的低2位来指定句柄表的级数)
CapturedTable 减去 TableLevel (即CapturedTable的低2位置0)
4) 根据 TableLevel 来进行不同方式的查找
令:
PCHAR TableLevel1; 最低层的表指针
PCHAR TableLevel2; 中间层的表指针
PCHAR TableLevel3; 最上层的表指针
ULONG_PTR i; 最低层的表索引
ULONG_PTR j; 中间层的表索引
ULONG_PTR k; 最上层的表索引
PHANDLE_TABLE_ENTRY Entry 最后找到的句柄项目的指针
a) TableLeve = 0 句柄表只有1级,此时CapturedTable只是一个大小为512(4K/8=512)
的HANDLE_TABLE_ENTRY数组,Handle的高30位即是索引.
(当然,实际上因为一级表为512项,所以其实只有2~10 9位为有效索引,其中11~31位在
第二步中被检查过,必为0).
由于每个HANDLE_TABLE_ENTRY大小为8,所以对应的Entry相对于表起始地址的偏移
为 Handle.Value>>2 * 8
TableLevel1 = CapturedTable;
Entry = TableLevel1 + (Handle.Value>>2) * 8
= TableLevel1 + Handle.Index * 8
= TableLevel1 + Handle.Value * 2 (因为第1步中已经将低2位置0了)
wrk 1.2中代码如下
Entry = (PHANDLE_TABLE_ENTRY) &TableLevel1[Handle.Value *
(sizeof (HANDLE_TABLE_ENTRY) / HANDLE_VALUE_INC)];
其中HANDLE_VALUE_INC在EXHANDLE结构中定义,值是4
b) TableLeve = 1 句柄表有2级,
第1层存储的是第2层表的指针(大小为4字节).
第2层存储的是HANDLE_TABLE_ENTRY(大小为8个字节)
句柄的2~10位为最底层表(第2层)的索引,11~N位为中间层表(第1层)索引
(注:N的值从3层句柄表的情况看,应该为)
wrk 1.2中的代码及分析如下
+++++++++++++++++++++++++++++++++++++++++++++++
i = Handle.Value % (LOWLEVEL_COUNT * HANDLE_VALUE_INC);
/*
= Handle.Value % 0x800
= Handle & 0x7FF
等效作用是取低11位
*/
Handle.Value -= i; 低11位置0
j = Handle.Value / ((LOWLEVEL_COUNT * HANDLE_VALUE_INC) / sizeof (PHANDLE_TABLE_ENTRY));
/*
= Handle.Value / (0x800 / 4)
= (Handle.Value >> 11) * 4
等效如下代码,实际上是取11~N为位为索引,并乘以4,得到中间表的表中偏移
*/
TableLevel2 = (PUCHAR) CapturedTable;
TableLevel1 = *(PUCHAR)(TableLevel2 + j);
Entry = TableLevel1+ (i * (sizeof (HANDLE_TABLE_ENTRY) / HANDLE_VALUE_INC));
/*
= TableLevel1+ (i*2)
= TableLevel1+ ((i>>4) * 8)
等效于最低层表索引(2~10位)乘以sizeof(HANDLE_TABLE_ENTRY),得到表中偏移.
*/
------------------------------------------------
c) TableLeve = 2 句柄表有3层
第1层存储的是第2层表的指针(大小为4字节).
第2层存储的是第3层表的指针(大小为4字节).
第3层存储的是HANDLE_TABLE_ENTRY(大小为8个字节)
句柄的2~10位为最底层表(第3层)的索引,11~20位为中间层表(第2层)索引,
20~N位为第一层索引(因为第1层最有4K/4个元素,索引应该也为10位,故推测
N实际上应该为30)
wrk 1.2中的代码及分析如下
+++++++++++++++++++++++++++++++++++++++++++++++
i = Handle.Value % (LOWLEVEL_COUNT * HANDLE_VALUE_INC);
Handle.Value -= i; 低11位清0
k = Handle.Value / ((LOWLEVEL_COUNT * HANDLE_VALUE_INC) / sizeof (PHANDLE_TABLE_ENTRY));
/*
= Handle.Value / (0x800 / 4)
= (Handle.Value >> 11) * 4
k取句柄的11~N为位,并乘以4
*/
j = k % (MIDLEVEL_COUNT * sizeof (PHANDLE_TABLE_ENTRY));
/*
= k % (4K/4 * 4)
= k % 0x1000
= k & 0xFFF
取k的低12位,实际上就是:句柄的11~20位为索引 * 4
*/
k -= j; k的低12位清0
k /= MIDLEVEL_COUNT;
/*
= k / (4k/4)
= k / 0x1000
= k >> 10
取k的高10~N位,实际上就是句柄的21~N位为索引再乘以4
*/
TableLevel3 = (PUCHAR) CapturedTable;
TableLevel2 = (PUCHAR) *(PHANDLE_TABLE_ENTRY *) &TableLevel3[k];
TableLevel1 = (PUCHAR) *(PHANDLE_TABLE_ENTRY *) &TableLevel2[j];
Entry = (PHANDLE_TABLE_ENTRY) &TableLevel1[i * (sizeof (HANDLE_TABLE_ENTRY) / HANDLE_VALUE_INC)];
------------------------------------------------
<Windows句柄表格式>
句柄是Windows对象管理中引入的一个东西,它的实际意义是对象在句柄表中的索引。Windows2000使用的是固定的三层句柄表,而WindowsXP和Windows2003都是使用的动态可扩展的三层句柄表,这是一种很优秀的结构,易扩展,且查找迅速,值得学习。通常情况下每个进程的句柄表都是一级表,当句柄数超过一级表的容量时,就会扩展为二级表,此时二级表中放的是指向一级表的指针。同样,当二级表也放满时,就会扩展为三级表,里面放指向二级表的指针。但是通常我们看不到这种情况,因为就目前的大多数情况来说,二级表的容量已经够我们用了。
接下来具体地看一下进程的句柄表。每个进程都有一个句柄表,包括System进程(不过System的句柄表稍有点特殊),但是Idle除外,因为它并不是一个事实上真正的进程。在进程对象EPROCESS中,可以直接找到句柄表指针。
来找个进程对象看看:
lkd> dt _EPROCESS 8a92cb98
nt!_EPROCESS
...
+0x0c4 ObjectTable : 0xe23d3690 _HANDLE_TABLE
...
句柄表是一个HANDLE_TABLE结构,继续来看:
lkd> dt _HANDLE_TABLE 0xe23d3690
nt!_HANDLE_TABLE
+0x000 TableCode : 0xe3202001 //句柄表
+0x004 QuotaProcess : 0x89833da0 _EPROCESS //所属进程的EPROCESS
+0x008 UniqueProcessId : 0x00000430 //所属进程的pid
+0x00c HandleTableLock : [4] _EX_PUSH_LOCK //句柄表锁,用于进程某些操作时锁住句柄表
+0x01c HandleTableList : _LIST_ENTRY [ 0xe216735c - 0xe2394fd4 ] //句柄表的双链,所有进程的句柄表链在一起,如果搞隐藏进程的话,这个地方是必须要照顾到的
+0x024 HandleContentionEvent : _EX_PUSH_LOCK
+0x028 DebugInfo : (null)
+0x02c ExtraInfoPages : 0
+0x030 FirstFree : 0x114c
+0x034 LastFree : 0
+0x038 NextHandleNeedingPool : 0x1800 //当前句柄表池的上界
+0x03c HandleCount : 1152 //当前进程中句柄总数
+0x040 Flags : 0
+0x040 StrictFIFO : 0y0
这里面最重要的,当然就是TableCode了。
TableCode的低两位被用作标志位,用于表示当前句柄表的级数,0,1,2分别表示一级表,二级表,三级表。
比如这里的0xe3202001 ,掩去高位,低两位的值为1,即为二级表。对于二级表,表中存放的是指向一级表的指针,一级表又被称为基本表,这实际上一个HANDLE_TABLE_ENTRY数组,它里面存放的HANDLE_TABLE_ENTRY中才是真正的对象,其定义如下:
typedef struct _HANDLE_TABLE_ENTRY {
//
// The pointer to the object overloaded with three ob attributes bits in
// the lower order and the high bit to denote locked or unlocked entries
//
union {
PVOID Object;
ULONG ObAttributes;
PHANDLE_TABLE_ENTRY_INFO InfoTable;
ULONG_PTR Value;
};
//
// This field either contains the granted access mask for the handle or an
// ob variation that also stores the same information. Or in the case of
// a free entry the field stores the index for the next free entry in the
// free list. This is like a FAT chain, and is used instead of pointers
// to make table duplication easier, because the entries can just be
// copied without needing to modify pointers.
//
union {
union {
ACCESS_MASK GrantedAccess;
struct {
USHORT GrantedAccessIndex;
USHORT CreatorBackTraceIndex;
};
};
LONG NextFreeTableEntry;
};
} HANDLE_TABLE_ENTRY, *PHANDLE_TABLE_ENTRY;
一级表可以放多大的句柄呢?按WRK中的宏展开,有
(TABLE_PAGE_SIZE / sizeof(HANDLE_TABLE_ENTRY))*HANDLE_VALUE_INC
即0x1000/8*4=0x800
计算过程:一级表(基本表)的大小为一页即4K=0x1000 Byte,而HANDLE_TABLE_ENRY大小为8字节,则可存放的个数为0x1000/8=0x200=512,而句柄以4为步进(注0),因此最大句柄为0x200*4=0x800.其中可存放的最大句柄不超过0x800(最大为0x800-4),而每个一级表的第一个HANDLE_TABLE_ENTRY的Object总是为0,因为我们都知道0是一个无效的句柄,它不指向一个有效的对象。因此,每个一级表实际存放的句柄数为511个。我们当前查看的句柄表中共有句柄1152个,显然一个表是不够的,1152=511*2+130,这里显然需要三个表,且第三个表未放满。而TableCode值为0xe3202001,其低两位也说明了这是个二级表。掩去低两位之后才是二级表的真正地址。来查看一下:
lkd> dd 0xe3202000
e3202000 e2a7b000 e3203000 e2c1e000 00000000
e3202010 00000000 00000000 00000000 00000000
e3202020 00000000 00000000 00000000 00000000
e3202030 00000000 00000000 00000000 00000000
e3202040 00000000 00000000 00000000 00000000
没错的,二级表中放了三个一级表指针。再来查看一下第一个一级表的内容。
lkd> dd e2a7b000
e2a7b000 00000000 fffffffe e100b4e9 000f0003
e2a7b010 e1a9ef41 00000003 898343b3 00100020
e2a7b020 8982ece9 021f0003 e1a794e9 000f000f
e2a7b030 e23a61b9 021f0001 e2390a59 020f003f
明显看到,第一个HANDLE_TABLE_ENTRY的Object为0.从第二个起才是有效的对象(如果某对象所对应的句柄被关闭,那么该对象就会从相应的句柄表中删除,所以并非句柄表中的每个位置存放的都是有效对象)。
接下来看HANDLE_TABLE_ENTRY,e100b4e9 000f0003这一对数据就是HANDLE_TABLE_ENTRY结构中的Object和GrantedAccess了。但是这里的Object并不是直接指向对象的。因为对象体总是8字节对齐,所以地址的低三位总是0,因此被用于标志位,表明该对象所对应的句柄的一些属性,比如可继承等等。因此,要对这里得到的Object掩去低三位,才会得到一个指向对象头OBJECT_HEADER的指针。
e100b4e9这里掩去之后为e100b4e8
lkd> dt _OBJECT_HEADER e100b4e8
nt!_OBJECT_HEADER
+0x000 PointerCount : 28
+0x004 HandleCount : 27
+0x004 NextToFree : 0x0000001b
+0x008 Type : 0x8a928e70 _OBJECT_TYPE
+0x00c NameInfoOffset : 0x20 ‘ ‘
+0x00d HandleInfoOffset : 0 ‘‘
+0x00e QuotaInfoOffset : 0 ‘‘
+0x00f Flags : 0x36 ‘6‘
+0x010 ObjectCreateInfo : 0x00000001 _OBJECT_CREATE_INFORMATION
+0x010 QuotaBlockCharged : 0x00000001
+0x014 SecurityDescriptor : 0xe100b44d
+0x018 Body : _QUAD
继续来看一下这个OBJECT_TYPE
lkd> dt _OBJECT_TYPE 0x8a928e70
nt!_OBJECT_TYPE
+0x000 Mutex : _ERESOURCE
+0x038 TypeList : _LIST_ENTRY [ 0xe100b4d8 - 0xe100b4d8 ]
+0x040 Name : _UNICODE_STRING "KeyedEvent"
+0x048 DefaultObject : 0x80568b40
+0x04c Index : 0x10
+0x050 TotalNumberOfObjects : 1
+0x054 TotalNumberOfHandles : 0x1b
+0x058 HighWaterNumberOfObjects : 1
+0x05c HighWaterNumberOfHandles : 0x1b
+0x060 TypeInfo : _OBJECT_TYPE_INITIALIZER
+0x0ac Key : 0x6579654b
+0x0b0 ObjectLocks : [4] _ERESOURCE
是个KeyedEvent对象。对象头再加上它本身的大小0x18之后就到了对象体OJBECT_BODY了,这个因不同对象而异。直接来看一下:
lkd> !object e100b4e8+0x18
Object: e100b500 Type: (8a928e70) KeyedEvent
ObjectHeader: e100b4e8 (old version)
HandleCount: 27 PointerCount: 28
Directory Object: e10000a8 Name: CritSecOutOfMemoryEvent
跟前面看到的对比一下,完全是一致的。这样就了解了如何从进程的句柄表中得到实际的对象。
下面以该进程中的一个句柄0x1078为例说明句柄如何索引到对象。
0x1078/0x800=2
0x1078%0x800=0x78
前面已经知道,该进程的句柄表为二级表,而上面的计算结果表明,该句柄大小已经超出了两个句柄表的范围,在第三个句柄表中,且偏移为0x78*2.(因为句柄按4递增,而HANDLE_TABLE_ENTRY结构大小为8,所以正确公式应该为0x78除以4得到索引值,再乘以8得到以字节计算的偏移值,结果即0x78*2)
而第三张表基址为e2c1e000 。来看一下:
lkd> dd e2c1e000+0x78*2
e2c1e0f0 896b7019 001f03ff 8984e659 00100003
e2c1e100 8984e611 00100003 e2d183d9 00020019
e2c1e110 898003f1 001f0003 896c0701 0012019f
e2c1e120 8984e5c9 00100003 89834691 00100003
可以得到Object为896b7019,掩去低两位,则对象头指针为896b7018,对象体指针为896b7018+0x18
lkd> !object 896b7018+0x18
Object: 896b7030 Type: (8a92c040) Thread
ObjectHeader: 896b7018 (old version)
HandleCount: 3 PointerCount: 5
结果显示这是一个线程对象,与Process Explorer中显示的结果完全一致。以上过程很容易转化为程序实现并且不依赖任何系统函数,具体实现过程可参考WRK中的ExpLookupHandleTableEntry 函数,其原型为
PHANDLE_TABLE_ENTRY
ExpLookupHandleTableEntry (
IN PHANDLE_TABLE HandleTable,
IN EXHANDLE tHandle
);
---------------------------------------------------------------------------------------------------------
注0: 这里需要这样理解: 句柄的值是4对齐的, 即4, 8, c, f, 10等等, 也就是说, 索引为1句柄为4, 句柄是索引的4倍, 所以要乘以4. 这里参考文章<浅谈Windows句柄表>