2.进程创建时,PspCreateProcess()在PspCidTable中以进程对象创建句 柄,是为PID
//
// Create the process ID
3.线程创建时,PspCreateThread()在PspCidTable中以线程对象创建句 柄,是为TID
这儿可以清楚地知道:PID和TID分别是EPROCESS和ETHREAD对象在PspCidTable这个句柄表中的索引
4.进程和线程的查询,主要是以下三个函数,按照给定的PID或TID从PspCidTable从查找相应 的进线程对象
PsLookupProcessThreadByCid()
PsLookupProcessByProcessId()
PsLookupThreadByThreadId()
其中有如下调用:
CidEntry = ExMapHandleToPointer(PspCidTable, ProcessId);
CidEntry = ExMapHandleToPointer(PspCidTable, ThreadId);
6.进程退出时,PspProcessDelete()在PspCidTable中销毁句柄
这里要注意,如果进线程退出时,销毁句柄却发现句柄不存在造成ExDestroyHandle返回失败,可是要蓝屏滴~
所以抹了PspCidTable来隐藏的进程,在退出时必须把进线程对象再放回去,欲知后事如何,请看下回分解~~
这个方法没什么好说的,匹配就是了~
另一种方法是从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
代码如下:
二、如何遍历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就拿到对象了,接下来嘛,该干啥干啥~
然后这样调用:
ExEnumHandleTable(PspCidTable,MyEnumerateHandleRoutine,NULL,&hLastHandle);
好了,打开DebugView看结果吧,还不错~~
第二种方法就是自己遍历PspCidTable了,结构嘛前面已经清楚了,和普通句柄表结构一样,不难下手.
自己实现一个山寨的MyEnumHandleTable了,接口和ExEnumHandleTable一样~~
在回调函数中,我们可以根据情况作出具体处理.若是检测进程,就像我写的那样,检查一下对象类型是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把我雷到了...
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~
很明显了,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作为枚举时正确的句柄上限值。