基于线程调度链表的检测和隐藏技术

来源:百度文库 编辑:神马文学网 时间:2024/04/19 10:49:31

基于线程调度链表的检测和隐藏技术

1.       什么是ETHREAD和KTHREAD块

Windows2000是由执行程序线程(ETHREAD)块表示的,ETHREAD成员都是指向的系统空

间,进程环境块(TEB)除外。ETHREAD块中的第一个结构体就是内核线程(KTHREAD)块。在KTHREAD块中包含了windows2000内核需要访问的信息。这些信息用于执行线程的调度和同步正在运行的线程。

kd> !kthread

struct   _KTHREAD (sizeof=432)

+000 struct   _DISPATCHER_HEADER Header

+010 struct   _LIST_ENTRY MutantListHead

+018 void     *InitialStack

+01c void     *StackLimit

+020 void     *Teb

+024 void     *TlsArray

+028 void     *KernelStack

+02c byte     DebugActive

+02d byte     State

+02e byte     Alerted[2]

+030 byte     Iopl

+031 byte     NpxState

+032 char     Saturation

+033 char     Priority

+034 struct   _KAPC_STATE ApcState

+034    struct   _LIST_ENTRY ApcListHead[2]

+044    struct   _KPROCESS *Process

+04c uint32   ContextSwitches

+050 int32    WaitStatus

+054 byte     WaitIrql

+055 char     WaitMode

+056 byte     WaitNext

+057 byte     WaitReason

+058 struct   _KWAIT_BLOCK *WaitBlockList

+05c struct   _LIST_ENTRY WaitListEntry

+064 uint32   WaitTime

+068 char     BasePriority

+069 byte     DecrementCount

+06a char     PriorityDecrement

+06b char     Quantum

+06c struct   _KWAIT_BLOCK WaitBlock[4]

+0cc void     *LegoData

+0d0 uint32   KernelApcDisable

+0d4 uint32   UserAffinity

+0d8 byte     SystemAffinityActive

+0d9 byte     PowerState

+0da byte     NpxIrql

+0db byte     Pad[1]

+0dc void     *ServiceTable

+0e0 struct   _KQUEUE *Queue

+0e4 uint32   ApcQueueLock

+0e8 struct  _KTIMER Timer

+110 struct   _LIST_ENTRY QueueListEntry

+118 uint32   Affinity

+11c byte     Preempted

+11d byte     ProcessReadyQueue

+11e byte     KernelStackResident

+11f byte     NextProcessor

+120 void     *CallbackStack

+124 void     *Win32Thread

+128 struct   _KTRAP_FRAME *TrapFrame

+12c struct   _KAPC_STATE *ApcStatePointer[2]

+134 char     PreviousMode

+135 byte     EnableStackSwap

+136 byte     LargeStack

+137 byte     ResourceIndex

+138 uint32   KernelTime

+13c uint32   UserTime

+140 struct   _KAPC_STATE SavedApcState

+158 byte     Alertable

+159 byte     ApcStateIndex

+15a byte     ApcQueueable

+15b byte     AutoAlignment

+15c void     *StackBase

+160 struct   _KAPC SuspendApc

+190 struct   _KSEMAPHORE SuspendSemaphore

+1a4 struct   _LIST_ENTRY ThreadListEntry

+1ac char     FreezeCount

+1ad char     SuspendCount

+1ae byte     IdealProcessor

+1af byte     DisableBoost

在偏移0x5c处有一个WaitListEntry成员,这个就是用来链接到线程调度链表的。在偏移0x34处有一个ApcState成员结构,在ApcState中的Process域就是指向当前线程关联的进程的KPROCESS块,由于KPROCESS块是EPROCESS块的第一个元素,所以找到了KPROCESS块指针也就是找到了EPROCESS块的指针。找到了EPROCESS就不用多少了,就可以取得当前线程的进程的名字,ID号等。

2.       线程调度

在windows系统中,线程调度主要分成三条主要的调度链表。分别是KiWaitInListHead,

