搜索内存检测进程的原理
1) 问题的提出——进程检测
Rootkit等后门为了在系统中长期驻留,需要隐藏相应的信息。这些信息包括自启动项、文件、进程、模块、端口、注册表、服务等。其中,以进程隐藏特别突出(因为用户经常会打开任务管理器看看是不是有异常的进程在自己的机器上运行)。
隐藏进程的方法有多种,例如挂钩NtQuerySystemInformation函数;从内核EPROCESS结构的ActiveProcessLinks等双向链表上摘除自身;从csrss.exe进程的句柄表上摘除自身;从PspCidTable上摘除自身等等。如果Rootkit用到了其中的某种方法,那么基于此的检测将会失败。比如,hxdef100(黑客守护者)挂钩NtQuerySystemInformation实现隐藏,如果想通过NtQuerySystemInformation函数来获取全部进程信息就会失败。再比如,FU_rootkit通过从内核EPROCESS结构的ActiveProcessLinks双向链表上摘除自身实现隐藏,如果想通过遍历该链表来检测隐藏的FU_rootkit就会失败。现在问题来了,如果上面隐藏进程的方法都用到了,该怎么检测呢?
2)解决的思路之一——搜索内存
在Windows系统中,进程由内存空间、进程打开的各种对象和进程中运行的线程所组成。线程仅仅是一个执行上下文,系统调度的最基本单位,但每一个线程的运行都必须依附(attach)一个进程。
在Windbg中可以使用dt命令来查看线程ETHREAD的结构,命令格式为“dt _ETHREAD 线程地址”。例如:
kd> dt _ETHREAD 8238F3B8
nt!_ETHREAD
+0x000 Tcb : _KTHREAD
+0x1c0 CreateTime : _LARGE_INTEGER 0xe4d5cd5`36b71830
……
+0x21c DeviceToVerify : (null)
+0x220 ThreadsProcess : 0x8234f718 _EPROCESS
+0x224 StartAddress : 0x7c810867
+0x228 Win32StartAddress : 0x010027f2
……
+0x254 ForwardClusterOnly : 0 ''
+0x255 DisablePageFaultClustering : 0 ''
在线程控制块的偏移0x220处是一个ThreadsProcess指针,指向一个EPROCESS结构,即一个进程EPROCESS。
在Windbg中可以使用“!process”命令来查看进程的线程摘要,命令格式为“!process 进程地址
3”。我们看看上面显示的地址0x8234f718对应进程的线程摘要信息。
kd> !process 0x8234f718 3
PROCESS 8234f718 SessionId: 0 Cid: 05bc Peb: 7ffd6000 ParentCid: 0424
DirBase: 07a00280 ObjectTable: e1975848 HandleCount: 38.
Image: wscntfy.exe
……
BasePriority 8
CommitCharge 164
THREAD 8238f3b8 Cid 05bc.05c4 Teb: 7ffdf000 Win32Thread: e1d88da8 WAIT:
(UserRequest) UserMode Non-Alertable
8258cef8 SynchronizationEvent
8259b7b8 SynchronizationEvent
从命令输出可以看出该进程名为wscntfy.exe,有一个线程地址为8238f3b8,这个地址就是最开始的那个线程控制块的地址。
现在思路有了,既然每一个线程都要依附一个进程,每一个线程控制块中有一个指向该进程的进程控制块的指针,那么,我们就可以通过搜索系统中所有线程得到所有进程。
搜索内存检测进程的具体实现
1) 问题
问题1,如何判断一个内存块是线程的ETHREAD结构?
Windows系统中存在两个系统描述符表,一个是KeServiceDescriptorTable,一个是KeServiceDescriptorTableShadow。两个系统描述符表中的KeServiceDescriptorTable描述系统基本服务,我们称之为主表;KeServiceDescriptorTableShadow除了描述系统基本服务之外,还描述GUI和USER服务,我们称之为副表。每个线程都有自己指向系统服务描述符表的指针,这个指针指向两个系统描述符表中的任意一个。线程ETHREAD偏移为0的位置是一个KTHREAD结构,这个结构的偏移0x0e0处就是这样一个指向系统描述符表的32位指针ServiceTable。
kd> dt _KTHREAD
nt!_KTHREAD
+0x000 Header : _DISPATCHER_HEADER
+0x010 MutantListHead : _LIST_ENTRY
……
+0x0df InitialNode : UChar
+0x0e0 ServiceTable : Ptr32 Void
+0x0e4 Queue : Ptr32 _KQUEUE
……
+0x1bb DisableBoost : Uchar
假定我们读取地址Addr的信息,如果Addr偏移0x0e0处的内容是系统服务调度表的地址,就可以初步判定这个Addr很有可能就是一个线程ETHREAD的入口。
问题2,如何获得系统服务调度表的地址?
任何一个系统的系统服务描述符表在系统运行时是不会改变的,而且从前面的分析可以看出,这个表的位置线程是知道的,这样线程在运行时才能成功找到自己要调用的服务。
可是不同版本的Windows系统的描述符表地址并不一样,同一版本的各个系统上描述符表的地址也不一样,因此最好能够有一种动态获得系统描述符表地址的方法。
基于前面对于主表和副表的分析,找到一个需要系统基本服务和GUI/USER服务的进程,获取其所有线程中的ServiceTable内容,就可以得到系统描述符表的地址。而通过这种方法得到的地址是在程序运行过程中动态获取的,可以满足我们的需求。
我们通过遍历EPROCESS中偏移0x088
处的ActiveProcessLinks来找到拟采用的进程,比如explorer.exe(偏移0x174处为ImageFileName,映像名称)。
kd> dt _EPROCESS 81405560
nt!_EPROCESS
+0x000 Pcb : _KPROCESS
……
+0x084 UniqueProcessId : 0x000005fc
+0x088 ActiveProcessLinks : _LIST_ENTRY [ 0x815c0c98 - 0x815a6b00 ]
……
+0x170 Session : 0xf9ebc000
+0x174 ImageFileName : [16] "explorer.exe"
+0x184 JobLinks : _LIST_ENTRY [ 0x0 - 0x0 ]
+0x18c LockedPagesList : (null)
+0x190 ThreadListHead : _LIST_ENTRY [ 0x81405424 - 0x813adadc ]
+0x198 SecurityPort : (null)
……
+0x255 WorkingSetAcquiredUnsafe : 0 ''
+0x258 Cookie : 0x70f74bdb
在找到这个进程之后,我们接着要遍历它所有的线程,以获取我们需要的系统服务描述符表的地址。EPROCESS结构偏移0x190
处的ThreadListHead是一个类型为_LIST_ENTRY的双向链表,通过它可以得到线程链表的头。
kd> dt _ETHREAD
nt!_ETHREAD
+0x000 Tcb : _KTHREAD
……
+0x220 ThreadsProcess : Ptr32 _EPROCESS
+0x224 StartAddress : Ptr32 Void
+0x228 Win32StartAddress : Ptr32 Void
+0x228 LpcReceivedMessageId : Uint4B
+0x22c ThreadListEntry : _LIST_ENTRY
+0x234 RundownProtect : _EX_RUNDOWN_REF
……
+0x254 ForwardClusterOnly : UChar
+0x255 DisablePageFaultClustering : UChar
而线程ETHREAD结构的偏移0x22c处也是这样一个双向链表,通过它可以遍历所有线程。问题好像已经解决了,地址“EPROCESS+0x190-0x22c+0x0e0”中的内容即是ServiceTable,亦即系统描述符表的地址,其中0x190是ThreadListHead相对EPROCESS的偏移,0x22c是ThreadListEntry相对ETHREAD的偏移,0x0e0是ServiceTable相对ETHREAD的偏移。
图1给出了系统中EPROCESS和ETHREAD结构的关系,我们可以从这个图中看出上面所描述的过程(注:ServiceTable在Tcb结构中,为了使结构清晰明了,图1没有给出)。
图1 EPROCESS结构与ETHREAD结构的关系
问题3,如何获取系统中所有进程的EPROCESS结构?
通过图1我们可以看出,在内存中搜索线程之后,通过EPROCESS与ETHREAD的关系,可以得到线程依附的进程。“ETHREAD+
ThreadsProcess”中即是EPROCESS的地址,其中“ETHREAD”为线程ETHREAD结构的地址,“ThreadsProcess”为ThreadsProcess指针相对ETHREAD的偏移量。获得EPROCESS地址,便对应上了进程。一个线程会依附一个进程,而一个进程则可以对应多个线程。用这个方法会得到很多重复的进程地址,我们只需要把重复部分删除即可。
2)实现
获取系统服务描述符表的部分代码如下,通过遍历,能够获得两个描述符表的地址。
currlist=currlist->Flink;
ssdt=*(PULONG)((ULONG)currlist-ThreadListEntry+ServiceTable);
if(s[0]==0)
s[0]=ssdt;
else
{
if((s[1]==0)&&(s[0]!=ssdt))
s[1]=ssdt;
}
if((s[0]!=0)&&(s[1]!=0))
break;
count++;
采用链表的形式记录所有进程EPROCESS结构地址的部分代码如下,需要在内核中申请非分页的地址空间。
{
if((r=(process *)ExAllocatePool(NonPagedPool,sizeof(process)))==NULL)
{
return;
}
p->next=r;
r->addr=EPROCaddr;
r->next=NULL;
p=r;
}
具体细节的补充说明
我所给出的基本结构和原理是Windows NT架构下所有系统都采用的,因而在其他系统下这个方法依然可以实现进程检测。但由于在Winodws
2000/XP/2003下相关数据结构有一定的差别,所以在其他系统下需要对相应系统数据偏移量等值进行修改。我所给出的驱动程序在Windows XP
SP2下由Windows DDK编译,可以使用rootkit.com提供的InstDrv进行加载,使用Dbgview查看运行结果,在Windows XP
SP2下测试通过,可以成功检测FU_rootkit、hxdef100(黑客守护者)和FUTo_enhanced隐藏的进程。
目前给出的方法具有一定的不准确性。比如内核空间地址范围,线程、进程的判定,系统服务描述符表的获取等都具有一定的局限性,这些都会影响检测的准确性。虽然目前的测试结果都是准确的,但是一种从方法上能够确保准确的思路是以后努力的方向。