遍历PspCidTable表检测隐藏进程
参考:http://www.alonemonkey.com/details-in-pspcidtbale.html
一、PspCidTable概述
PspCidTable也是一个句柄表,其格式与普通的句柄表是完全一样的,但它与每个进程私有的句柄表有以下不同:
1.PspCidTable中存放的对象是系统中所有的进程线程对象,其索引就是PID和TID。
2.PspCidTable中存放的直接是对象体(EPROCESS和ETHREAD),而每个进程私有的句柄表则存放的是对象头(OBJECT_HEADER)。
3.PspCidTable是一个独立的句柄表,而每个进程私有的句柄表以一个双链连接起来。
注意访问对象时要掩掉低三位,每个进程私有的句柄表是双链连接起来的,实际上ZwQuerySystemInformation枚举系统句柄时就是走的这条双链,隐藏进程的话,这条链也是要断掉的~~在遍历进程活动链表(ActiveProcessLinks)、DKOM隐藏进程时,还要把隐藏进程的句柄表从链表中摘去。
二、PspCidTable相关调用
1.系统初始化时调用PspInitPhase0()初始化进程管理子系统,此时初始化进程活动链表和PspCidTable。
初始化进程活动链表:
// Initialize active process list head and mutex
InitializeListHead (&PsActiveProcessHead);
初始化PspCidTable:
// Initialize CID handle table.
// N.B. The CID handle table is removed from the handle table list so
// it will not be enumerated for object handle queries.
PspCidTable = ExCreateHandleTable (NULL);
if (PspCidTable == NULL) {
return FALSE;
}
// Set PID and TID reuse to strict FIFO. This isn’t absolutely needed but
// it makes tracking audits easier.
ExSetHandleTableStrictFIFO (PspCidTable);
ExRemoveHandleTable (PspCidTable); //使得PspCidTable独立于其它句柄表
2.进程创建时,PspCreateProcess()在PspCidTable中以进程对象创建句柄,视为PID。
// Create the process ID
CidEntry.Object = Process;
CidEntry.GrantedAccess = 0;
Process->UniqueProcessId = ExCreateHandle (PspCidTable, &CidEntry); //进程的PID就是这么创建的
if (Process->UniqueProcessId == NULL) {
Status = STATUS_INSUFFICIENT_RESOURCES;
goto exit_and_deref;
}
3.线程创建时,PspCreateThread()在PspCidTable中以线程对象创建句柄,视为TID。
// Assign this thread to the process so that from now on
// we don’t have to dereference in error paths.
Thread->ThreadsProcess = Process;
Thread->Cid.UniqueProcess = Process->UniqueProcessId;
CidEntry.Object = Thread;
CidEntry.GrantedAccess = 0;
Thread->Cid.UniqueThread = ExCreateHandle (PspCidTable, &CidEntry);
if (Thread->Cid.UniqueThread == NULL) {
ObDereferenceObject (Thread);
return (STATUS_INSUFFICIENT_RESOURCES);
}
可以清楚地知道: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中销毁句柄。
if (Thread->Cid.UniqueThread != NULL) {
if (!ExDestroyHandle (PspCidTable, Thread->Cid.UniqueThread, NULL)) {
KeBugCheck(CID_HANDLE_DELETION);
}
}
6.进程退出时,PspProcessDelete()在PspCidTable中销毁句柄。
// Remove the process from the global list
if (Process->ActiveProcessLinks.Flink != NULL) {
CurrentThread = PsGetCurrentThread ();
PspLockProcessList (CurrentThread);
RemoveEntryList (&Process->ActiveProcessLinks);
PspUnlockProcessList (CurrentThread);
}
if (Process->UniqueProcessId) {
if (!(ExDestroyHandle (PspCidTable, Process->UniqueProcessId, NULL))) {
KeBugCheck (CID_HANDLE_DELETION);
}
}
如果进线程退出时,销毁句柄却发现句柄不存在造成ExDestroyHandle返回失败,就会蓝屏!!!
所以抹了PspCidTable来隐藏的进程,在退出时必须把进线程对象再放回去。
三、PspCidTable表及结构分析
在windows下所有的资源都是用对象的方式进行管理的,诸如:文件、进程、设备等都是对象。当要访问一个对象的时候,如打开一个文件,系统就会创建一个对象句柄,通过这个句柄可以对这个文件进行各种操作。句柄和对象的联系是通过句柄表来进行的,准确来说一个句柄就是它所对应的对象在句柄表中的索引。通过句柄,可以在句柄表中找到对象的指针,通过指针对对象进行操作。PspCidTable就是这样的一种表,它不属于任何进程,也不连接在系统的句柄表上,通过它可以反问系统的任何对象。遍历PspCidTable句柄表首先要获取PspCidTable表的内存地址。由于进程活动是动态的,一个进程包括多个线程,windows7操作系统的PspCidTable句柄表采用多层表及动态扩展的方法保存进程和线程对象的指针。因此需要分析PspCidTable句柄表的层次结构,写出遍历PspCidTable表检测隐藏进程的算法,最后变成实现自动隐藏进程。
1.获取PspCidTable的地址
上面我们说到PsLookupProcessByProcessId这个函数调用了PspCidTable,所以我们来反汇编一下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | ULONG GetPspCidTable() { UNICODE_STRING uniPsLookup; ULONG psLookbyidAddr; ULONG pspCidTableAddr; PUCHAR cPtr; RtlInitUnicodeString(&uniPsLookup, L"PsLookupProcessByProcessId"); psLookbyidAddr = (ULONG)MmGetSystemRoutineAddress(&uniPsLookup); for (;;psLookbyidAddr++) { //xp => 0x35ff if((0x3d8b == (*(PUSHORT)psLookbyidAddr)) && (0xe8 == (*(PUCHAR)(psLookbyidAddr+6)))) { pspCidTableAddr = *(PULONG)(psLookbyidAddr + 2); break; } } return pspCidTableAddr; } |
2.PspCidTable的层次结构
PspCidTable是一个HANDLE_TALBE结构,当新建一个进程时,对应的会在PspCidTable存在一个该进程和线程对应的HANDLE_TABLE_ENTRY项。在windows7中采用动态扩展的方法,当句柄数少的时候就采用下层表,多的时候才启用中层表或上层表。
句柄表分为三层,下层表是一个HANDLE_TABLE_ENTRY项的索引,整个表共有256个元素,每个元素是一个8个字节长的HANDLE_TABLE_ENTRY项及索引,HANDLE_TABLE_ENTRY项中保存着指向对象的指针,下层表可以看成是进程和线程的稠密索引。中层表共有256个元素,每个元素是4个字节长的指向下层表的入口指针及索引,中层表可以看成是进程和线程的稀疏索引。上层表共有256个元素,每个元素是4个字节长的指向中层表的入口指针及索引,上层表可以看成是中层表的稀疏索引。一个句柄表有一个上层表,一个上层表最多可以有256个中层表的入口指针,每个中层表最多可以有256个下层表的入口指针,每个下层表最多可以有256个进程和线程对象的指针。PspCidTable表可以看成是HANDLE_TBALE_ENTRY项的多级索引。
3.确定PspCidTable的层次
上面我们从windbg中看到PspCidTable的地址为8298aeb4。我们来看看这个地址的内容:
上面87a011d8是HANDLE_TBALE的地址,我们来看看HANDLE_TBALE的结构:
这里的TbaleCode记录这句柄表的地址,如果后两位是00则采用的是一层表,01是两层表,10是三层表。通过上面的TableCode就可以判断这个系统采用了两层表的结构。
如果系统采用了两层或者三层的时候,TableCode就不是句柄表的地址了,把这个值后两位置为0之后,则是一个指向多层表的指针。
来看看这个指针指向的值:
kd> dd 0x92500000
92500000 87a04000 92501000 00000000 00000000
根据上面说的,那么这个87a04000 指向的肯定是一个HANDLE_TBALE_ENTRY的数组。
_HANDLE_TABLE_ENTRY是两个32位的结构体。
一个指向对象的指针 一个是32位的标志
一定要记得,低3位值不是我们需要的,应该置0 。即:value & 0xFFFFFFF8。
kd> !Object 841ce020
Object: 841ce020 Type: (84133718) Process
ObjectHeader: 841ce008 (new version)
HandleCount: 3 PointerCount: 128
而且这个对象就是pid为4的system.exe进程。
我们已经找到了句柄值为0004的内核对象。
OK,冒出了句柄值,那么句柄值是如何被关联起来呢?想下0004,它对应_HANDLE_TABLE_ENTRY表项的低几个啊?
句柄值为0x0000代表是NULL
刚好_HANDLE_TABLE_ENTRY的第0个表项为无效值
句柄值为0x0004有效
刚好指的是_HANDLE_TABLE_ENTRY的第1个表项。
那句柄值为0x0008了?
原来句柄值总是4的倍数。值/4就代表句柄表项数组_HANDLE_TABLE_ENTRY的索引啊
这时,句柄值的低两位永远是0啦,为啥呢?是4的倍数,第2为不就为0?自己算算
0x00,0x04,0x08,0x10,0x14等等的二进制
既然第2位永远为0,那么微软就利用了这两位做一个标志位,用来指示当前句柄值所代表的内核对象到那个表项数组中找到。(表示层次结构)
四、遍历PspCidTable表枚举进程
1.首先找到PspCidTable的地址。
2.然后找到HANDLE_TBALE的地址。
3.根据TableCode来判断层次结构。
4.遍历层次结构来获取对象地址。
5.判断对象类型是否为进程对象。
6.判断进程是否有效。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | VOID RefreshProcessByPspCidTable() { ULONG PspCidTable = 0; ULONG HandleTable = 0; ULONG TableCode = 0; ULONG flag = 0; PEPROCESS pCsrssEprocess = NULL; LookupProcessByName("CSRSS.EXE\0", &pCsrssEprocess); //这个是通过获取PspCidTable的地址来的。 PspCidTable = GetPspCidTable(); HandleTable = *(PULONG)PspCidTable; //这个是通过直接从csrss.exe进程获取HandleTable //HandleTable = *(PULONG)((ULONG)pCsrssEprocess + ObjectTable); TableCode = *(PULONG)HandleTable; flag = TableCode & 3; //最后两位 TableCode &= 0xfffffffc; switch (flag) { case 0: //一层表 BrowerTableL3(TableCode); break; case 1: //二层表 BrowerTableL2(TableCode); break; case 2: //三层表 BrowerTableL1(TableCode); break; } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 | ULONG BrowerTableL3(ULONG TableAddr) { ULONG Object = 0; ULONG ItemCount = 511; DWORD dwProcessId = 0; ULONG flags; do{ TableAddr += 8; Object = *(PULONG)TableAddr; Object = Object & 0xfffffff8; //Object最后三位置0,Object即为EPROCESS的地址 if(Object == 0) { continue; } if(!MmIsAddressValid((PVOID)Object)) { continue; } if(GetProcessType() == *(PULONG)ObGetObjectType((PVOID)Object)) { dwProcessId = *(PULONG)(Object + PIDOFFSET); if(dwProcessId < 65536 && dwProcessId != 0) { flags = *(PULONG)(Object + 0x270); //flags显示进程没有退出 if((flags & 0xc) != 0xc) { DbgPrint("ProcessId: %d\r\n",dwProcessId); DbgPrint("ProcessName: %s\r\n", PsGetProcessImageFileName((PEPROCESS)Object)); /*if(dwProcessId == 960) { InterlockedExchangePointer(&((PHANDLE_TABLE_ENTRY)TableAddr)->Object, NULL); }*/ //进程退出时会调用ExDestroyHandle()销毁句柄,若找不到就会蓝屏,所以要小心在进程退出的时候恢复 } } } }while (--ItemCount > 0); return 0; } ULONG BrowerTableL2(ULONG TableAddr) { do{ BrowerTableL3(*(PULONG)TableAddr); TableAddr += 4; }while((*(PULONG)TableAddr) != 0); return 0; } ULONG BrowerTableL1(ULONG TableAddr) { do{ BrowerTableL2(*(PULONG)TableAddr); TableAddr += 4; }while((*(PULONG)TableAddr) != 0); return 0; } |
扩展:EPROCESS里的ObjectTable保存的是该进程对象的地址。PspCidTable保存所有句柄的系统对象。