PspCidTable

来源:互联网 发布:淘宝店装修步骤 编辑:程序博客网 时间:2024/05/21 06:29
PspCidTable也是一个句柄表,其格式与普通的句柄表是完全一样的.但它与每 个进程私有的句柄表有以下不同:
1.PspCidTable中存放的对象是系统中所有的进线程对象,其索引就是PID和TID
2.PspCidTable中存放的直接是对象体(EPROCESS和ETHREAD),而每个进程私有的句柄表则存放的是对象头 (OBJECT_HEADER)
3.PspCidTable是一个独立的句柄表,而每个进程私有的句柄表以一个双链连接起来
注意访问对象时要掩掉低三位,每个进程私有的句柄表是双链连接起来的,实际上ZwQuerySystemInformation枚举系统句柄时就是走的这 条双链,隐藏进程的话,这条链也是要断掉的~~

PspCidTable相关的调用:
1.系统初始化时调用PspInitPhase0()初始化进程管理子系统,此时创建 PspCidTable
  1. PspCidTable = ExCreateHandleTable (NULL);  //创建!
  2.     if (PspCidTable == NULL) {
  3.         return FALSE;
  4.      }
  5.  
  6.     //
  7.     // Set PID and TID reuse to strict FIFO. This isn't absolutely needed but
  8.     // it makes tracking audits easier.
  9.     //
  10.      ExSetHandleTableStrictFIFO (PspCidTable);
  11.  
  12.      ExRemoveHandleTable (PspCidTable);  //使得PspCidTable独立于其它句柄表

2.进程创建时,PspCreateProcess()在PspCidTable中以进程对象创建句 柄,是为PID
//

    // Create the process ID

  1.     //
  2.  
  3.      CidEntry.Object = Process;
  4.      CidEntry.GrantedAccess = 0;
  5.      Process->UniqueProcessId = ExCreateHandle (PspCidTable, &CidEntry);  //进程的PID是这么来的
  6.     if (Process->UniqueProcessId == NULL) {
  7.          Status = STATUS_INSUFFICIENT_RESOURCES;
  8.         goto exit_and_deref;
  9.      }

3.线程创建时,PspCreateThread()在PspCidTable中以线程对象创建句 柄,是为TID

  1.      Thread->ThreadsProcess = Process;
  2.      Thread->Cid.UniqueProcess = Process->UniqueProcessId;
  3.  
  4.      CidEntry.Object = Thread;
  5.      CidEntry.GrantedAccess = 0;
  6.      Thread->Cid.UniqueThread = ExCreateHandle (PspCidTable, &CidEntry); //线程的TID
  7.  
  8.     if (Thread->Cid.UniqueThread == NULL) {
  9.          ObDereferenceObject (Thread);
  10.         return (STATUS_INSUFFICIENT_RESOURCES);
  11.      }


这儿可以清楚地知道:PID和TID分别是EPROCESS和ETHREAD对象在PspCidTable这个句柄表中的索引。

4.进程和线程的查询,主要是以下三个函数,按照给定的PID或TID从PspCidTable从查找相应 的进线程对象
PsLookupProcessThreadByCid()
PsLookupProcessByProcessId()
PsLookupThreadByThreadId()
其中有如下调用:
CidEntry = ExMapHandleToPointer(PspCidTable, ProcessId);
CidEntry = ExMapHandleToPointer(PspCidTable, ThreadId);


ExMapHandleToPointer内部仍然是调用ExpLookupHandleTableEntry()根据指定的句柄查找相应的 HANDLE_TABEL_ENTRY,从而获取Object

5.线程退出时,PspThreadDelete()在PspCidTable中销毁句柄
  1. if (Thread->Cid.UniqueThread != NULL) {
  2.         if (!ExDestroyHandle (PspCidTable, Thread->Cid.UniqueThread, NULL)) {
  3.              KeBugCheck(CID_HANDLE_DELETION);
  4.          }
  5.      }


6.进程退出时,PspProcessDelete()在PspCidTable中销毁句柄

  1. if (Process->UniqueProcessId) {
  2.         if (!(ExDestroyHandle (PspCidTable, Process->UniqueProcessId, NULL))) {
  3.              KeBugCheck (CID_HANDLE_DELETION);
  4.          }
  5.      }


这里要注意,如果进线程退出时,销毁句柄却发现句柄不存在造成ExDestroyHandle返回失败,可是要蓝屏滴~
所以抹了PspCidTable来隐藏的进程,在退出时必须把进线程对象再放回去,欲知后事如何,请看下回分解~~

