当前位置:编程学习 > 网站相关 >>

一种基于PspCidTable的进程检测方法

作者:段俊锋

PspCidTable组织结构简介
在Windows NT下,所有的资源都是以对象(object)的方式进行管理。我们平时最常见的进程(process)、文件(file)、设备(device)等都是对象。当我们要访问一个对象时,比如说打开一个文件,系统就会创建一个对象句柄,通过这个句柄来完成对文件的打开、关闭、删除等操作。句柄和对象之间的联系是通过句柄表来完成的。准确地说,一个句柄是它所对应的对象在句柄表中的索引。通过句柄,可以在句柄表找到对象的指针,通过指针对对象进行操作。PspCidTable是Windows系统中一种特殊的句柄表,它不链接在系统句柄表上,也不属于任何一个进程。通过它可以访问系统中所有的对象。在Windbg下面可以查看PspCidTable的地址:

kd> dd PspCidTable
8055a360  e1000860 00000002 00000000 00000000
8055a370  00000000 00000000 00000000 00000000

看得出来,这个系统里PspCidTable的地址为0x8055a360(不同的系统中,这个值是不同的)。这个地址中是一个指向_HANDLE_TABLE结构的指针:

kd> dt _HANDLE_TABLE e1000860
ntdll!_HANDLE_TABLE
+0x000 TableCode    : 0xe1003000
……
+0x038 NextHandleNeedingPool : 0x800
……
+0x040 StrictFIFO    : 0y1

这里的TableCode中记录着句柄表的地址。在Windows 2000中PspCidTable句柄表采用的是三层表结构,TableCode中记录着三层表——上层表、中层表和下层表的地址。但是在Windows XP中,为了节省系统空间,采用了动态扩展的表结构,当句柄数目少的时候仅仅采用下层表,当句柄数目较大的时候系统才会启用中层表,直至上层表。那么怎么判断系统使用了几层表呢?TableCode的后两位是判断的依据,后两位是00则是一层表结构,后两位是01则是两层表结构,后两位是10则是三层表结构。如果系统采用了两层或者三层表的话,TableCode中就不是句柄表地址了,把这个值最后两位置0之后,则是一个指向多层表地址的指针。下面看看另外一个Windows XP系统下的对应的_HANDLE_TABLE。

lkd> dt _HANDLE_TABLE e1001820
nt!_HANDLE_TABLE
+0x000 TableCode   : 0xe27ef001
……
+0x038 NextHandleNeedingPool : 0x1000
……
+0x040 StrictFIFO   : 0y1
lkd> dd 0xe27ef000
e27ef000  e1003000 e27f5000 00000000 00000000
e27ef010  00000000 00000000 00000000 00000000

看得出来,这个系统采用了两层表结构。下层表地址为0xe1003000,中层表地址为0xe27f5000。这其中有一个差别,一般采用一层表结构时,偏移0x038 处的NextHandleNeedingPool是0x800,采用两层表结构时为0x1000(增加了0x800),相应的,采用三层表时为0x1800。
获得了三层句柄表之后,我们就可以通过句柄来访问对象了。具体方法是,如果句柄值小于0x800则将句柄值乘以2作为下层表中的偏移与下层表基地址相加就可以获得一个类型为_HANDLE_TABLE_ENTRY 的地址了,该结构中偏移00处为Object,将这个值最高位置1,最低三位置0就是一个对象(体)指针了;如果句柄值大小大于0x1000,则将句柄值减去0x800再乘以2作为中层表中的偏移与中层表地址相加,同样可以获得一个类型为_HANDLE_TABLE_ENTRY 的地址,依次类推。
下面举一个例子来说明怎么通过句柄来访问对象。Windows XP下,System.exe进程的PID始终为04,实际上就是句柄值。当前系统的句柄表为两层表结构,地址分别为下层表0xe1003000、中层表地址为0xe27f5000,由于04小于0x800,所以_HANDLE_TABLE_ENTRY的地址为0xe1003000+0x04*2=0xe1003008,即:

lkd> dt _HANDLE_TABLE_ENTRY e1003008
nt!_HANDLE_TABLE_ENTRY
+0x000 Object           : 0x8234a661
……
+0x004 NextFreeTableEntry : 0

我们把Object处的指针转换为对象(体)指针,即0x8234a660,看看对不对。

lkd> !object 0x8234a660
Object: 8234a660  Type: (8234aca0) Process
ObjectHeader: 8234a648 (old version)
HandleCount: 2  PointerCount: 90

lkd> !process 0x8234a660 0
PROCESS 8234a660  SessionId: none  Cid: 0004    Peb: 00000000  ParentCid: 0000
DirBase: 006f2000  ObjectTable: e1001c80  HandleCount: 524.
Image: System

