PspCidTable 完全解读 by sysdog 2009.5.1 Whu
-------------------------------------------------------------------------------
一、句柄
句柄:HADNLE 在winnt.h 定义如下:
typedef void *HANDLE;
是一个 32 位无类型指针,充当内核对象在内核的索引.
句柄本质—内核对象(object)的索引
每个进程都有自己的进程句柄表,当操作进程对象时,会把对象指针放在自己的句柄表中,
返回一个句柄,通过句柄来索引对象指针,然后操作对象。
进程句柄表是根据 HANDLE 索引对象指针的,而pspcidtable(未导出,需要自己定位地址)
是根据 CID 索引对象指针的.
EPROCESS 和 ETHREAD 分别是进程对象和线程对象
NT 执行体处理进程线程对象,进程和线程描述表可由 EPROCESS 结构来访问,NT 内核处理进
程和线程描述表
二、句柄表
XP 下句柄表是动态分配的,是个三层表,1 级表(称为基本表)存放系统底层的。
句柄表大小为 1页即 4K,而每个表项是一个HANDLE_TABLE_ENTRY占8个字节,所以
每个表最多只能放 512个表项,又因为表项索引是以 4递增的,因此一个最终表能承受的最
大索引值也是 2048即 0x800,两个表最大为 0x1000,三个表就是 0x1800。
句柄表的格式如下:
句柄表就是 HANDLE_TABLE 结构,如下:
里面没有指针结构啊,那么句柄表到底存储在那里?
实际是保存在 TableCode 里面,但是 TableCode 并不是地址,它比地址所担任的责任多一
点:标明这个指针指向的是哪一级别的句柄,在 x86 上面Windows 总是分配内存使其沿四个
字节对齐,所以实际的句柄表地址的低两位一定是 0? 。XP 就用这两位来标示这个表是哪个
级别,00 表示 1 级表,01 表示 2 级表,02 表示 3 级表。2 级表存放的是 1 级表的指针,3 级
表存放的是 2 级表的指针。
TableCode转变一下就是HANDLE_TABLE_ENTRY了,转变也很简单,就是低两位清零,
因为底两位代表当前句柄表的级数,HANDLE_TABLE里面存放的是一些信息,
HANDLE_TABLE_ENTRY才是真正的句柄表。
HANDLE_TABLE_ENTRY结构如下:
如果是一级表 HANDLE_TABLE_ENTRY=TableCode
如果是二级表 HANDLE_TABLE_ENTRY需要根据 2级表中的指针来找 1级表的基址,然后
再根据 1级表中的偏移找到进程对象…..三级表类推
三、内核对象
作为一个 Wi n d o w s软件开发人员,经常需要创建、打开和操作各种内核对象。系统要创
建和操作若干类型的内核对象,比如存取符号对象、事件对象、文件对象、文件映射对象、
I / O完成端口对象、作业对象、信箱对象、互斥对象、管道对象、进程对象、信标对象、线
程对象和等待计时器对象等。这些对象都是通过调用函数来创建的。例如, C r e a t e F i l e M
a p p i n g函数可使系统能够创建一个文件映射对象。每个内核对象只是内核分配的一个内存
块,并且只能由该内核访问。该内存块是一种数据结构,它的成员负责维护该对象的各种信
息。有些数据成员(如安全性描述符、使用计数等)在所有对象类型中是相同的,但大多数
数据成员属于定的对象类型。例如,进程对象有一个进程 I D、一个基本优先级和一个退出
代码,而文件对象则拥有一个字节位移、一个共享模式和一个打开模式。由于内核对象的数
据结构只能被内核访问,因此应用程序无法在内存中找到这些数据结构并直接改变它们的内
容。M i c r o s o f t规定了这个限制条件,目的是为了确保内核对象结构保持状态的一致。这
个限制也使 M i c r o s o f t能够在不破坏任何应用程序的情况下在这些结构中添加、删除和修
改数据成员。如果我们不能直接改变这些数据结构,那么我们的应用程序如何才能操作这些
内核对象呢?解决办法是,Wi n d o w s提供了一组函数,以便用定义得很好的方法来对这些
结构进行操作。这些内核对象始终都可以通过这些函数进行访问。当调用一个用于创建内核
对象的函数时,该函数就返回一个用于标识该对象的句柄。该句柄可以被视为一个不透明值,
你的进程中的任何线程都可以使用这个值。将这个句柄传递给 Wi n d o w s的各个函数,这
样,系统就能知道你想操作哪个内核对象。
四、PspCidTable
对象指针:从其线形地址来看,它将常驻内存的结构体划分为两部分:一个对象头和一个对
象体。对象指针并没有指向对象自身的基地址,而是指向了对象体,由于紧接着对象头的就
是对象体,所以可以给对象指针加上一个负的偏移量来访问对象头。
PspCidTable:指向 HANDLE_TABLE的指针,未导出结构,存放所有进程及线程对象指针。
PspCidTable 是一个句柄表,其格式与普通的句柄表是完全一样的.但它与每个进程私有的句
柄表有以下不同:
1.PspCidTable中存放的对象是系统中所有的进线程对象指针,其索引就是 PID 和 CID
2.PspCidTable中存放是对象体(指向 EPROCESS 和 ETHREAD),而每个进程私有的句柄表则存放
的是对象头(OBJECT_HEADER)
3.PspCidTable是一个独立的句柄表,而每个进程私有的句柄表以一个双链连接起来
PspCidTable 在XP 下是个三层表,动态分配的
PspCidTable 是内核未导出的 HANDLE_TABLE 结构,它保存着所有进程及线程对象指针.只要
可以遍历这个表就可以遍历所有进程,包括隐藏进程(hook ZWQuerySystemInformation),
抹除 PspCidTable 除外,下面根据 KD 查看此表:
图一
通过 PspCidTable 遍历进程就是为了获取进程对象指针。PspCidTable 是根据 PID 或者 CID
索引的,所以就依据进程 CID 获取进程对象指针。
五、演示说明
1.根据 HANDLE 索引object 指针
注意红色部分。ObjectTable: e7313480指向HANDLE_TABLE 结构
TableCode: 0xe5018000最低两位为 00 表示 0 级表。于是查找句柄为 0x14 的对象指针
有(因为索引时按 4 递增的,一个HANDLE_TABLE_ENTRYA 占 8 个字节)
Ptr=HANDLE_TABLE_ENTRY+HANDLE*8/4=HANDLE_TABLE_ENTRY+HANDLE*2
看到 e1c050b9 就是 0x14 对应的 object Header,这里需要转换一下(低三位清零再加上 Object
Header 大小,因为 object header 是 8 个字节对齐,低三位做标志位)
Object=objectHeader&0xfffffff8+sizeof(Object Header)
e1c050b9 &0xfffffff8 + 0x18=e1c050d0-
跟上面的一模一样。
2.根据 CID 索引(PspCidTable句柄表)对象指针
首先是查找 pspcidtable 句柄表的地址:
然后观察 HANDLE_TABLE结构:
TableCode=e3a39001
第二位为 01表示有 2层表。
NextHandleNeedingPool: 0x1000,该字段说明了当 PID或CID达到多少以上时,需要再建
立一个新表(最终表)。我们的系统里现在已经有两个最终表了。最终表大小为 1页即 4K,
而每个表项是一个 HANDLE_TABLE_ENTRY占 8个字节,所以每个表最多只能放512个表
项,又因为表项是用 PID或者 TID来索引的,而索引是以 4递增的,因此一个最终表能承受
的最大 ID也是 2048即 0x800,两个表最大为 0x1000,以此类推…..
TableCode=e3a39001可知道:有 2个基本表
一级表中放的是基本表的指针
0级表的 HANDLE_TABLE_ENTRY= e1005000
计算公式:
PHANDLE_TABLE_ENTRY=HANDLE_TABLE_ENTRY+CID*sizeof(HANDLE_TABLE_ENTRY)/PID_INC
即
PHANDLE_TABLE_ENTRY =HANDLE_TABLE_ENTRY+CID*2
对 CID=0x16C<0x800
下面对这个进行验证。
由于 CID<0x800说明在 0级表中
PHANDLE_TABLE_ENTRY =e1005000+16c*2=e10052d8
得到 EPROCESS的地址为0x88cb7021&(~3)= 0x88cb7020
验证正确。
对于 CID=0x984>0x800
CID>800说明在 1级表中
PHANDLE_TABLE_ENTRY = e3a39000+984*2(e3a3a000+184*2)
得到 EPROCESS的地址为0x88cc78b1&(~3)= 0x88cc78b0,验证正确。
六、基于 PspCidTable 的进程检测及隐藏技术分析
1.基于 HOOK 的检测技术:遍历PspCidTable 所有进程对象指针
由于 PspCidTable 是未导出结构,需要自己定位
【一】PspCidTble 的定位
1.PsLookupProcessByProcessId(PsLookupThreadByThreadId或者PsLookupProcessThreadByCid)
函数中搜索
特征串(0x35ff 和 0x8e)定位 PspCidTalbe
CODE:
2.利用 KDDEBUGGER_DATA(32/64)结构得到 pspCidTable
KdEnableDebugger->KdInitSystem->KdDebuggerDataBlock->KDDEBUGGER_DATA32->PspCi
dTable
3. 利用 KPCR 中取,KRCR 地址
ffdff000处是一个叫做 KPCR的结构,PCR即 Processor Control Region,处理器控制域。这
是一个很有用的结构。系统本身就大量使用。0xffdff000是 KPCR这个结构变量的地址
那么+0x34就是KdVersionBlock成员变量在该结构中的偏移
但是在 0xffdff034指向的地方对应有个结构_DBGKD_GET_VERSION64
可惜的是这个结构只有 0x28字节大小但是....嘿嘿这个结构后面藏着 N多超级重要的内核
变量。我们的 pspcidtable这个变量其实就在这个结构起始位置的 0x80字节偏移处~
【二】进程对象的遍历
1.利用未导出的ExEnumHandleTable函数
2.直接自己获取PHANDLE_TABLE_ENTRY等,然后 PsLookupProcessByProcessId
3.自己写实现遍历(完成)