PspCidTable的攻与防,其实就是进程隐藏与检测所涉及到的一部分 工作~~
不管基于PspCidTable的进线程检测,还是抹PspCidTable进行进程对象的隐藏,都涉及到对 PspCidTable的遍历,所以如何安全正确地遍历PspCidTable才是该技术的关键。
一、获取PspCidTable的地址
常用的方法是从前面提到的三个查询PspCidTable的函数中特征搜索.
PsLookupProcessThreadByCid()
PsLookupProcessByProcessId()
PsLookupThreadByThreadId()
比如PsLookupProcessByProcessId()中:
  1. lkd> u
  2. nt!PsLookupProcessByProcessId+0x12:
  3. 8057ce2f ff8ed4000000     dec      dword ptr [esi+0D4h]
  4. 8057ce35 ff35e0955680     push     dword ptr [nt!PspCidTable (805695e0)] //就是这儿了
  5. 8057ce3b e89b1dffff       call     nt!ExMapHandleToPointer (8056ebdb)
  6. 8057ce40 8bd8             mov      ebx,eax

这个方法没什么好说的,匹配就是了~
另一种方法是从KPCR中取,我比较喜欢这种方法:
lkd> dt _KPCR ffdff000
nt!_KPCR
+0x000 NtTib            : _NT_TIB
...
+0x034 KdVersionBlock   : 0x80555038
lkd> dd 0x80555038 
80555038 0a28000f 00020006 030c014c 0000002d
80555048 804e0000 ffffffff 80563420 ffffffff //这里分别是KernelBase和PsLoadedModuleList
...
805550a8 80563420 00000000 805694d8 00000000 //这里是PsLoadedModuleList和PsActiveProcessHead
805550b8 805695e0 00000000 8056ba08 00000000 //这里是PspCidTable和ExpSystemResourcesList
代码如下:

  1. PHANDLE_TABLE PspCidTable;
  2. _asm
  3. {
  4.     mov eax,fs:[0x34]
  5.     mov eax,[eax+0x80]
  6.     mov eax,[eax]
  7.     mov PspCidTable,eax
  8. }
  9. DbgPrint("PspCidTable=0x%08X\n",PspCidTable);

二、如何遍历PspCidTable
第一种方法是使用导出的ExEnumHandleTable,优点是该函数导出了,用起来安全快捷
可能的缺点也是因为被导出了,所以比较容易被XX再XXX,不是足够可靠~
函数原型如下:
NTKERNELAPI
BOOLEAN
ExEnumHandleTable (
PHANDLE_TABLE HandleTable,
EX_ENUMERATE_HANDLE_ROUTINE EnumHandleProcedure,
PVOID EnumParameter,
PHANDLE Handle
);
typedef BOOLEAN (*EX_ENUMERATE_HANDLE_ROUTINE)(
IN PHANDLE_TABLE_ENTRY HandleTableEntry,
IN HANDLE Handle,
IN PVOID EnumParameter
);
我们只要自己实现EnumHandleProcedure就可以了,传递给我们的参数有HANDLE_TABEL_ENTRY的指针和对应的句柄.
HandleTableEntry->Object就拿到对象了,接下来嘛,该干啥干啥~

  1. BOOLEAN MyEnumerateHandleRoutine(
  2.      IN PHANDLE_TABLE_ENTRY HandleTableEntry,
  3.      IN HANDLE Handle,
  4.      IN PVOID EnumParameter
  5.      )
  6. {
  7.     BOOLEAN Result=FALSE;
  8.     ULONG ProcessObject;
  9.     ULONG ObjectType;
  10.  
  11.      ProcessObject=(HandleTableEntry->Value)&~7; //掩去低三位
  12.     
  13.      ObjectType=*(ULONG*)(ProcessObject-0x10);//取对象类型
  14.     if (ObjectType==(ULONG)PsProcessType)//判断是否为Process
  15.      {
  16.          (*(ULONG*)EnumParameter)++;
  17.         //注意PID其实就是Handle,而 不是从EPROCESS中取,可以对付伪pid
  18.          DbgPrint("PID=%4d\t EPROCESS=0x%08X %s\n",Handle,ProcessObject,PsGetProcessImageFileName((PEPROCESS)ProcessObject));
  19.      }
  20.     return Result;//返回FALSE继续
  21. }