KiWaitOutListhead,KiDispatcherReadyListHead,分别是两条阻塞链,一条就绪链表,当线程获得CPU执行的时候,系统分配一个时间片给线程,当发生一次时钟中断就从分配的时间片上减去一个时钟中断的值,如果这个值小于零了也就是时间片用完了,那么这个线程根据其优先级载入到相应的就绪队列末尾。KiDispatcherReadyListHead是一个数组链的头部,在windows 2000中它包含有32个队列,分别对应线程的32个优先级。如果线程因为同步,或者是对外设请求,那么阻塞线程,让出CPU的所有权,加如到阻塞队列里面去。CPU从就绪队列里面,按照优先权的前后,重新调度新的线程的执行。当阻塞队列里面的线程获得所需求的资源,或者是同步完成就又重新加到就绪队列里面等待执行。

3.       通过线程调度链表进行隐藏进程的检测

void DisplayList(PLIST_ENTRY ListHead)

{

    PLIST_ENTRY List = ListHead->Flink;

    if ( List == ListHead )

    {

    // DbgPrint("return\n");

        return;

    }

    PLIST_ENTRY NextList = List;

    while ( NextList != ListHead )

    {

        PKTHREAD Thread = ONTAINING_RECORD(NextList, KTHREAD, WaitListEntry);

        PKPROCESS Process = Thread->ApcState.Process;

        PEPROCESS pEprocess = (PEPROCESS)Process;

        DbgPrint("ImageFileName = %s \n",pEprocess->ImageFileName);

        NextList = NextList->Flink;

    }

}

以上是对一条链进行进程枚举。所以我们必须找到KiWaitInListHead,KiWaitOutListhead,KiDispatcherReadyListHead的地址,由于他们都没有被ntoskrnl.exe导出来,所以只有通过硬编码的办法给他们赋值。通过内核调试器,能找到(windows2000 sp4):

PLIST_ENTRY KiWaitInListHead =          (PLIST_ENTRY)0x80482258;

PLIST_ENTRY KiDispatcherReadyListHead = (PLIST_ENTRY)0x804822e0;

PLIST_ENTRY KiWaitOutListhead =         (PLIST_ENTRY)0x80482808;

遍历所有的线程调度链表。

for ( i =0; i<32 ;i++ )

{

    DisplayList(KiDispatcherReadyListHead+i);

}

DisplayList(KiWaitInListHead);

DisplayList(KiWaitOutListhead);

通过上面的那一小段核心代码就能把删除活动进程链表的隐藏进程给查出来。也可以改写一个友好一点的驱动,加入IOCTL,得到的进程信息把打印在DbgView中把它返回给Ring3的应用程序,然后应用程序对返回的数据进行处理,和Ring3级由PSAPI得到的进程对比,然后判断是不是有隐藏的进程。

4.       绕过内核调度链表隐藏进程。

Xfocus上SoBeIt提出了绕过内核调度链表进程检测。详情可以参见原文:

http://www.xfocus.net/articles/200404/693.html

由于现在的基于线程调度的检测系统都是通过内核调试器得硬编码来枚举所有的调度线程的,所以我们完全可以自己创造一个那三个调度链表头,然后把原链表头从链中断开,把自己的申请的链表头接上去。由于线程调度的时候会用到KiFindReadyThread等内核API,在KiFindReadyThread里面又会去访问KiDispatcherReadyListHead,所以我完全可以把KiFindReadyThread中那段访问KiDispatcherReadyListHead的机器码修改了,把原KiDispatcherReadyListHead的地址改成我们新申请的头。

kd> u KiFindReadyThread+0x48

nt!KiFindReadyThread+0x48:

804313db 8d34d5e0224880 lea esi,[nt!KiDispatcherReadyListHead (804822e0)+edx*8]

很明显我们可以在机器码中看到e0224880,由于它是在内存中以byte序列显示的转换成DWORD就是804822e0就是我们KiDispatcherReadyListHead的地址。所以我们要做的就是把[804313db+3]赋值成我们自己申请的一个链头。使其系统以后对原链表头的操作变化成对我们自己申请的链表头的操作。同理用到那三个链表头的还有一些内核API,所以必须找到他们在机器码中含有原表头地址信息的具体地址然后把它全部替换掉。不然系统调度就会出错.系统中用到KiWaitInListHead的例程:KeWaitForSingleObject、 KeWaitForMultipleObject、 KeDelayExecutionThread、 KiOutSwapKernelStacks。用到KiWaitOutListHead的例程和KiWaitInListHead的一样。使用KiDispatcherReadyListHead的例程有:KeSetAffinityThread、KiFindReadyThread、KiReadyThread、KiSetPriorityThread、NtYieldExecution、KiScanReadyQueues、KiSwapThread。

