基本概念
- 进程的句柄表是私有的,每个进程都有一个自己的句柄表
- 系统拥有一个全局的句柄表,通过全局变量PspCidTable获取到全局句柄表的地址
- 每个进程和线程都有一个唯一的编号:PID和CID,这两个值就是该内核对象在全局句柄表中的索引
与局部句柄表的差异
- 全局句柄表的句柄直接指向内核对象;局部句柄表的句柄指向_OBJECT_HEADER,所以在运算时还需加上0x18字节
- 全局句柄表只有进程和线程这俩内核对象的句柄;局部句柄表包含了当前进程所有打开的内核对象,不限于进程和线程,还有互斥体等。
全局句柄表结构
全局句柄表的结构取决于TableCode的低2位的值:
- 取00b时:最多可以存储512个句柄(4KB/8Byte = 512)
- 取01b时:第一级可以存1024个地址,第二级用来存句柄。最多可存1024x512个句柄
- 取10b时:第一级可以存1024个地址,每个第二级的表也存1024个地址,第三级表存句柄。最多可存1024x1024x512个句柄
具体如下图所示:
查找全局句柄表
常规方式
下面按照步骤,通过Windbg实现查找全局句柄表中的进程句柄
根据_HANDLE_TABLE中的TableCode字段,以及PID的值(用于索引),来确定句柄的位置。注意事项如下:
1)观察TableCode后2位的值,判断是否需要跨表
2)3环PID是四字节的值,但是句柄在内核是8字节,因此寻址时需要PID/4*8
根据句柄的低四字节,找到对应的内核对象。注意事项如下:
1)全局句柄表的句柄是直接指向内核对象结构体的,所以不需要加0x18字节
2)根据低四字节查找时,需清空后3bit属性位再进行运算
代码查找思路
由于PspCidTable无法直接引用,在代码实现的过程中,需要一些其它技巧
1)特征搜索
PsLookupProcessThreadByCid()
PsLookupProcessByProcessId()
PsLookupThreadByThreadId()
这三个函数都会根据PID或者CID去查找全局句柄表。在这个过程中,就会用到PspCidTable,这样我们可以通过Hook这三个函数,就可以借用PspCidTable这个值,找到全局句柄表了。由于这三个函数都是未导出的,所以需要通过特征搜索先进行定位,特征搜索可以参考之前的文章
2)KPCR
KPCR有一个成员,KdVersionBlock。这个成员非常奇妙,在他指向地址(+0x80)位置处有一个地址,这个地址指向的就是PspCidTable。具体如下图所示:
因此我们可以在驱动中采用如下手法。获取到PspCidTable
1 | PULONG PspCidTable |
当然,这个KdVersionBlock指向的地址还能找到一些其它的值:
- +0x10:KernelBase
- +0x18/+0x70:PsLoadedModuleList
- +0x78:PsActiveProcessHead
- +0x88:ExpSystemResourcesList
遍历全局句柄表
有了获取PspCidTable的方法后,我们就可以试着遍历一下全局句柄表,例如打印每个进程的进程名。
代码实现
1 |
|
实验效果
由于代码比较草率,并没有过滤掉线程结构体,所以会有较多的乱码,但仍然可以打印出所有的进程
参考资料
参考书籍:《Windows内核原理与实现》——潘爱民
参考教程:https://www.bilibili.com/video/BV1NJ411M7aE?t=143&p=68
参考文章:
- https://blog.csdn.net/weixin_42052102/article/details/83479303
- https://blog.csdn.net/qq_41988448/article/details/104945311
- https://www.cnblogs.com/joneyyana/p/12595525.html
参考笔记:张嘉杰,时间刺客.