在上一篇文章《驱动开发:内核枚举DpcTimer定时器》
中我们通过枚举特征码的方式找到了DPC
定时器基址并输出了内核中存在的定时器列表,本章将学习如何通过特征码定位的方式寻找Windows 10
系统下面的PspCidTable
内核句柄表地址。
首先引入一段基础概念;
- 1.在
windows
下所有的资源都是用对象的方式进行管理的(文件、进程、设备等都是对象)
,当要访问一个对象时,如打开一个文件,系统就会创建一个对象句柄,通过这个句柄可以对这个文件进行各种操作。 - 2.句柄和对象的联系是通过句柄表来进行的,准确来说一个句柄就是它所对应的对象在句柄表中的索引。
- 3.通过句柄可以在句柄表中找到对象的指针,通过指针就可以对,对象进行操作。
PspCidTable 就是这样的一种表(内核句柄表)
,表的内部存放的是进程EPROCESS
和线程ETHREAD
的内核对象,并通过进程PID
和线程TID
进行索引,ID号以4递增,内核句柄表不属于任何进程,也不连接在系统的句柄表上,通过它可以返回系统的任何对象。
内核句柄表与普通句柄表完全一样,但它与每个进程私有的句柄表有以下不同;
- 1.PspCidTable 中存放的对象是系统中所有的进程线程对象,其索引就是
PID
和TID
。 - 2.PspCidTable 中存放的直接是对象体
EPROCESS和ETHREAD
,而每个进程私有的句柄表则存放的是对象头OBJECT_HEADER
。 - 3.PspCidTable 是一个独立的句柄表,而每个进程私有的句柄表以一个双链连接起来。
- 4.PspCidTable 访问对象时要掩掉低三位,每个进程私有的句柄表是双链连接起来的。
那么在Windows10
系统中该如何枚举句柄表;
- 1.首先找到
PsLookupProcessByProcessId
函数地址,该函数是被导出的可以动态拿到。 - 2.其次在
PsLookupProcessByProcessId
地址中搜索PspReferenceCidTableEntry
函数。 - 3.最后在
PspReferenceCidTableEntry
地址中找到PspCidTable
函数。
首先第一步先要得到PspCidTable
函数内存地址,输入dp PspCidTable
即可得到,如果在程序中则是调用MmGetSystemRoutineAddress
取到。
PspCidTable是一个HANDLE_TALBE
结构,当新建一个进程时,对应的会在PspCidTable
存在一个该进程和线程对应的HANDLE_TABLE_ENTRY
项。在windows10
中依然采用动态扩展
的方法,当句柄数少的时候就采用下层表,多的时候才启用中层表或上层表。
接着我们解析ffffdc88-79605dc0
这个内存地址,执行dt _HANDLE_TABLE 0xffffdc8879605dc0
得到规范化结构体。
内核句柄表分为三层如下;
- 下层表:是一个
HANDLE_TABLE_ENTRY
项的索引,整个表共有256
个元素,每个元素是一个8个字节
长的HANDLE_TABLE_ENTRY
项及索引,HANDLE_TABLE_ENTRY
项中保存着指向对象的指针,下层表可以看成是进程和线程的稠密索引。 - 中层表:共有
256
个元素,每个元素是4个字节
长的指向下层表的入口指针及索引,中层表可以看成是进程和线程的稀疏索引。 - 上层表:共有
256
个元素,每个元素是4个字节
长的指向中层表的入口指针及索引,上层表可以看成是中层表的稀疏索引。
总结起来一个句柄表有一个上层表,一个上层表最多可以有256
个中层表的入口指针,每个中层表最多可以有256
个下层表的入口指针,每个下层表最多可以有256
个进程和线程对象的指针。PspCidTable
表可以看成是HANDLE_TBALE_ENTRY
项的多级索引。
如上图所示TableCode
是指向句柄表的指针,低二位(二进制)记录句柄表的等级:0(00)表示一级表,1(01)表示二级表,2(10)表示三级表。这里的 0xffffdc88-7d09b001
就说名它是一个二级表。
一级表里存放的就是进程和线程对象(加密过的,需要一些计算来解密),二级表里存放的是指向某个一级表的指针,同理三级表存放的是指向二级表的指针。
x64 系统中,每张表的大小是 0x1000(4096)
,一级表中存放的是 _handle_table_entry
结构(大小 = 16)
,二级表和三级表存放的是指针(大小 = 8)
。
我们对 0xffffdc88-7d09b001
抹去低二位,输入dp 0xffffdc887d09b000
输出的结果就是一张二级表,里面存储的就是一级表指针。
继续查看第一张一级表,输入dp 0xffffdc887962a000
命令,我们知道一级句柄表是根据进程或线程ID来索引的,且以4累加,所以第一行对应id = 0
,第二行对应id = 4
。根据尝试,PID = 4
的进程是System
。
所以此处的第二行0xb281de28-3300ffa7
就是加密后的System
进程的EPROCESS
结构,对于Win10系统来说解密算法(value >> 0x10) & 0xfffffffffffffff0
是这样的,我们通过代码计算出来。
#include <Windows.h>
#include <iostream>
int _tmain(int argc, _TCHAR* argv[])
{
std::cout << "hello lyshark.com" << std::endl;
ULONG64 ul_recode = 0xb281de283300ffa7;
ULONG64 ul_decode = (LONG64)ul_recode >> 0x10;
ul_decode &= 0xfffffffffffffff0;
std::cout << "解密后地址: " << std::hex << ul_decode << std::endl;
getchar();
return 0;
}
运行程序得到如下输出,即可知道System
系统进程解密后的EPROCESS
结构地址是0xffffb281de283300
回到WinDBG调试器,输入命令dt _EPROCESS 0xffffb281de283300
解析以下这个结构,输出结果是System进程。
理论知识总结已经结束了,接下来就是如何实现枚举进程线程了,枚举流程如下:
- 1.首先找到
PspCidTable
的地址。 - 2.然后找到
HANDLE_TBALE
的地址。 - 3.根据
TableCode
来判断层次结构。 - 4.遍历层次结构来获取对象地址。
- 5.判断对象类型是否为进程对象。
- 6.判断进程是否有效。
这里先来实现获取PspCidTable
函数的动态地址,代码如下。
// 署名
// PowerBy: LyShark
// Email: me@lyshark.com
#include <ntifs.h>
#include <windef.h>
// 获取 PspCidTable
// By: LyShark