实验环境:win7 x32
首先引入一段基础概念;
- 1.在
windows
下所有的资源都是用对象的方式进行管理的(文件、进程、设备等都是对象)
,当要访问一个对象时,如打开一个文件,系统就会创建一个对象句柄,通过这个句柄可以对这个文件进行各种操作。 - 2.句柄和对象的联系是通过句柄表来进行的,准确来说一个句柄就是它所对应的对象在句柄表中的索引。
- 3.通过句柄可以在句柄表中找到对象的指针,通过指针就可以对,对象进行操作。
PspCidTable 就是这样的一种表(内核句柄表)
,表的内部存放的是进程EPROCESS
和线程ETHREAD
的内核对象,并通过进程PID
和线程TID
进行索引,ID号以4递增。
首先第一步先要得到PspCidTable
内存地址,在windbg中输入dp PspCidTable
即可得到,如果在程序中则是调用MmGetSystemRoutineAddress
取PsLookupProcessByProcessId函数里面的PspCidTable
。
PspCidTable是一个_HANDLE_TALBE
结构,当新建一个进程时,对应的会在PspCidTable
存在一个该进程和线程对应的HANDLE_TABLE_ENTRY
项。在windows10
中依然采用动态扩展
的方法,当句柄数少的时候就采用下层表,多的时候才启用中层表或上层表。
最重要的字段是:TableCode,当这个字段值的低2位,决定了有几层表,如果0x9c29c001,低2位是0,就代表只有一层,0x9c29c000指向的就是最终的全局句柄表,如果0x9c29c001,低2位是1,就代表只有两层,0x9c29c000指向下一层表,然后下一层表才指向最终的全局句柄表,那么0x9c29c002同理,最多只能有3层表。
需要注意的是,每次层表的大小是4k(4096),除了最终的全局句柄表的每一项是8字节,其他上层表每项都是4字节大小,假如只有一层,那么可以存放的句柄:4096 / 8 = 512项,而一般我们的PC不可能只有512个句柄,所以基本但是两层以上,即:(4096 / 4)*(4096 / 8) = 524288项。比如我的物理机就是69000多项。
根据TableCode的值,可以推断有两层,Windbg查看下一层:dp 0x9c29c000
可以看到这个上层表(1024项),只用到了2项(虚拟机Win7 x32),我们以“x32dbg”为例,pid:3636,3636 / 4 = 909(全局句柄表索引),通过上图,我们知道有2张表(每张表512项),而909项明显在第二张表里面,那么它在第二张表的具体索引是:909 - 512 = 397(0x18D),
那么dt _handle_table_entry 9c2a0000 +0x18D*8,其中最重要的字段:Object,同样该字段的最低位是属性位(去掉)。因为我们前面是找的x32dbg的pid,所以这里可以直接用EPROCESS结构解释。
如果,我们不知道这个id到底是进程的还是线程的怎么办?
其实,dt _handle_table_entry 9c2a0000 +0x18D*8,其中最重要的字段:Object,其真实结构是指向_OBJECT_HEADER的Body字段,但这个结构里面TypeIndex字段描述着这个Object究竟是进程还是线程。
所以,dt _OBJECT_HEADER 0x8766d030 - 0x18 ,就指向了_OBJECT_HEADER的首地址
可以看到TypeIndex字段的值是0x7,它其实也是一个表的索引,这个表就表示这个值是进程还是线程还是其他,我们 dp ObTypeIndexTable
继续用 dt _object_type 86ad8e38,解析它,可以看到name字段指明它是进程还是线程