PspCidTable 完全解读

PspCidTable 完全解读 by sysdog 2009.5.1 Whu

-------------------------------------------------------------------------------


一、句柄

句柄:HADNLE winnt.h 定义如下:

typedef void *HANDLE;

是一个 32 位无类型指针,充当内核对象在内核的索引.

句柄本质内核对象(object)的索引

 

每个进程都有自己的进程句柄表,当操作进程对象时,会把对象指针放在自己的句柄表中,

返回一个句柄,通过句柄来索引对象指针,然后操作对象。

进程句柄表是根据 HANDLE 索引对象指针的,而pspcidtable(未导出,需要自己定位地址)

是根据 CID 索引对象指针的.

EPROCESS ETHREAD 分别是进程对象和线程对象

PspCidTable 完全解读_第1张图片


PspCidTable 完全解读_第2张图片

NT 执行体处理进程线程对象,进程和线程描述表可由 EPROCESS 结构来访问,NT 内核处理进

程和线程描述表

二、句柄表

XP 下句柄表是动态分配的,是个三层表,1 级表(称为基本表)存放系统底层的。

句柄表大小为 1页即 4K,而每个表项是一个HANDLE_TABLE_ENTRY8个字节,所以

每个表最多只能放 512个表项,又因为表项索引是以 4递增的,因此一个最终表能承受的最

大索引值也是 2048 0x800,两个表最大为 0x1000,三个表就是 0x1800

句柄表的格式如下:

PspCidTable 完全解读_第3张图片

句柄表就是 HANDLE_TABLE 结构,如下:

PspCidTable 完全解读_第4张图片

里面没有指针结构啊,那么句柄表到底存储在那里?

实际是保存在 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结构如下:

PspCidTable 完全解读_第5张图片

如果是一级表 HANDLE_TABLE_ENTRY=TableCode

如果是二级表 HANDLE_TABLE_ENTRY需要根据 2级表中的指针来找 1级表的基址,然后

再根据 1级表中的偏移找到进程对象…..三级表类推

PspCidTable 完全解读_第6张图片

三、内核对象

作为一个 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 完全解读_第7张图片

PspCidTable:指向 HANDLE_TABLE的指针,未导出结构,存放所有进程及线程对象指针。

PspCidTable 完全解读_第8张图片

PspCidTable 是一个句柄表,其格式与普通的句柄表是完全一样的.但它与每个进程私有的句

柄表有以下不同:

1.PspCidTable中存放的对象是系统中所有的进线程对象指针,其索引就是 PID CID

2.PspCidTable中存放是对象体(指向 EPROCESS ETHREAD),而每个进程私有的句柄表则存放

的是对象头(OBJECT_HEADER)

3.PspCidTable是一个独立的句柄表,而每个进程私有的句柄表以一个双链连接起来

PspCidTable XP 下是个三层表,动态分配的

PspCidTable 是内核未导出的 HANDLE_TABLE 结构,它保存着所有进程及线程对象指针.只要

可以遍历这个表就可以遍历所有进程,包括隐藏进程(hook ZWQuerySystemInformation),

抹除 PspCidTable 除外,下面根据 KD 查看此表:

PspCidTable 完全解读_第9张图片

图一

通过 PspCidTable 遍历进程就是为了获取进程对象指针。PspCidTable 是根据 PID 或者 CID

索引的,所以就依据进程 CID 获取进程对象指针。

五、演示说明

1.根据 HANDLE 索引object 指针

 PspCidTable 完全解读_第10张图片

注意红色部分。ObjectTable: e7313480指向HANDLE_TABLE 结构

PspCidTable 完全解读_第11张图片

 

TableCode: 0xe5018000最低两位为 00 表示 0 级表。于是查找句柄为 0x14 的对象指针

有(因为索引时按 4 递增的,一个HANDLE_TABLE_ENTRYA 8 个字节)

Ptr=HANDLE_TABLE_ENTRY+HANDLE*8/4=HANDLE_TABLE_ENTRY+HANDLE*2

PspCidTable 完全解读_第12张图片

看到 e1c050b9 就是 0x14 对应的 object Header,这里需要转换一下(低三位清零再加上 Object

Header 大小,因为 object header 8 个字节对齐,低三位做标志位

Object=objectHeader&0xfffffff8+sizeof(Object Header)

e1c050b9 &0xfffffff8 + 0x18=e1c050d0-

PspCidTable 完全解读_第13张图片

跟上面的一模一样。

2.根据 CID 索引(PspCidTable句柄表)对象指针

首先是查找 pspcidtable 句柄表的地址:

PspCidTable 完全解读_第14张图片

然后观察 HANDLE_TABLE结构:

PspCidTable 完全解读_第15张图片

TableCode=e3a39001


第二位为 01表示有 2层表。


NextHandleNeedingPool 0x1000,该字段说明了当 PIDCID达到多少以上时,需要再建

立一个新表(最终表)。我们的系统里现在已经有两个最终表了。最终表大小为 1页即 4K

而每个表项是一个 HANDLE_TABLE_ENTRY 8个字节,所以每个表最多只能放512个表

项,又因为表项是用 PID或者 TID来索引的,而索引是以 4递增的,因此一个最终表能承受

的最大 ID也是 2048 0x800,两个表最大为 0x1000,以此类推…..

PspCidTable 完全解读_第16张图片

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

 PspCidTable 完全解读_第17张图片

得到 EPROCESS的地址为0x88cb7021&~3= 0x88cb7020

验证正确。

对于 CID=0x984>0x800

CID>800说明在 1级表中

PHANDLE_TABLE_ENTRY = e3a39000+984*2e3a3a000+184*2

PspCidTable 完全解读_第18张图片

得到 EPROCESS的地址为0x88cc78b1&~3= 0x88cc78b0,验证正确。

六、基于 PspCidTable 的进程检测及隐藏技术分析

1.基于 HOOK 的检测技术:遍历PspCidTable 所有进程对象指针

由于 PspCidTable 是未导出结构,需要自己定位

【一】PspCidTble 的定位

1.PsLookupProcessByProcessId(PsLookupThreadByThreadId或者PsLookupProcessThreadByCid)

函数中搜索

特征串(0x35ff 0x8e)定位 PspCidTalbe

CODE:

PspCidTable 完全解读_第19张图片

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字节偏移处~

PspCidTable 完全解读_第20张图片

【二】进程对象的遍历

1.利用未导出的ExEnumHandleTable函数

2.直接自己获取PHANDLE_TABLE_ENTRY等,然后 PsLookupProcessByProcessId

3.自己写实现遍历(完成)



你可能感兴趣的:(PspCidTable 完全解读)