然后这样调用:
ExEnumHandleTable(PspCidTable,MyEnumerateHandleRoutine,NULL,&hLastHandle);
好了,打开DebugView看结果吧,还不错~~

第二种方法就是自己遍历PspCidTable了,结构嘛前面已经清楚了,和普通句柄表结构一样,不难下手.
自己实现一个山寨的MyEnumHandleTable了,接口和ExEnumHandleTable一样~~

  1. #define   MAX_ENTRY_COUNT (0x1000/8)  //一级表中的 HANDLE_TABLE_ENTRY个数
  2. #define   MAX_ADDR_COUNT   (0x1000/4) //二级表和 三级表中的地址个数
  3.  
  4. BOOLEAN
  5. MyEnumHandleTable (
  6. PHANDLE_TABLE HandleTable,
  7. MY_ENUMERATE_HANDLE_ROUTINE EnumHandleProcedure,
  8. PVOID EnumParameter,
  9. PHANDLE Handle
  10.      )
  11. {
  12. ULONG i,j,k;
  13. ULONG_PTR CapturedTable;
  14.     ULONG TableLevel;
  15. PHANDLE_TABLE_ENTRY TableLevel1,*TableLevel2,**TableLevel3;
  16. BOOLEAN CallBackRetned=FALSE;
  17. BOOLEAN ResultValue=FALSE;
  18. ULONG MaxHandle;
  19. //判断几个参数是否有效
  20. if (!HandleTable
  21.    && !EnumHandleProcedure
  22.    && !MmIsAddressValid(Handle))
  23. {
  24.   return ResultValue;
  25. }
  26. //取表基址和表的级数
  27. CapturedTable=(HandleTable->TableCode)&~3;
  28. TableLevel=(HandleTable->TableCode)&3;
  29. MaxHandle=HandleTable->NextHandleNeedingPool;
  30. DbgPrint("句柄上限值为0x%X\n",MaxHandle);
  31. //判断表的等级
  32. switch(TableLevel)
  33. {
  34. case 0:
  35.    {
  36.    //一级表
  37.     TableLevel1=(PHANDLE_TABLE_ENTRY)CapturedTable;
  38.     DbgPrint("解析一级表 0x%08x...\n",TableLevel1);
  39.    for (i=0;i<MAX_ENTRY_COUNT;i++)
  40.     {
  41.      *Handle=(HANDLE)(i*4);
  42.     if (TableLevel1[i].Object && MmIsAddressValid(TableLevel1[i].Object))
  43.      {
  44.      //对象有效时,再调用回调函数
  45.       CallBackRetned=EnumHandleProcedure(&TableLevel1[i],*Handle,EnumParameter);
  46.      if (CallBackRetned)  break;
  47.      }
  48.     }//end of for i
  49.     ResultValue=TRUE;
  50.    
  51.    }
  52.   break;
  53. case 1:
  54.    {
  55.    //二级表
  56.     TableLevel2=(PHANDLE_TABLE_ENTRY*)CapturedTable;
  57.     DbgPrint("解析二级表 0x%08x...\n",TableLevel2);
  58.     DbgPrint("二级表的个 数:%d\n",MaxHandle/(MAX_ENTRY_COUNT*4));
  59.    for (j=0;j<MaxHandle/(MAX_ENTRY_COUNT*4);j++)
  60.     {
  61.      TableLevel1=TableLevel2[j];
  62.     if (!TableLevel1)
  63.      break//为零则跳出
  64.     for (i=0;i<MAX_ENTRY_COUNT;i++)
  65.      {
  66.       *Handle=(HANDLE)(j*MAX_ENTRY_COUNT*4+i*4);
  67.      if (TableLevel1[i].Object && MmIsAddressValid(TableLevel1[i].Object))
  68.       {
  69.       //对象有效时,再调用回调函数
  70.        CallBackRetned=EnumHandleProcedure(&TableLevel1[i],*Handle,EnumParameter);
  71.       if (CallBackRetned)  break;
  72.       //DbgPrint("Handle=%d\tObject=0x%08X\n",Handle,(TableLevel1[i].Value)&~3);
  73.       }
  74.      }//end of for i
  75.     }//end of for j
  76.     ResultValue=TRUE;
  77.    }
  78.   break;
  79. case 2:
  80.    {
  81.    //三级表
  82.     TableLevel3=(PHANDLE_TABLE_ENTRY**)CapturedTable;
  83.     DbgPrint("解析三级表 0x%08x...\n",TableLevel3);
  84.     DbgPrint("三级表的个 数:%d\n",MaxHandle/(MAX_ENTRY_COUNT*4*MAX_ADDR_COUNT));
  85.    for (k=0;k<MaxHandle/(MAX_ENTRY_COUNT*4*MAX_ADDR_COUNT);k++)
  86.     {
  87.      TableLevel2=TableLevel3[k];
  88.     if (!TableLevel2)
  89.      break//为零则跳出
  90.     for (j=0;j<MaxHandle/(MAX_ENTRY_COUNT*4);j++)
  91.      {
  92.       TableLevel1=TableLevel2[j];
  93.      if (!TableLevel1)
  94.       break//为零则跳出
  95.      for (i=0;i<MAX_ENTRY_COUNT;i++)
  96.       {
  97.        *Handle=(HANDLE)(k*MAX_ENTRY_COUNT*MAX_ADDR_COUNT+j*MAX_ENTRY_COUNT+i*4);
  98.       if (TableLevel1[i].Object && MmIsAddressValid(TableLevel1[i].Object))
  99.        {
  100.        //对象有效时,再调用回调函数
  101.         CallBackRetned=EnumHandleProcedure(&TableLevel1[i],*Handle,EnumParameter);
  102.        if (CallBackRetned)  break;
  103.        //DbgPrint("Handle=%d\tObject=0x%08X\n",Handle,(TableLevel1[i].Value)&~3);
  104.        }
  105.       }//end of for i
  106.      }//end of for j
  107.     }//end of for k
  108.     ResultValue=TRUE;
  109.    }
  110.      break;
  111. default:
  112.    {
  113.     DbgPrint("Shoud NOT get here!\n");
  114.    }
  115.      break;
  116. }//end of switch
  117. return ResultValue;
  118. }

