涉及到句柄表的有以下这些概念:
- HANDLE_TABLE
- HANDLE_TABLE结构体中的TableCode变量
实际上啊,TableCode是指向句柄表项第一个句柄表项的指针(NULL句柄表项),TableCode就是HANDLE_TABLE_ENTRY的指针。
但是,当有两级以上表时,这个时候就不是了,先来搞定最简单的。
HANDLE_TABLE_ENTRY:句柄表项
对象头_OBJECT_HANDLE
EXHANDLE:这个就是提供给用户使用的句柄值
HANDLE_TABLE_ENTRY是一个8个字节的结构体。它包括:
- 指向对象头的指针
- 32位的访问掩码
因为对象头_OBJECT_HANDLE的大小是0x18h,是8的倍数,因为对象头总是按照8个字节对齐。所以对象头的地址低3位肯定是0。所以HANDLE_TABLE_ENTRY的对象头的指针的低3位为0,低三位被用作一些访问标志。
句柄值:通过查看句柄值可以发现,句柄值总是4的倍数。例如:
0x0004
0x0008
0x000C
0x0010
所以,句柄值低2位总是0,所以低2位可以被用作标志位。
句柄值的结构类型是EXHANDLE,在EXHANDLE中低2位为标志位。
0x0000这一个句柄值被用来做位NULL无效句柄。提供给用户使用:
if(Handle == NULL/ *0x0000*/ )
return;
typedef struct _EXHANDLE
{
// 注意啦,这里是个联合。
// 实际上该结构体就占4个字节
union
{
struct
{
ULONG TagBits:2;
ULONG Index:30;
}
HANDLE GenericHandleOverlay; //呵呵,这是用来提供给用户使用的句柄。
#define HANLE_VALUE_INC 4
ULONG_PTR Value; //这个表示什么意思呢?
}
}EXHANDLE,*PEXHANDLE;
*******************************************************************************
如何知道HANDLE_TABLE和HANDLE_TABLE_ENTRY的关系,可以参考ExpAllocateHandleTable。
ExpAllocateHandleTable用来为每个进程分配句柄表,并初始化句柄表。第一次只分配0级的句柄(保存真正句柄项)。
*******************************************************************************
我不知道该怎么写才顺,我就按照我的分析流程写吧。
!Process 查看当前进程
PROCESS 87ca7cf8 SessionId: 0 Cid: 0cb0 Peb: 7ffd8000 ParentCid: 03c4
DirBase: 3f13d000 ObjectTable: e2d11b78 HandleCount: 76.
Image: windbg.exe
其中EPROCESS的地址在87ca7cf8,ObjectTable的地址在e2d11b78 。
ObjectTable是HANDLE_TABLE结构变量,它保存在EPROCESS中。
dt _EPROCESS 87ca7cf8 来查看EPROCESS结构体的值。
.....
+0x0c4 ObjectTable : 0xe2d11b78_HANDLE_TABLE
......
ObjectTable在EPROCESS偏移0x0c4处。
ObjectTable保存着关于句柄表的信息。
我们使用下面的命令来查看HANDLE_TABLE内容:
dt _HANDLE_TABLE 0xe2d11b78
+0x000 TableCode : 0xe4702000
......
其中偏移0为TableCode,它实际上是句柄项(_HANDLE_TABLE_ENTRY)的指针。
句柄项(_HANDLE_TABLE_ENTRY)是用来保存真正的句柄信息的结构体。
(注意,当TableCode低2位为0时,TableCode才指向_HANDLE_TABLE_ENTRY,至于如何给TableCode分配值,还是需要分析一下源码。)
_HANDLE_TABLE_ENTRY是两个32位的结构体:一个指向对象头的指针;一个是32位的标志。
注意,这个32位的对象头的指针并不是全部有效。因为对象头为0x18个字节,所以windows能保证对象头的分配地址总是8的倍数。所以这32位的对象头的指针低3位肯定都为0,windows将这低3位用作其他用途。因此,当我们使用对象头指针时,一定要记得,低3位值不是我们需要的,应该置0 。即:value & 0xFFFFFFF8。
我们使用dt _HANDLE_TABLE_ENTRY 0xe4702000 来查看其值。
lkd> dt _HANDLE_TABLE_ENTRY 0xe4702000
nt!_HANDLE_TABLE_ENTRY
+0x000 Object : (null)
+0x000 ObAttributes : 0
+0x000 InfoTable : (null)
+0x000 Value : 0
+0x004 GrantedAccess : 0xfffffffe
+0x004 GrantedAccessIndex : 0xfffe
+0x006 CreatorBackTraceIndex : 0xffff
+0x004 NextFreeTableEntry : -2
但是得到的值好像是无效的。
这就对了,因为句柄表的第一个值不被使用,做为一个无效值,用来提供给程序员做错误处理使用。
下面该怎么办呢?句柄表,句柄表肯定是一个连续的数组,连续的保存一些句柄表项。
我们使用dd 0x4702000来查看这块地址的一些值
lkd> dd 0xe4702000
e4702000 00000000 fffffffe e1008719 000f0003
e4702010 e1858019 00000003 87d68f13 00100020
......
看,值出来了,因为一个句柄表项_HANDLE_TABLE_ENTRY占8个字节。所以,前两个值看似一个无效的值。但是红色标示的却象个有用的句柄项。
注意了,_HANDLE_TABLE_ENTRY是2个32位组成的64位结构体。前面说过了,低32位是对象头的指针。但是要得到对象头的指针,我们必须将值&0xFFFFFFF8,将低3位置0。
e1008719 & 0xFFFFFFF8 = e1008718
我们还必须加上0x18才能得到真正的内核对象。因为内核对象在对象头的后面,对象头的大小是0x18。
dt _object_header
lkd> dt _object_header
nt!_OBJECT_HEADER
+0x000 PointerCount : Int4B
...
+0x018 Body : _QUAD
用刚才的 e1008718 + 0x018 = e1008730 ,然后我们使用!Object e1008730查看。
lkd> !object e1008730
Object: e1008730 Type: (89bddad0) KeyedEvent
ObjectHeader: e1008718 (old version)
HandleCount: 48 PointerCount: 49
Directory Object: e1000270 Name: CritSecOutOfMemoryEvent
哈哈,挺像的 。使用!Handle列出当前句柄来测试一把。
lkd> !handle
processor number 0, process 87ca7cf8
PROCESS 87ca7cf8 SessionId: 0 Cid: 0cb0 Peb: 7ffd8000 ParentCid: 03c4
DirBase: 3f13d000 ObjectTable: e2d11b78 HandleCount: 99.
Image: windbg.exe
Handle table at e4702000 with 99 Entries in use
0004: Object: e1008730 GrantedAccess: 000f0003 Entry: e4702008
Object: e1008730 Type: (89bddad0) KeyedEvent
ObjectHeader: e1008718 (old version)
HandleCount: 48 PointerCount: 49
Directory Object: e1000270 Name: CritSecOutOfMemoryEvent
0008: Object: e1858030 GrantedAccess: 00000003 Entry: e4702010
Object: e1858030 Type: (89c153b0) Directory
......
对比一下,成功,我们已经找到了句柄值为0004的内核对象。
OK,冒出了句柄值,那么句柄值是如何被关联起来呢?想下0004,它对应_HANDLE_TABLE_ENTRY表项的低几个啊?
句柄值为0x0000代表是NULL,刚好_HANDLE_TABLE_ENTRY的第0个表项为无效值
句柄值为0x0004有效,刚好指的是_HANDLE_TABLE_ENTRY的第1个表项。
那句柄值为0x0008了?
原来句柄值总是4的倍数。值/4就代表句柄表项数组_HANDLE_TABLE_ENTRY的索引啊。
这时,句柄值的低两位永远是0啦,为啥呢?是4的倍数,第2为不就为0?自己算算。
0x00,0x04,0x08,0x10,0x14等等的二进制
既然第2位永远为0,那么微软就利用了这两位做一个标志位,用来指示当前句柄值所代表的内核对象到那个表项数组中找到?
什么意思呢?
句柄表实际上是分级的,分3级,我们可以像理解分页一样。
分页分为:页目录、页表、物理页。
每个页目录保存1024个页表,每个页表保存着1024个物理页,每个页为4k。
句柄表可以这样分:
4K的目录,保存1K个表指针(指针为一项,占4个字节吗,总共是1024个项)。
每个4K的表,保存着1K个_HANDLE_TABLE_ENTRY数组指针。
每个4K的_HANDLE_TABLE_ENTRY数组保存着512个_HANDLE_TABLE_ENTRY,咋是512个?因为每一个_HANDLE_TABLE_ENTRY是8个字节。
每个_HANDLE_TABLE_ENTRY指向真正的内核对象前的对象头。(第1个表项除外,代表空。)
哈哈,句柄表就这么简单,就是太多繁琐的东西。记不住。
记下笔记备忘之。
这两天试着写写访问句柄表信息的小驱动。写完后配合代码和WRK再整理。