-------------------------------------这是分割线---------------------------------------
Windows句柄表格式
句柄是Windows对象管理中引入的一个东西,它的实际意义是对象在句柄表中的索引。Windows2000使用的是固定的三层句柄表,而WindowsXP和Windows2003都是使用的动态可扩展的三层句柄表,这是一种很优秀的结构,易扩展,且查找迅速,值得学习。通常情况下每个进程的句柄表都是一级表,当句柄数超过一级表的容量时,就会扩展为二级表,此时二级表中放的是指向一级表的指针。同样,当二级表也放满时,就会扩展为三级表,里面放指向二级表的指针。但是通常我们看不到这种情况,因为就目前的大多数情况来说,二级表的容量已经够我们用了。
接下来具体地看一下进程的句柄表。每个进程都有一个句柄表,包括System进程(不过System的句柄表稍有点特殊),但是Idle除外,因为它并不是一个事实上真正的进程。在进程对象EPROCESS中,可以直接找到句柄表指针。
来找个进程对象看看:
lkd> dt _EPROCESS 8a92cb98
nt!_EPROCESS
句柄表是一个HANDLE_TABLE结构,继续来看:
lkd> dt _HANDLE_TABLE0xe23d3690
nt!_HANDLE_TABLE
这里面最重要的,当然就是TableCode了。
TableCode的低两位被用作标志位,用于表示当前句柄表的级数。0,1,2分别表示一级表,二级表,三级表。
比如这里的0xe3202001,掩去高位,低两位的值为1,即为二级表。对于二级表,表中存放的是指向一级表的指针,一级表又被称为基本表,这实际上一个HANDLE_TABLE_ENTRY数组,它里面存放的HANDLE_TABLE_ENTRY中才是真正的对象,其定义如下:
typedef struct _HANDLE_TABLE_ENTRY{
} HANDLE_TABLE_ENTRY,*PHANDLE_TABLE_ENTRY;
一级表可以放多大的句柄呢?按WRK中的宏展开,有(TABLE_PAGE_SIZE /sizeof(HANDLE_TABLE_ENTRY))*HANDLE_VALUE_INC,即0x1000/8*4=0x800。
计算过程:一级表(基本表)的大小为一页即4K=0x1000Byte,而HANDLE_TABLE_ENRY大小为8字节,则可存放的个数为0x1000/8=0x200=512,而句柄以4为步进,因此最大句柄为0x200*4=0x800.其中可存放的最大句柄不超过0x800(最大为0x800-4),而每个一级表的第一个HANDLE_TABLE_ENTRY的Object总是为0,因为我们都知道0是一个无效的句柄,它不指向一个有效的对象。因此,每个一级表实际存放的句柄数为511个。我们当前查看的句柄表中共有句柄1152个,显然一个表是不够的,1152=511*2+130,这里显然需要三个表,且第三个表未放满。而TableCode值为0xe3202001,其低两位也说明了这是个二级表。掩去低两位之后才是二级表的真正地址。来查看一下:
lkd> dd 0xe3202000
e3202000 e2a7b000 e3203000 e2c1e000 00000000
e3202010 00000000 00000000 00000000 00000000
e3202020 00000000 00000000 00000000 00000000
e3202030 00000000 00000000 00000000 00000000
e3202040 00000000 00000000 00000000 00000000
没错的,二级表中放了三个一级表指针。再来查看一下第一个一级表的内容。
lkd> dd e2a7b000
e2a7b000 00000000 fffffffe e100b4e9 000f0003
e2a7b010 e1a9ef41 00000003 898343b3 00100020
e2a7b020 8982ece9 021f0003 e1a794e9 000f000f
e2a7b030 e23a61b9 021f0001 e2390a59 020f003f
明显看到,第一个HANDLE_TABLE_ENTRY的Object为0,从第二个起才是有效的对象(如果某对象所对应的句柄被关闭,那么该对象就会从相应的句柄表中删除,所以并非句柄表中的每个位置存放的都是有效对象)。
接下来看HANDLE_TABLE_ENTRY,e100b4e9 000f0003这一对数据就是HANDLE_TABLE_ENTRY结构中的Object和GrantedAccess了。但是这里的Object并不是直接指向对象的。因为对象体总是8字节对齐,所以地址的低三位总是0,因此被用于标志位,表明该对象所对应的句柄的一些属性,比如可继承等等。因此,要对这里得到的Object掩去低三位,才会得到一个指向对象头OBJECT_HEADER的指针。
e100b4e9这里掩去之后为e100b4e8。
lkd> dt _OBJECT_HEADERe100b4e8
nt!_OBJECT_HEADER
继续来看一下这个OBJECT_TYPE
lkd> dt _OBJECT_TYPE0x8a928e70
nt!_OBJECT_TYPE
是个KeyedEvent对象。对象头再加上它本身的大小0x18之后就到了对象体OJBECT_BODY了,这个因不同对象而异。直接来看一下:
lkd> !object e100b4e8+0x18
Object: e100b500 Type: (8a928e70) KeyedEvent
跟前面看到的对比一下,完全是一致的。这样就了解了如何从进程的句柄表中得到实际的对象。
下面以该进程中的一个句柄0x1078为例说明句柄如何索引到对象。
0x1078/0x800=2
0x1078%0x800=0x78
前面已经知道,该进程的句柄表为二级表,而上面的计算结果表明,该句柄大小已经超出了两个句柄表的范围,在第三个句柄表中,且偏移为0x78*2.(因为句柄按4递增,而HANDLE_TABLE_ENTRY结构大小为8,所以正确公式应该为0x78除以4得到索引值,再乘以8得到以字节计算的偏移值,结果即0x78*2)
而第三张表基址为e2c1e000 。来看一下:
lkd> dd e2c1e000+0x78*2
e2c1e0f0 896b7019 001f03ff 8984e659 00100003
e2c1e100 8984e611 00100003 e2d183d9 00020019
e2c1e110 898003f1 001f0003 896c0701 0012019f
e2c1e120 8984e5c9 00100003 89834691 00100003
可以得到Object为896b7019,掩去低两位,则对象头指针为896b7018,对象体指针为896b7018+0x18
lkd> !object 896b7018+0x18
Object: 896b7030 Type: (8a92c040) Thread
结果显示这是一个线程对象,与ProcessExplorer中显示的结果完全一致。以上过程很容易转化为程序实现并且不依赖任何系统函数,具体实现过程可参考WRK中的ExpLookupHandleTableEntr
PHANDLE_TABLE_ENTRY
ExpLookupHandleTableEntr
下一篇准备再写写PspCidTable~
ps:大部分进程中句柄数并不多,因此通常为一级表。那么如何找到一个有二级表的进程进行观察呢?首先,你可以自己人为地“造”一个出来。或者,找服务数最多的那个svchost.exe进程(用ProcessExplorer可以很容易找出来),它是20多个服务的宿主,句柄不多就怪了~~
-------------------------------------这是分割线---------------------------------------
PspCidTable概述
1.PspCidTable中存放的对象是系统中所有的进线程对象,其索引就是PID和TID。
2.PspCidTable中存放的直接是对象体(EPROCESS和ETHREAD),而每个进程私有的句柄表则存放的是对象头(OBJECT_HEADER)。
3.PspCidTable是一个独立的句柄表,而每个进程私有的句柄表以一个双链连接起来。
注意访问对象时要掩掉低三位,每个进程私有的句柄表是双链连接起来的,实际上ZwQuerySystemInformation
PspCidTable相关的调用:
1.系统初始化时调用PspInitPhase0()初始化进程管理子系统,此时创建PspCidTable。
- PspCidTable = ExCreateHandleTable(NULL);
//创建! -
if(PspCidTable == NULL) { -
returnFALSE; -
} -
// -
//Set PID and TID reuse to strict FIFO. This isn't absolutely neededbut -
//it makes tracking audits easier. -
// -
ExSetHandleTableStrictFI FO (PspCidTable); -
ExRemoveHandleTable(PspCidTable); //使得PspCidTable独立于其它句柄表
2.进程创建时,PspCreateProcess()在PspCidTable中以进程对象创建句柄,是为PID。
//
-
// -
CidEntry.Object = Process; -
CidEntry.GrantedAccess = 0; -
Process->UniqueProcessId = ExCreateHandle(PspCidTable,&CidEntry); //进程的PID是这么来的 -
if(Process->UniqueProcessId == NULL) { -
Status = STATUS_INSUFFICIENT_RESOURCES; -
gotoexit_and_deref; -
}
3.线程创建时,PspCreateThread()在PspCidTable中以线程对象创建句柄,是为TID。
-
Thread->ThreadsProcess = Process; -
Thread->Cid.UniqueProcess =Process->UniqueProcessId; -
CidEntry.Object = Thread; -
CidEntry.GrantedAccess = 0; -
Thread->Cid.UniqueThread = ExCreateHandle(PspCidTable, &CidEntry); //线程的TID -
if(Thread->Cid.UniqueThread == NULL) { -
ObDereferenceObject (Thread); -
return(STATUS_INSUFFICIENT_RESOURCES); -
}
这儿可以清楚地知道:PID和TID分别是EPROCESS和ETHREAD对象在PspCidTable这个句柄表中的索引。
4.进程和线程的查询,主要是以下三个函数,按照给定的PID或TID从PspCidTable从查找相应的进线程对象。
PsLookupProcessThreadByC
PsLookupProcessByProcess
PsLookupThreadByThreadId
其中有如下调用:
CidEntry = ExMapHandleToPointer(PspCidTable, ProcessId);
CidEntry = ExMapHandleToPointer(PspCidTable, ThreadId);
5.线程退出时,PspThreadDelete()在PspCidTable中销毁句柄。
- if (Thread->Cid.UniqueThread != NULL) {
-
if(!ExDestroyHandle (PspCidTable,Thread->Cid.UniqueThread, NULL)) { -
KeBugCheck(CID_HANDLE_DELETION); -
} -
}
6.进程退出时,PspProcessDelete()在PspCidTable中销毁句柄。
- if (Process->UniqueProcessId) {
-
if(!(ExDestroyHandle (PspCidTable,Process->UniqueProcessId, NULL))) { -
KeBugCheck (CID_HANDLE_DELETION); -
} -
}
这里要注意,如果进线程退出时,销毁句柄却发现句柄不存在造成ExDestroyHandle返回失败,可是要蓝屏滴~
所以抹了PspCidTable来隐藏的进程,在退出时必须把进线程对象再放回去,欲知后事如何,请看下回分解~~
-------------------------------------这是分割线---------------------------------------
PspCidTable攻与防
不管基于PspCidTable的进线程检测,还是抹PspCidTable进行进程对象的隐藏,都涉及到对PspCidTable的遍历。
所以如何安全正确地遍历PspCidTable才是该技术的关键。
一、获取PspCidTable的地址
常用的方法是从前面提到的三个查询PspCidTable的函数中特征搜索。
PsLookupProcessThreadByC
PsLookupProcessByProcess
PsLookupThreadByThreadId
比如PsLookupProcessByProcess
- lkd> u
- nt!PsLookupProcessByProcess
Id+0x12: - 8057ce2fff8ed4000000
dec dword ptr [esi+0D4h] - 8057ce35ff35e0955680
push dword ptr [nt!PspCidTable (805695e0)] //就是这儿了 - 8057ce3be89b1dffff
call nt!ExMapHandleToPointer (8056ebdb) - 8057ce408bd8
mov ebx,eax
这个方法没什么好说的,匹配就是了~
另一种方法是从KPCR中取,我比较喜欢这种方法。
lkd> dt _KPCR ffdff000
nt!_KPCR
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
- {
-
moveax,fs:[0x34] -
moveax,[eax+0x80] -
moveax,[eax] -
movPspCidTable,eax - }
- DbgPrint("PspCidTable=0xX\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就拿到对象了,接下来嘛,该干啥干啥~
- BOOLEAN MyEnumerateHandleRoutine
( -
IN PHANDLE_TABLE_ENTRY HandleTableEntry, -
IN HANDLE Handle, -
IN PVOID EnumParameter -
) - {
-
BOOLEANResult=FALSE; -
ULONGProcessObject; -
ULONGObjectType; -
ProcessObject=(HandleTableEntry->Value)&~7;//掩去低三位 -
-
ObjectType=*(ULONG*)(ProcessObject-0x10);//取对象类型 -
if(ObjectType==(ULONG)PsProcessType)//判断是否为Process -
{ -
(*(ULONG*)EnumParameter)++; -
//注意PID其实就是Handle,而不是从EPROCESS中取,可以对付伪pid -
DbgPrint("PID=M\t EPROCESS=0xX%s\n",Handle,ProcessObject,PsGetProcessImageFileNam e((PEPROCESS)ProcessObject)); -
} -
returnResult;//返回FALSE继续 - }
然后这样调用:
ExEnumHandleTable(PspCidTable,MyEnumerateHandleRoutine
好了,打开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;
-
ULONGTableLevel; - PHANDLE_TABLE_ENTRYTableLevel1,*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\n",MaxHandle);
- //判断表的等级
- switch(TableLevel)
- {
- case 0:
-
{ -
//一级表 -
TableLevel1=(PHANDLE_TABLE_ENTRY)CapturedTable; -
DbgPrint("解析一级表0xx...\n",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; -
} -
}//endof for i -
ResultValue=TRUE; -
-
} -
break; - case 1:
-
{ -
//二级表 -
TableLevel2=(PHANDLE_TABLE_ENTRY*)CapturedTable; -
DbgPrint("解析二级表0xx...\n",TableLevel2); -
DbgPrint("二级表的个数:%d\n",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\tObject=0xX\n",Handle,(TableLevel1[i].Value)&~3); -
} -
}//end of for i -
}//endof for j -
ResultValue=TRUE; -
} -
break; - case 2:
-
{ -
//三级表 -
TableLevel3=(PHANDLE_TABLE_ENTRY**)CapturedTable; -
DbgPrint("解析三级表0xx...\n",TableLevel3); -
DbgPrint("三级表的个数:%d\n",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\tObject=0xX\n",Handle,(TableLevel1[i].Value)&~3); -
} -
}//end of for i -
}//end of for j -
}//endof for k -
ResultValue=TRUE; -
} -
break; - default:
-
{ -
DbgPrint("Shoud NOT get here!\n"); -
} -
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()会调用PspCreateThreadNotifyRou
另外也可以来个ObjectHook,Fuck掉PsProcessType->TypeInfo->DeleteProcedure和PsThreadType->TypeInfo->DeleteProcedure然后是隐藏掉的进程就照顾一下~~~
下面是PsProcessType的部分内容:
总之,只要在PspCidTable中找到空位把目标进程的EPROCESS和ETHREAD再放回去,并且其索引与EPROCESS->UniqueProcessId和ETHREAD->Cid.UniqueThread保持一致就可以了。抽点时间写个隐藏进程的Demo练习下~~