http://blog.csdn.net/yeook/article/details/6705225
基于pspCidTable的进程检测技术
文章作者:gz1x
信息来源:邪恶八进制信息安全团队(www.eviloctal.com)
一. pspCidTable概念及内核调试
二. 获取pspCidTable的方法
三. 几种进程检测方法的对比
四. anti-pspCidTable技术及其他
一. pspCidTable概念及内核调试
-----------------------------------------------------
pspCidTable是内核未导出的HANDLE_TALBE结构,它保存着所有进程和线程对象的指针。
只要能遍历这个PspCidTable句柄表,就可以遍历到系统的所有进程,包括所有隐藏进程,除非你抹掉pspCidTable...
结构如下:
typedef struct _HANDLE_TABLE
{
ULONG Flags;
LONG HandleCount;
PHANDLE_TABLE_ENTRY **Table;
PEPROCESS QuotaProcess;
HANDLE UniqueProcessId;
LONG FirstFreeTableEntry;
LONG NextIndexNeedingPool;
ERESOURCE HandleTableLock;
LIST_ENTRY HandleTableList;
KEVENT HandleContentionEvent;
} HANDLE_TABLE , *PHANDLE_TABLE ;
我们利用调试器先从内核里找到这张表:
kd> version
Windows XP Kernel Version 2600 (Service Pack 2) UP Free x86 compatible
Built by: 2600.xpsp_sp2_rtm.040803-2158
Kernel base = 0x804d7000 PsLoadedModuleList = 0x8055ab20
//...
dbgeng: image 6.7.0005.1, built Wed Jun 20 11:50:35 2007
//...
kd> dd pspCidTAble
80560ce0 e10018c8 00000002 00000000 00000000
80560cf0 00000000 00000000 00000000 00000000
80560d00 00000000 00000000 00000000 00000000
80560d10 00000000 00000000 00000000 00000000
kd> dt _HANDLE_TABLE e10018c8
nt!_HANDLE_TABLE
+0x000 TableCode : 0xe1003000
+0x004 QuotaProcess : (null)
+0x008 UniqueProcessId : (null)
+0x00c HandleTableLock : [4] _EX_PUSH_LOCK
+0x01c HandleTableList : _LIST_ENTRY [ 0xe10018e4 - 0xe10018e4 ]
+0x024 HandleContentionEvent : _EX_PUSH_LOCK
+0x028 DebugInfo : (null)
+0x02c ExtraInfoPages : 0
+0x030 FirstFree : 0x770
+0x034 LastFree : 0x764
+0x038 NextHandleNeedingPool : 0x800
+0x03c HandleCount : 266
+0x040 Flags : 1
+0x040 StrictFIFO : 0y1
注意NextHandleNeedingPool : 0x800,句柄表依靠NextHandleNeedingPool来计数的,
凡是大于0x800(2048)的部分就放入第二张表中.所以当CID(PID)号大于0x800时,
就需要查找第二张表了.例如要定位CID等于0x844的句柄表位置,
0xe1003800 + ( 0x844 - 0x800 ) * 2 这个偏移就是CID为0x844的句柄项了.
kd> dd e1003000
e1003000 00000000 fffffffe 81fc9a01 00000000
e1003010 81fc9789 00000000 81fc9341 00000000
e1003020 81fc8021 00000000 81fc8da9 00000000
e1003030 81fc8b31 00000000 81fc88b9 00000000
e1003040 81fc8641 00000000 81fc83c9 00000000
e1003050 81fc7021 00000000 81fc7da9 00000000
e1003060 81fc7b31 00000000 81fc78b9 00000000
e1003070 81fc7641 00000000 81fc73c9 00000000
kd> !object 81fc9a01
Object: 81fc9a01 Type: (0081fc90)
ObjectHeader: 81fc99e9 (old version)
HandleCount: 1073741824 PointerCount: 33554432
kd> !object 81fc9a00
Object: 81fc9a00 Type: (81fc9040) Process
ObjectHeader: 81fc99e8 (old version)
HandleCount: 2 PointerCount: 56
81fc9a00的进程是System...以后的都是其线程。
kd> !object 81fc73c9
Object: 81fc73c9 Type: (0081fc9e)
ObjectHeader: 81fc73b1 (old version)
HandleCount: 1879048192 PointerCount: 0
kd> !object 81fc73c8
Object: 81fc73c8 Type: (81fc9e70) Thread
ObjectHeader: 81fc73b0 (old version)
HandleCount: 0 PointerCount: 1
kd> !thread 81fc73c8
THREAD 81fc73c8 Cid 0004.003c Teb: 00000000 Win32Thread: 00000000 WAIT: (WrQueue) UserMode Non-Alertable
80561b7c Unknown
Not impersonating
DeviceMap e10001e8
Owning Process 81fc9a00 Image: System
Wait Start TickCount 16940 Ticks: 20342 (0:00:03:23.712)
Context Switch Count 418
UserTime 00:00:00.000
KernelTime 00:00:03.124
//....
我们可以根据PID查找进程对象.HANDLE_TABLE的Entry地址+PID*2 。
在此之前,你可以用:
kd>!process 0 0
枚举所有的进程,然后再根据PID来查找验证。
我们以calc.exe PID738为例:
kd> dd e1003000+738*2
e1003e70 81f15da1 00000000 81ef26e1 00000000
e1003e80 81d1f021 00000000 00000000 00000124
e1003e90 00000000 00000744 00000000 00000150
e1003ea0 00000000 0000075c 00000000 00000750
e1003eb0 00000000 00000760 00000000 00000758
e1003ec0 00000000 00000764 00000000 00000768
e1003ed0 00000000 0000076c 00000000 00000774
e1003ee0 00000000 0000077c 00000000 00000770
kd> !object 81f15da1
Object: 81f15da1 Type: (0081fc90)
ObjectHeader: 81f15d89 (old version)
HandleCount: 1073741824 PointerCount: 33554432
kd> !object 81f15da0
Object: 81f15da0 Type: (81fc9040) Process
ObjectHeader: 81f15d88 (old version)
HandleCount: 2 PointerCount: 15
kd> !process 81f15da0
PROCESS 81f15da0 SessionId: 0 Cid: 0738 Peb: 7ffd5000 ParentCid: 03ec
DirBase: 0ddf4000 ObjectTable: e1a222f8 HandleCount: 26.
Image: calc.exe
//...
二. 获取pspCidTable的方法
-----------------------------------------------------
罗嗦几句,在得到pspCidTable后,我们可以有很多方法来实现枚举。
比如:
1.利用未导出的ExEnumHandleTable函数 ;
2.直接自己获取PHANDLE_TABLE_ENTRY等,然后PsLookupProcessByProcessId ;
回归正题:
1.在PsLookupProcessByProcessId函数中搜索特征串定位 PspCidTalbe.
这个方法也是本人用的方法,代码如下:
void GetPspCidTable()
{
PUCHAR cPtr;
unsigned char * pOpcode;
ULONG Length;
for (cPtr = (PUCHAR)PsLookupProcessByProcessId;
cPtr < (PUCHAR)PsLookupProcessByProcessId + PAGE_SIZE;
cPtr += Length)
{
Length = SizeOfCode(cPtr, &pOpcode); //credit to LDasm.c by Ms-Rem
if (!Length) break;
if (*(PUSHORT)cPtr == 0x35FF && *(pOpcode + 6) == 0xE8)
{
pPspCidTable = **(PVOID **)(pOpcode + 2);
break;
}
}
}
其中,全局变量:
PHANDLE_TABLE pPspCidTable = NULL;
当然,PsLookupProcessThreadByCid里也是一样的。
2.利用KDDEBUGGER_DATA32结构得到pspCidTable.
流程是这样的: KdEnableDebugger->KdInitSystem->KdDebuggerDataBlock->KDDEBUGGER_DATA32
->PspCidTable
当然也是内存字节搜索,比如006A006Ah,05C6006ah,0e801h,4742444bh,42444b68h等等。
typedef struct _KDDEBUGGER_DATA32 {
DBGKD_DEBUG_DATA_HEADER32 Header;
ULONG KernBase;
ULONG BreakpointWithStatus; // address of breakpoint
ULONG SavedContext;
USHORT ThCallbackStack; // offset in thread data
USHORT NextCallback; // saved pointer to next callback frame
USHORT FramePointer; // saved frame pointer
USHORT PaeEnabled:1;
ULONG KiCallUserMode; // kernel routine
ULONG KeUserCallbackDispatcher; // address in ntdll
ULONG PsLoadedModuleList;
ULONG PsActiveProcessHead;
ULONG PspCidTable; // <--------- What we need!!
//...
ULONG MmLoadedUserImageList;
} KDDEBUGGER_DATA32, *PKDDEBUGGER_DATA32;
三. 几种进程检测方法的对比
-----------------------------------------------------
1.既然要检测隐藏进程,那就先写个简单的隐藏吧。
typedef struct ServiceDescriptorEntry {
unsigned int *ServiceTableBase;
unsigned int *ServiceCounterTableBase; //Used only in checked build
unsigned int NumberOfServices;
unsigned char *ParamTableBase;
} ServiceDescriptorTableEntry_t, *PServiceDescriptorTableEntry_t;
__declspec(dllimport) ServiceDescriptorTableEntry_t KeServiceDescriptorTable;
嗯,ssdt,hook掉ZwQuerySystemInformation。网上代码多如牛毛,就不多浪费时间现眼了。
if(0 == memcmp(SystemProcesses->ProcessName.Buffer, L"services.exe", 24))
把services.exe藏起来吧...
2.枚举进程,用户态下最多的(包括taskmgr)就是ZwQuerySystemInformation了。
真是哪壶不开提哪壶啊,哈哈...
ZwQuerySystemInformation(SystemProcessesAndThreadsInformation, pBuffer,
cbBuffer, NULL);
PSYSTEM_PROCESS_INFORMATION pInfo =
(PSYSTEM_PROCESS_INFORMATION)pBuffer;
for (;;)
{
printf("ProcessID: %d (%ls)/n", pInfo->ProcessId,
pInfo->ProcessName.Buffer);
if (pInfo->NextEntryDelta == 0)
break;
pInfo = (PSYSTEM_PROCESS_INFORMATION)(((PUCHAR)pInfo)
+ pInfo->NextEntryDelta);
}
分析结果:
================================================
ProcessID: 0 ((null))
ProcessID: 4 (System)
ProcessID: 312 (smss.exe)
ProcessID: 404 (csrss.exe)
ProcessID: 428 (winlogon.exe)
ProcessID: 492 (lsass.exe)
ProcessID: 636 (svchost.exe)
ProcessID: 704 (svchost.exe)
ProcessID: 740 (svchost.exe)
ProcessID: 780 (svchost.exe)
ProcessID: 1004 (explorer.exe)
ProcessID: 1024 (vmsrvc.exe)
ProcessID: 1152 (vpcmap.exe)
ProcessID: 1320 (vmusrvc.exe)
ProcessID: 1512 (svchost.exe)
ProcessID: 1816 (InstDrv.exe)
ProcessID: 1836 (notepad.exe)
ProcessID: 1848 (calc.exe)
ProcessID: 200 (dv.exe)
ProcessID: 344 (InstDrv.exe)
ProcessID: 356 (enumprocsnt.exe)
==================================================
没有services.exe...
3.该我们的 get PspCidTable to ExEnumHandleTable 出马了。
为了方便,代码做了很多简略处理,EnumHandleCallback也没发挥应有的作用,实际的代码中你可以充分利用ExEnumHandleTable,
当然自己遍历表处理结构也是可以的,而且还更好。
我写的代码只为演示PspCidTable,walk list就不繁琐的贴上来了。凑合着看吧 :-)
ULONG GetFunctionAddr(IN PCWSTR FunctionName)
{
UNICODE_STRING UniCodeFunctionName;
RtlInitUnicodeString( &UniCodeFunctionName, FunctionName );
return (ULONG)MmGetSystemRoutineAddress( &UniCodeFunctionName );
}
BOOLEAN EnumHandleCallback(PHANDLE_TABLE_ENTRY HandleTableEntry,HANDLE Handle,
PVOID EnumParameter)
{
NTSTATUS ntStatus;
HANDLE Cid;
PEPROCESS Process;
PETHREAD Thread;
if(EnumParameter== HandleTableEntry)
{
return TRUE;
}
else
{ // we ignore the in handle param,use i,j to walk the list
for(uWalkTableCount=0;uWalkTableCount<0x100;uWalkTableCount++)
{
if(HandleTableEntry->Object)
{
Cid=(HANDLE)((1024*uWalkTablePage)+(uWalkTableCount<<2));
//you can take Handle,too;
if (Cid> (PVOID)4)
{//you can walk the list yourself completely...I'm slothful :-)
ntStatus = PsLookupProcessByProcessId( Cid, &Process );
if(NT_SUCCESS(ntStatus))
{
DbgPrint("PID:%4d/tNAME:/t%-16s/n",
Cid, ((PUCHAR)Process+EPROC_NAME_OFFSET) );
ObDereferenceObject( Process );
}
}
else
{
if (Cid== 0)
{
DbgPrint("PID:%4d/tNAME:/tIdle/n",0); //简化
}
else
{
DbgPrint("PID:%4d/tNAME:/tSystem/n",4); //简化,自己EPROCESS吧
}
}
}
}
uWalkTablePage++;
return FALSE;
}
}
NTSTATUS
DriverEntry( IN PDRIVER_OBJECT DriverObject,
IN PUNICODE_STRING RegistryPath )
{
HANDLE h;
DbgPrint("DriverEntry Loading.../n");
DriverObject->DriverUnload = DriverUnload;
ExEnumHandleTable = (EXENUMHANDLETABLE)GetFunctionAddr(L"ExEnumHandleTable");
if ( ExEnumHandleTable == NULL )
{
DbgPrint("Get ExEnumHandleTable Addr Error!!");
return STATUS_DEVICE_CONFIGURATION_ERROR;
}
DbgPrint("Address of ExEnumHandleTable:%x/n",ExEnumHandleTable);
GetPspCidTable();
DbgPrint("CidTable:%x/n",pPspCidTable);
if (!ExEnumHandleTable(pPspCidTable, EnumHandleCallback, NULL, &h ))
{
DbgPrint( "HandleTable Found: %lx./n", h );
}
else
{
DbgPrint( "HandleTable not found./n");
}
return STATUS_SUCCESS;
}
分析结果:
==================================================
Address of ExEnumHandleTable:805a4274
CidTable:e10018c8
PID: 0 NAME: Idle
PID: 4 NAME: System
PID: 200 NAME: dv.exe
PID: 312 NAME: smss.exe
PID: 344 NAME: InstDrv.exe
PID: 356 NAME: enumprocsnt.exe
PID: 404 NAME: csrss.exe
PID: 428 NAME: winlogon.exe
PID: 480 NAME: services.exe
PID: 492 NAME: lsass.exe
PID: 636 NAME: svchost.exe
PID: 704 NAME: svchost.exe
PID: 740 NAME: svchost.exe
PID: 780 NAME: svchost.exe
PID:1004 NAME: explorer.exe
PID:1024 NAME: vmsrvc.exe
PID:1152 NAME: vpcmap.exe
PID:1320 NAME: vmusrvc.exe
PID:1512 NAME: svchost.exe
PID:1816 NAME: InstDrv.exe
PID:1836 NAME: notepad.exe
PID:1848 NAME: calc.exe
Handle Found: bb40.
=====================================================
看起来蛮强大,连死进程也能读出来... :-)
4.ring3下的间接pspCidTable,看代码:
void main()
{
for (int i=0;i<=65535;i+=4) //实际上,取到0x4e1c就够了
{
if(OpenProcess(PROCESS_QUERY_INFORMATION,FALSE,i)!= 0)
{
printf("ProcessID: %d/n",i);
CloseHandle(&i);
}
}
getchar();
}
很猥琐的代码,哈哈...
想得到更多信息,自己#include "psapi.h",把lib库link上去,然后:
EnumProcessModules,GetModuleFileNameEx,GetProcessImageFileName等等吧...
至于想比较出哪些是隐藏进程或者恶意进程,EnumProcesses或者CreateToolhelp32Snapshot,
得到用户态下的一张表对比就知道了。
分析结果:
=============================================
ProcessID: 4
ProcessID: 200
ProcessID: 312
ProcessID: 344
ProcessID: 356
ProcessID: 428
ProcessID: 480
ProcessID: 492
ProcessID: 636
ProcessID: 740
ProcessID: 852
ProcessID: 1004
ProcessID: 1024
ProcessID: 1152
ProcessID: 1320
ProcessID: 1836
ProcessID: 1848
==============================================
ProcessID: 480,嗯,是services.exe...
其实原理很简单,OpenProcess->NtOpenProcess->PsLookupProcessByProcessId,
反汇编PsLookupProcessByProcessId,有这样一段代码:
mov eax, large fs:124h
push [ebp+ProcessId]
mov esi, eax
dec dword ptr [esi+0D4h]
push PspCidTable
call ExMapHandleToPointer
还原出来是这样的:
PHANDLE_TABLE_ENTRY CidEntry;
CidEntry = ExMapHandleToPointer(PspCidTable, ProcessId);
像PsLookupProcessThreadByCid,PspExitThread,PspCreateThread等也有类似的代码,
但是由于入函数太深,查找不方便。
-----------------------------------
综合起来看,pspCidTable枚举进程还是很成功的,一般的进程隐藏技术都过不了它。 :-)
四. anti-pspCidTable技术及其他
-----------------------------------------------------
写这篇的原因仅仅是先前有人在maillist上问起,这个技术并不神秘,想anti它很简单,抹掉pspCidTable就行了,
扫描内存得到pspCidTable,然后对应EPROCESS进行分析,找到要隐藏的进程,将相关信息置成0,安放一个进程notify routine。
FUTo的代码一直放着还没看,也不知道是不是其他的思想,有空扫一眼。
文章没提到其他检测技术,比如用activelist,handletable,handletablelisthead获得进程表,
挂钩KiSwapContext,KiService,CreateProcessNotifyRoutine等等...
当然,相应地可以摘除进程控制块中的ActiveProcessLinks里的自己,或者摘除csrss.exe进程句柄表,
再或者挂钩SwapContext,自己实现线程调度...
隐藏进程的方法多种多样,也有很多高深的技术,未公开的技术,谁叫RK和ARK技术总是相生相克的呢...
:-)