在回调函数中,我们可以根据情况作出具体处理.若是检测进程,就像我写的那样,检查一下对象类型是Process的就记录之~
若是抹PspCidTable,则将相应的Object清零,并设置到FirstFree,达到这个Handle就像真的被Destroy了一样的效果~
Futo_enhanced的驱动中是这样写的(稍稍改动了一下,假设是在抹掉一个EPROCESS):
p_tableEntry[a].Object = 0;
p_tableEntry[a].GrantedAccess = PspCidTable->FirstFree;
PspCidTable->FirstFree = pid ;
这里涉及到句柄的分配算法了.这样,基于PspCidTable的进程检测就歇菜了.但是上一篇的分析提到了,进线程退出时会调用 ExDestroyHandle()销毁句柄,若找不到就会蓝屏.因此,必须在被保护的目标进程及其线程退出前的某个时候把抹掉的进线程对象再放回去.结 束线程其实就是给线程插APC执行PspExitThread(),而PspExitThread()会调用 PspCreateThreadNotifyRoutine,因此在这个回调中把线程对象放回去是合适的.同法,进程在退出时调用的 PspExitProcess()也会执行PspCreateProcessNotifyRoutine,此时再把进程对象放回去.这里我想如不设置 FirstFree为我们抹掉的句柄项,这个被我们抹掉的HANDLE_TABLE_ETNRY项会被保留吗?
另外也可以来个ObjectHook,Fuck掉PsProcessType->TypeInfo->DeleteProcedure和 PsThreadType->TypeInfo->DeleteProcedure然后是隐藏掉的进程就照顾一下~~~
下面是PsProcessType的部分内容:
   +0x02c DumpProcedure    : (null) 
+0x030 OpenProcedure    : (null) 
+0x034 CloseProcedure   : (null) 
+0x038 DeleteProcedure : 0x805d2cdc     void nt!PspProcessDelete+0
   +0x03c ParseProcedure   : (null) 
+0x040 SecurityProcedure : 0x805f9150     long nt!SeDefaultObjectMethod+0
+0x044 QueryNameProcedure : (null) 
+0x048 OkayToCloseProcedure : (null)

总之,只要在PspCidTable中找到空位把目标进程的EPROCESS和ETHREAD再放回去,并且其索引与 EPROCESS->UniqueProcessId和ETHREAD->Cid.UniqueThread保持一致就可以了.抽点时间写个 隐藏进程的Demo练习下~~

    PspCidTable现在已经很科普了,关于其具体格式及如何枚举,网上相关 文章一大堆,最多的两篇是gz1x和sudami写的。我这里只谈一个问题,就是枚举PspCidTable时cid的上限问题。很多人提到以一个 Magic Number为上限,其值为0x4e1c,比如sudami的《PspCidTable杂谈》。虽然大多数时候这个上限已经大到足够用了,但事实上这个上 限值是不可靠的,附张图,这个极BT的pid把我雷到了...