申请新的表头空间:

pNewKiWaitInListHead = (PLIST_ENTRY)ExAllocatePool \

                        (NonPagedPool,sizeof(LIST_ENTRY));
pNewKiWaitOutListHead = (PLIST_ENTRY)ExAllocatePool \

                        (NonPagedPool, sizeof(LIST_ENTRY));

pNewKiDispatcherReadyListHead = (PLIST_ENTRY)ExAllocatePool \

                        (NonPagedPool, 32 * sizeof(LIST_ENTRY));

下面仅仅以pNewKiWaitInListHead头为例,其他的表头都是一样的操作。

新调度链表的表头替换:

InitializeListHead(pNewKiWaitInListHead);  

把原来的系统链表头摘除,把新的接上去:

pFirstEntry = pKiWaitInListHead->Flink;
pLastEntry = pKiWaitInListHead->Blink;
pNewKiWaitInListHead->Flink = pFirstEntry;
pNewKiWaitInListHead->Blink = pLastEntry;
pFirstEntry->Blink = pNewKiWaitInListHead;
pLastEntry->Flink = pNewKiWaitInListHead;

剩下的就是在原来的线程调度链表上做文章了使其基于线程调度检测系统看不出什么异端.

for(;;)

{

    InitializeListHead(pKiWaitInListHead);

    for(pEntry = pNewKiWaitInListHead->Flink;

    pEntry && pEntry != pNewKiWaitInListHead;

    pEntry = pEntry->Flink)

{

pETHREAD = (PETHREAD)(((PCHAR)pEntry)-0x5c);

pEPROCESS = (PEPROCESS)(pETHREAD->Tcb.ApcState.Process);

        PID = *(PULONG)(((PCHAR)pEPROCESS)+0x9c);

        if(PID == 0x8)

                 continue;

pFakeETHREAD = ExAllocatePool(PagedPool,sizeof(FAKE_ETHREAD));

        memcpy(pFakeETHREAD, pETHREAD,sizeof(FAKE_ETHREAD));

        InsertHeadList(pKiWaitInListHead, &pFakeETHREAD->WaitListEntry);

}

...休息一段时间

}

首先每过一小段时间就把原来的线程调度链表清空,然后遍历当前的线程调度链,判断链中的每一个KPROCESS块是不是要属于要隐藏的进程线程,如果是就跳过,不是就自己构造一个ETHREAD块把当前的信息拷贝过去,然后把自己构造的ETHREAD块加入到原来的调度链表中。为什么要自己构造一个ETHREAD?其原因主要有2个,其一为了使检测系统看起来更可信,如果仅仅清空原来的线程调度链表那么检测系统将查不出来任何的线程和进程信息,

很明显,这无疑不打自招的说,系统里面已经有东西了。其二,如果把自己构造的ETHREAD块挂接在原调度链表中,检测系统会访问挂在原来调度链表上的ETHREAD块里面的成员,如果不自己构造一个和真实ETHREAD块重要信息一样的块,那么检测系统很有可能出现非法访问,然后就boom兰屏了。

    实际上所谓的绕过系统检测仅仅是针对基于线程调度的检测进程的防御系统而言的,其实系统依旧在进行线程调度,访问的是我们新建的链表头部。而检测系统访问的是原来的头部,他后面的数据项是我们自己申请的,系统并不访问。

 

5.       检测绕过内核调度链表隐藏进程

一般情况下我们是通过内核调试器得到那三条链表的内核地址,然后进行枚举。这就给隐藏者留下了机会,如上面所示。但是我们完全可以把上面那种隐藏进程检测出来。我们也通过在内核函数中取得硬编码的办法来分别取得他们的链表头的地址。如上面我们已经看见了 KiFindReadyThread+0x48+3出就是KiDispatcherReadyListHead的地址,如果用上面的绕过内核调度链表检测办法同时也去要修改KiFindReadyThread+0x48+3的值为新链表的头部地址。所以我们的检测系统完全可以从KiFindReadyThread+0x48+3(0x804313de)去取得KiDispatcherReadyListHead的值。同理KiWaitInListHead, KiWaitOutListhead也都到使用他们的相应的内核函数里面去取得地址。就算原地址被修改过,我们也能把修改过后的调度链表头给找出来。所以欺骗就不行了。