【旧文章搬运】PspCidTable攻与防

原文发表于百度空间,2009-03-29
==========================================================================

PspCidTable的攻与防,其实就是进程隐藏与检测所涉及到的一部分工作~~
不管基于PspCidTable的进线程检测,还是抹PspCidTable进行进程对象的隐藏,都涉及到对PspCidTable的遍历.
所以如何安全正确地遍历PspCidTable才是该技术的关键
一、获取PspCidTable的地址
常用的方法是从前面提到的三个查询PspCidTable的函数中特征搜索.
PsLookupProcessThreadByCid()
PsLookupProcessByProcessId()
PsLookupThreadByThreadId()
比如PsLookupProcessByProcessId()中:

lkd> u
nt!PsLookupProcessByProcessId+0x12:
8057ce2f ff8ed4000000     dec      dword ptr [esi+0D4h]
8057ce35 ff35e0955680     push     dword ptr [nt!PspCidTable (805695e0)] //就是这儿了
8057ce3b e89b1dffff       call     nt!ExMapHandleToPointer (8056ebdb)
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

代码如下:

PHANDLE_TABLE PspCidTable;
_asm
{
    mov eax,fs:[0x34]
    mov eax,[eax+0x80]
    mov eax,[eax]
    mov PspCidTable,eax
}
DbgPrint("PspCidTable=0x%08X
",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就拿到对象了,接下来嘛,该干啥干啥~

BOOLEAN MyEnumerateHandleRoutine(
     IN PHANDLE_TABLE_ENTRY HandleTableEntry,
     IN HANDLE Handle,
     IN PVOID EnumParameter
     )
{
    BOOLEAN Result=FALSE;
    ULONG ProcessObject;
    ULONG ObjectType;
     ProcessObject=(HandleTableEntry->Value)&~7; //掩去低三位
    
     ObjectType=*(ULONG*)(ProcessObject-0x10);//取对象类型
    if (ObjectType==(ULONG)PsProcessType)//判断是否为Process
     {
         (*(ULONG*)EnumParameter)++;
        //注意PID其实就是Handle,而不是从EPROCESS中取,可以对付伪pid
         DbgPrint("PID=%4d	 EPROCESS=0x%08X %s
",Handle,ProcessObject,PsGetProcessImageFileName((PEPROCESS)ProcessObject));
     }
    return Result;//返回FALSE继续
}

然后这样调用:

ExEnumHandleTable(PspCidTable,MyEnumerateHandleRoutine,NULL,&hLastHandle);
好了,打开DebugView看结果吧,还不错~~

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

#define   MAX_ENTRY_COUNT (0x1000/8)  //一级表中的HANDLE_TABLE_ENTRY个数
#define   MAX_ADDR_COUNT   (0x1000/4) //二级表和三级表中的地址个数
BOOLEAN
MyEnumHandleTable (
PHANDLE_TABLE HandleTable,
MY_ENUMERATE_HANDLE_ROUTINE EnumHandleProcedure,
PVOID EnumParameter,
PHANDLE Handle
     )
{
ULONG i,j,k;
ULONG_PTR CapturedTable;
    ULONG TableLevel;
PHANDLE_TABLE_ENTRY TableLevel1,*TableLevel2,**TableLevel3;
BOOLEAN CallBackRetned=FALSE;
BOOLEAN ResultValue=FALSE;
ULONG MaxHandle;
//判断几个参数是否有效
if (!HandleTable
   && !EnumHandleProcedure
   && !MmIsAddressValid(Handle))
{
  return ResultValue;
}
//取表基址和表的级数
CapturedTable=(HandleTable->TableCode)&~3;
TableLevel=(HandleTable->TableCode)&3;
MaxHandle=HandleTable->NextHandleNeedingPool;
DbgPrint("句柄上限值为0x%X
",MaxHandle);
//判断表的等级
switch(TableLevel)
{
case 0:
   {
   //一级表
    TableLevel1=(PHANDLE_TABLE_ENTRY)CapturedTable;
    DbgPrint("解析一级表0x%08x...
",TableLevel1);
   for (i=0;i<MAX_ENTRY_COUNT;i++)
    {
     *Handle=(HANDLE)(i*4);
    if (TableLevel1[i].Object && MmIsAddressValid(TableLevel1[i].Object))
     {
     //对象有效时,再调用回调函数
      CallBackRetned=EnumHandleProcedure(&TableLevel1[i],*Handle,EnumParameter);
     if (CallBackRetned)  break;
     }
    }//end of for i
    ResultValue=TRUE;
   
   }
  break;
case 1:
   {
   //二级表
    TableLevel2=(PHANDLE_TABLE_ENTRY*)CapturedTable;
    DbgPrint("解析二级表0x%08x...
",TableLevel2);
    DbgPrint("二级表的个数:%d
",MaxHandle/(MAX_ENTRY_COUNT*4));
   for (j=0;j<MaxHandle/(MAX_ENTRY_COUNT*4);j++)
    {
     TableLevel1=TableLevel2[j];
    if (!TableLevel1)
     break; //为零则跳出
    for (i=0;i<MAX_ENTRY_COUNT;i++)
     {
      *Handle=(HANDLE)(j*MAX_ENTRY_COUNT*4+i*4);
     if (TableLevel1[i].Object && MmIsAddressValid(TableLevel1[i].Object))
      {
      //对象有效时,再调用回调函数
       CallBackRetned=EnumHandleProcedure(&TableLevel1[i],*Handle,EnumParameter);
      if (CallBackRetned)  break;
      //DbgPrint("Handle=%d	Object=0x%08X
",Handle,(TableLevel1[i].Value)&~3);
      }
     }//end of for i
    }//end of for j
    ResultValue=TRUE;
   }
  break;
case 2:
   {
   //三级表
    TableLevel3=(PHANDLE_TABLE_ENTRY**)CapturedTable;
    DbgPrint("解析三级表0x%08x...
",TableLevel3);
    DbgPrint("三级表的个数:%d
",MaxHandle/(MAX_ENTRY_COUNT*4*MAX_ADDR_COUNT));
   for (k=0;k<MaxHandle/(MAX_ENTRY_COUNT*4*MAX_ADDR_COUNT);k++)
    {
     TableLevel2=TableLevel3[k];
    if (!TableLevel2)
     break; //为零则跳出
    for (j=0;j<MaxHandle/(MAX_ENTRY_COUNT*4);j++)
     {
      TableLevel1=TableLevel2[j];
     if (!TableLevel1)
      break; //为零则跳出
     for (i=0;i<MAX_ENTRY_COUNT;i++)
      {
       *Handle=(HANDLE)(k*MAX_ENTRY_COUNT*MAX_ADDR_COUNT+j*MAX_ENTRY_COUNT+i*4);
      if (TableLevel1[i].Object && MmIsAddressValid(TableLevel1[i].Object))
       {
       //对象有效时,再调用回调函数
        CallBackRetned=EnumHandleProcedure(&TableLevel1[i],*Handle,EnumParameter);
       if (CallBackRetned)  break;
       //DbgPrint("Handle=%d	Object=0x%08X
",Handle,(TableLevel1[i].Value)&~3);
       }
      }//end of for i
     }//end of for j
    }//end of for k
    ResultValue=TRUE;
   }
     break;
default:
   {
    DbgPrint("Shoud NOT get here!
");
   }
     break;
}//end of switch
return ResultValue;
}

在回调函数中,我们可以根据情况作出具体处理.若是检测进程,就像我写的那样,检查一下对象类型是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练习下~~

原文地址:https://www.cnblogs.com/achillis/p/10181267.html