PspCidTable综合概述(转载) - 我,猪八戒 - 我,猪八戒的博客
看到这张图后,我自己也写个了程序来验证了一下,更证实了0x4e1c这个值的不可靠。
程序如下:
///////////////////////代码开始////////////////////
#include "stdafx.h"
#include <windows.h>
#include <stdio.h>

DWORD WINAPI ThreadProc(LPVOID lpParameter );
HANDLE hEvent=NULL;
int main(int argc, char* argv[])
{

int i=0;
DWORD tid=0;
hEvent=CreateEvent(NULL,FALSE,TRUE,"TEST");
for (i=0;i<2000;i++)
{

CreateThread(NULL,0,ThreadProc,NULL,NULL,&tid);
printf("Runing %d...TID=%d\n",i,tid);
Sleep(20);
}
while (1)
{
Sleep(100);
}
return 0;
}

DWORD WINAPI ThreadProc(LPVOID lpParameter )
{
WaitForSingleObject(hEvent,INFINITE);
return 0;
}
///////////////////////代码开始////////////////////

虽然ProcessId和ThreadId同样在PspCidTable占位,但是创建线程的开销明显要小于创建进程,因此这里创建2000个线程,而且 这个线程里等待一个无信号的事件而进入挂起状态,运行开销也要小得多。(当然也可以直接在创建时就挂起)运行这个程序,可以直接从返回的ThreadId 明显感觉到PspCidTable的膨胀。在我把这个程序运行了四次之后,就可以看到pid=25508=0x63A4(此时其线程的tid更大),再运 行Windbg可看到其pid=33736=0x83C8,很BT~

PspCidTable综合概述(转载) - 我,猪八戒 - 我,猪八戒的博客

    很明显了,0x4e1c这个不知道哪儿来的数(貌似是BlackLight提出的)并不可靠,正确的方法呢?
正确方法就是以PspCidTable的NextHandleNeedingPool为上限。
来看一下当前的PspCidTable:
lkd> dd pspcidtable L1
805695e0 e1001a00
lkd> dt _HANDLE_TABLE e1001a00
nt!_HANDLE_TABLE
+0x000 TableCode        : 0xe12e4001
+0x004 QuotaProcess     : (null) 
+0x008 UniqueProcessId : (null) 
+0x00c HandleTableLock : [4] _EX_PUSH_LOCK
+0x01c HandleTableList : _LIST_ENTRY [ 0xe1001a1c - 0xe1001a1c ]
+0x024 HandleContentionEvent : _EX_PUSH_LOCK
+0x028 DebugInfo        : (null) 
+0x02c ExtraInfoPages   : 0
+0x030 FirstFree        : 0x86d0
+0x034 LastFree         : 0x86bc
+0x038 NextHandleNeedingPool : 0x8800
+0x03c HandleCount      : 8380
+0x040 Flags            : 1
+0x040 StrictFIFO       : 0y1
可以看到NextHandleNeedingPool值为0x8800=34816,这对当前情况来说是个很合理的值。其实由Windows分配句柄表的 原理可知,0x8800才是当前句柄表(PspCidTable)的句柄上限,仅当当前Pool已满,不能放下更多对象时,才会再次分配一个一级表,而 NextHandleNeedingPool也将增大到0x9000,步长为一个一级表的句柄大小0x800.

在枚举PspCidTable时就不再用0x4e1c的另一个理由是(当然,前面那张图已经很有说服力了,哈哈):系统自己在枚举句柄表时使用的 ExEnumHandleTable函数仍然是调用了ExpLookupHandleTableEntry来实现的。而 ExpLookupHandleTableEntry的开头是这样写的:

////////////省略无关代码//////////
MaxHandle = *(volatile ULONG *) &HandleTable->NextHandleNeedingPool;

    //
// See if this can be a valid handle given the table levels.
//
if (Handle.Value >= MaxHandle) {
return NULL;        
}
////////////省略无关代码//////////

看到了吗?系统也是以NextHandleNeedingPool的值作为当前句柄表中的句柄上限,即使你用0x4e1c来暴力枚举,在超出 NextHandleNeedingPool之后也得不到哪怕一个有效的进线程对象。不用多说什么了,以后我们要忘掉0x4e1c,使用 NextHandleNeedingPool作为枚举时正确的句柄上限值。


0 0