看得出来,的确是系统中的System进程(Image为进程名)。前面“lkd> !object 0x8234a660”中显示,对象的Type内容为Process。系统中类型相同的对象,这个地方的值是一样的。进程对象同样如此。同时,在对象(体)指针之前有一个类型为_OBJECT_HEADER的对象(头)指针,记录着对象的一些基本信息,比如类型。上面显示的ObjectHeader为0x8234a648(在对象(体)之前偏移0x18处,0x8234a660-0x8234a648=0x18),即:

lkd> dt _OBJECT_HEADER 8234a648
nt!_OBJECT_HEADER
……
+0x008 Type   : 0x8234aca0 _OBJECT_TYPE
……
+0x018 Body  : _QUAD

偏移08处是类型为_OBJECT_TYPE的Type指针,是一个Process对象。

lkd> dt _OBJECT_TYPE 0x8234aca0
nt!_OBJECT_TYPE
+0x000 Mutex        : _ERESOURCE
……
+0x040 Name         : _UNICODE_STRING "Process"
……
+0x0b0 ObjectLocks    : [4] _ERESOURCE

基于PspCidTable结构的进程检测方法
利用PspCidTable来访问对象,我们已经简单地了解了。接下来介绍一下基于PspCidTable结构的进程检测如何具体实现。
基本原理是这样的:系统内所有进程对象的对象类型(即Type)是一样的,先取得任一进程的类型,然后访问所有可能的句柄值,如果句柄的类型是进程的话,就记录下来,这样的话,系统内所有的进程对象就都被记录下来了。
要解决的第一个问题是获取PspCidTable的地址。前面已经知道了,在不同的系统、不同的系统环境下,PspCidTable的地址是不同的。因为系统函数PsLookupProcessByProcessId中引用了该地址,因而在程序中我们可以通过搜索PsLookupProcessByProcessId的内存空间来获得该地址。我们先看看PsLookupProcessByProcessId的部分反汇编代码。

lkd> u PsLookupProcessByProcessId
nt!PsLookupProcessByProcessId:
805c8ecc 8bff     mov     edi,edi
805c8ece 55      push    ebp
……
805c8ede ff8ed4000000  dec  dword ptr [esi+0D4h]
805c8ee4 ff3560a55580  push dword ptr [nt!PspCidTable (8055a560)]
805c8eea e8c5b40300   call  nt!ExMapHandleToPointer (806043b4)

从上面的代码可知,在PsLookupProcessByProcessId函数的内存空间中搜索0x35ff和0xe8就可以确定PspCidTable的地址了。
要解决的第二个问题是由对象(体)指针确定对象(头)指针。大部分系统中,对象(头)指针在对象(体)指针之前偏移0x18处,但是也有特殊的,所以我们这里使用OBJECT_TO_OBJECT_HEADER宏来实现这个功能。它接受一个参数——对象体指针,返回对象头指针。

#define OBJECT_TO_OBJECT_HEADER(obj) CONTAINING_RECORD( (obj), OBJECT_HEADER, Body )

接着要获取进程对象的类型指针,我们可以取得任一进程对象的地址(其实就是进程对象(体)指针值),然后访问其对象(头)指针,获得进程对象类型的指针。

eproc=(ULONG)PsGetCurrentProcess();
//PsGetCurrentProcess获取当前活动进程的地址
eproc=(ULONG)OBJECT_TO_OBJECT_HEADER(eproc);
type=*(PULONG)(eproc+TYPE);

最后,取得系统的句柄表层数,对所有可能的句柄值分别进行索引,取得对象(体)指针和对象(头)指针,把类型指针与前面所获取的类型指针相同的对象存储下来,这样我们就获得了所有的进程对象了。判断句柄表结构的代码如下。

TableCode=*(PULONG)(*(PULONG)PspCidTable);
if((TableCode&0x3)==0x0) //采用一层表结构
{
table1=TableCode;
table2=0x0;
}
if((TableCode&0x3)==0x1) //采用两层表结构
{
TableCode=TableCode&0xfffffffc;
table1=*(PULONG)TableCode;
table2=*(PULONG)(TableCode+0x4);
}

遍历所有句柄的部分代码如下(句柄范围0x0-0x4e1c参考自《FUTo》)。

for(i=0x0;i<0x4e1c;i++)
{
if(i<=0x800)
{
if(MmIsAddressValid((PULONG)(table1+i*2)))
{
object=*(PULONG)(table1+i*2);
……
}
}
else
{
if(table2!=0)
{
if(MmIsAddressValid((PULONG)(table2+(i-0x800)*2)))
{
object=*(PULONG)(table2+(i-0x800)*2);
……
}
}
}
}

具体细节的补充说明
1)我所给出的基本结构和原理是Windows NT架构下所有系统都采用的,因而在其他系统下这个方法依然可以实现进程检测。但是由于在Winodws 2000/XP/2003下句柄表或者对象的数据结构都有一定的差别,所以在其他系统下需要对相应句柄表访问方式、系统数据偏移量等值进
补充:综合编程 , 其他综合 ,
CopyRight © 2012 站长网 编程知识问答 www.zzzyk.com All Rights Reserved
部份技术文章来自网络,