2K:
2K下采用三层表结构,上层表256个项,每个项占用4字节 对应一个中层表的地址,每个中层表有256项,每个项占用4字节对应一个下层表地址,每个下层表256项,每个项占用8字节(每个项就是HANDLE_TABLE_ENTRY)。
因此 每个进程支持256*256*256个句柄
上层表大小:256 * 4字节 = 1KB
中层表大小:256 *4字节 = 1KB
下层表大小:256 *sizeof(HANDLE_TABLE_ENTRY) = 2KB
当进程打开的对象不超过大约256个时,系统只为句柄表分配一个上层表,大小为1KB。一个中层表,大小为1KB。一个下层表,大小为2KB。正好可以放在同一个物理页中(4KB)。
当进程打开的对 象超过大约256个时,系统会再分配2个下层表,放在同一个物理页,并把这2个下层表的地址,填入中层表的相应项,这样就可以存放大约256*3=768 个对象的对象头指针。
更多的句柄以此类推。
所以绝大多数情况下,进程的句柄表消耗的物理内存为4K一个页(打开的对象不超过大约256个时)或者8K 两个页(打开的对象不超过大约768个时)。
如何通过句柄定位内核对象:
一个句柄被分为三部分,分别做这三个表中的索引:
bits2-9这 8bit,为下层表索引,乘以8得到在下层表中的偏移。
bits10-17这 8bit,为中层表的索引,乘以4得到在中层表中的偏移。
bits18-25这8bit,为高层表的索引,乘以4得到在高层表中的偏移。
如何定位句柄表:
通过 EPROCESS 的 struct HANDLE_TABLE *ObjectTable 我们可以找到一个进程的 HANDLE_TABLE 结构,通过 HANDLE_TABLE 结构的 struct _HANDLE_TABLE_ENTRY ***Table 我们可以找到这个进程的句柄表。
如何通过HANDLE_TABLE_ENTRY获得内核对象的地址:
HANDLE_TABLE_ENTRY 大小为 8 个字节,由2个32bit组成。如果第一个32bit值不为0,那么第一个32bit就可以转换成一个指向对象头的指针。由于对象头总是32bit对齐的,所以一个对象头的指针的低3bit总是0。所以 HANDLE_TABLE_ENTRY 第一个32bit 的低3bit,被用作标志。由于所有的对象都在系统地址空间(0x80000000-0xFFFFFFFF)中,所以一个对象头的指针的最高位总是1。所以 HANDLE_TABLE_ENTRY 第一个32bit 的最高位也被用作标志。当我们把一个HANDLE_TABLE_ENTRY 第一个32bit 转换成对象头的指针时,需要把低3bit设为0(&0xFFFFFFF8),最高位设为1(OR 0x80000000)。对象指针总是指的对象体的指针,由于对象头在对象体之前,大小为0x18字节,所以对象指针等于对象头指针加0x18。
XP/2003:
通过EPROCESS的struct HANDLE_TABLE *ObjectTable我们可以找到这个进程的HANDLE_TABLE结构,这点和2K没啥不同,通过HANDLE_TABLE结构的第一个成员TableCode来定位句柄表。由于地址要按照4字节对齐,SO,句柄表地址的低2位就是0,TableCode的高30位用来表示句柄表地址,而低2位则表示这是几级表。
原因是什么呢?其实就是因为XP/2K3不再像2K那样一下子就分配3层表,而是从下往上逐渐分配 。一个页面4KB ,每个HANDLE_TABLE_ENTRY占8个字节,那么一个页面就可以保存4KB/8字节= 512项,因为需要一项用来处理审计,那么实际上可用的为511项,上面两级的句柄表可以存储的指针项为4096/4=1024个,因此最多可以保存511 * 1024 *1024个句柄。(这一块的详细内容参看windows internals的137页)
当大于511 个句柄的时候才会分配1级表(中层表)
大于511 ×1023个句柄的时候才会分配2级表(上层表)
如何根据句柄来定位内核对象:
我们首先要看进程的句柄表有几层,通过HANDLE_TABLE里的TableCode成员的低2位,如果低2位是0 则说明只有一层表 也就是0级表(低层表),TableCode里保存的就是0级表(低层表)的地址;如果是1则说明有两层表,0级表(低层表)和1级表(中层表),TableCode里的地址就是1级表(中层表)的地址;如果是2则说明有三层表,0级表(低层表),1级表(中层表)和2层表(高层表),TableCode里的地址就是2级表(高层表)的地址。
然后句柄仍然当索引用:
bits2-10 这 9bit,为下层表的索引,乘以8得到在下层表中的偏移。
bits11-20这 10bit,为中层表的索引,乘以4得到在中层表中的偏移。
bits21-30这 10bit,为高层表的索引,乘以4得到在高层表中的偏移。
最高位一定是0
关于HANDLE_TABLE_ENTRY的其他都和2K一样。
其他:
PspCidTable是系统特殊的HANDLE_TALBLE ,保存着所有进程和线程对象的指针。PID(进程ID)和 ThreadID(线程ID) 就是在这个句柄表中的索引。这个HANDLE_TABLE不与任何HANDLE_TABLE相连接,而且特殊的是,一般的句柄表里的HANDLE_TABLE_ENTRY的第一个DWORD保存的是对象头的地址 需要加上对象头的大小才能定位到对象体的地址,而PspCidTable里的HANDLE_TABLE_ENTRY里保存的就是对象体的指针(当然上段文字讲的变换还是要做的),有些老的ARK程序在枚举进程的时候用到了PspCidTable。
所有进程的HANDLE_TABLE通过其成员HandleTableList链接起来,这个成员是个LIST_ENTRY结构 链表的首指针是HandleTableListHead,所以要遍历所有进程的句柄表可以通过这个符号。
参考资料:
1.《JIURL玩玩Win2k进程线程篇 HANDLE_TABLE 》 JIURL
2.《Windows句柄表格式(1) - 2000句柄表格式》 ftofficer
3.《Windows句柄表格式(2) - XP句柄表格式》 ftofficer
4.《windows internals 》第五版