以前就有了,只是还是自己实现了一下,当做复习吧,大侠飘过。进程的隐藏一直是木马程序设计者不断探求的重要技术,目前Rootkit使用的主要的进程隐藏技术有:通过hook内核API来来隐藏,通过从进程链表上摘除自身来隐藏。这样对于进程普通的API列举将不会得到真实的数据,为了获取可信进程信息,就要要采用其他的方法。
众所周知,windows执行的基本单位是线程,而不是进程,所以才有从进程链表上摘除自身的进程隐藏方法,这是虽然从进程链表上摘除了自身,但不会影响操作系统的调度,所以不影响程序运行。但是不能从线程链表上摘除自身,除非程序不想获得cpu时间了。在内核里和线程调度有关的结构是TCB,在ETHREAD里的KTHREAD里,和本文有关的域有如下几个:
 Typedef _KTHREAD {
……
 KAPC_STATE ApcState;
 LIST_ENTRY WaitListEntry 
 LIST_ENTRY QueueListEntry
 LIST_ENTRY ThreadListEntry
……
 };
从网上得来的资料说在windows xp下,windows线程分派器使用pKiDispatcherReadyListHead和pKiWaitListHead这两个结构来调度线程。它的线程分派主要利用KTHREAD中的WaitListEntry 、QueueListEntry和ThreadListEntry来完成,这三个成员变量代表了三种不同的线程状态,是三个双向链表结构,ThreadListEntry队列是处于准备状态的,其余两个处于等待状态。pKiDispatcherReadyListHead是指向一个有32个元素的LIST_ENTRY数组,Windows线程有32个优先级,所以有32个节点。每个数组元素连接一个双向链表。如图一所示


线程调度就是基于这三个链表的。如图所示

基于线程调度链表的隐藏进程检测,就是遍历这个两个链表获取线程,再通过ThreadProcess域得到进程的EPROCES。但是自己写出程序测试时发现没这么简单,在虚拟机里面pKiWaitListHead我目前测试的情况来看都是对应WaitListEntry,没有发现过QueueListEntry里面的线程,而且pKiDispatcherReadyListHead对应的都是在WaitListEntry里面的元素,相当无语。不知道是不是还有什么其他未知的数据结构,知道的大侠还请不吝赐教。首先要解决的问题是如何得到pKiDispatcherReadyListHead和pKiWaitListHead,已经有前辈总结过了,使用KiDispatcherReadyListHead的例程有:KeSetAffinityThread、KiFindReadyThread、KiReadyThread、KiSetPriorityThread、NtYieldExecution、KiScanReadyQueues、KiSwapThread。系统中用到KiWaitInListHead的例程:KeWaitForSingleObject()、 KeWaitForMultipleObject()、 KeDelayExecutionThread、 KiOutSwapKernelStacks。我自己实现的代码如下:
ULONG FindKiWaitListHead()
{ UCHAR* Addre=(UCHAR*)KeDelayExecutionThread;//这个WDK有申明
ULONG  index=0;  for(;index<=1000;index++) 
{ if(*(unsigned short*)Addre==0x3c7)//unsigned short 就是汇编里的word     

{if(*(unsigned short*)(Addre+6)==0x4389)    
KdPrint(("find the KiWaitListHead  address :%08X",*(ULONG*)(Addre+2))); break;     
}    
else    
{        Addre++;  
}  

if(index>1000)  
{   KdPrint(("没有找到KiWaitListHead")); 

return *(ULONG*)(Addre+2);
}
搜索KiDispatcherReadyListHead的代码有点长,就不贴出来了,实现思路是先由导出函数KeSetAffinityThread;获得KiSetAffinityThread的地址,从这个地址往上搜找到一个CMP指令,这个指令再上面的一个DWORD就是KiDispatcherReadyListHead了。具体实现请看附件,也可以用其他的方法获得,比如NtYieldExecution,这个进去之后一个lea指令就装载了KiDispatcherReadyListHead的地址,就是这个函数地址要由SSDT获取,不知道为什么没有导出,却在ICeSword下看到,用MmGetSystemRoutineAddress没办法得到,觉得不稳妥,就没有使用这个方法了。下面就开始列进程了,但是还有一个问题就是怎么知道KiWaitListEntry链表里的的指针到底是指向WaitListEntry 还是QueueListEntry呢,应该是还有一个域来表征的,可是我对ETHREAD结构学习不深,实际也很难找到完整资料来描述其中各个域的资料,知道的还望不吝赐教。在这里使用了ServiceTable 来判断,已经知道了他们的偏移,可以知道他们的偏移之差。ServiceTable和QueueListEntry的偏移之差是0x38,和WaitListEntry的偏移之差是0x80,,通过判断这个偏移差就可以知道到底是那种情况了。再插一点,有的读者可能对ServiceTable不太清楚,这个位,在线程调用任何GUI函数之前指向的是SSDT,调用过GUI函数之后就变成shadow ssdt,具体可以参考《深入解析windows操作系统》一书的有关章节。
找到这两个结构之后就就可以遍历链表来获取进程信息了,遍历链表的代码很简单 

代码:
NTSTATUS ListProcessInWaitList(PLIST_ENTRY List)
{//列举处于等待状态的进程
   if(!List)
     return STATUS_UNSUCCESSFUL;
   PLIST_ENTRY Seek=List->Flink;
   ULONG eproc;
   int ret;
   int Temp=0;
   KSPIN_LOCK Lock;
   KeInitializeSpinLock(&Lock);
   KIRQL irql=KeRaiseIrqlToDpcLevel();
     KeAcquireSpinLockAtDpcLevel(&Lock);
   while(Seek!=List)
   {   
     ret=IsWaitList(Seek);
    
      if(ret==0)//Seek此时是在WaitListEntry里
     { 
       eproc=GetProcessFormWaitList(Seek,WAITLIST);
         DisplayProcess(eproc);

     }
     else if(ret==1)
     {
       eproc=GetProcessFormWaitList(Seek,QUEUE_LISTENTRY);
      DisplayProcess(eproc);
     }
     else
     {
       KdPrint(("没有得到有效的数据\n"));
     }
     Seek=Seek->Flink;
     Temp++;
   }
   KeReleaseSpinLockFromDpcLevel(&Lock);
   KeLowerIrql(irql);
   KdPrint(("total %d thread in waitList!\n",Temp));
   return STATUS_SUCCESS;
}
下面一个主要函数是列举处于KiDispatcherReadyListHead队列里的线程从而得到进程。其主要代码为:
代码:
for(int i=0;i<32;i++)
  { 
    for( Seek=ListTemp[i].Flink;Seek!=&ListTemp[i];Seek=Seek->Flink)
    {    Temp++;
      ret=IsWaitList(Seek);
      if(ret==0)
      {
      eproc=GetProcessFormWaitList(Seek,WAITLIST);
      if(eproc==0)
      {
        KdPrint(("获取EPROCESS地址失败\n"));
        continue;
      }
      DisplayProcess(eproc);
      }
      else if(ret==1)
      {
      eproc=GetProcessFormWaitList(Seek,QUEUE_LISTENTRY);
      if(eproc==0)
      {
        KdPrint(("获取EPROCESS地址失败\n"));
        continue;
      }
      DisplayProcess(eproc);
      }
      else
      {eproc=GetProcessFormWaitList(Seek,THREAD_LISTENTRY);
      if(eproc==0)
      {
        KdPrint(("获取EPROCESS地址失败\n"));
        continue;
      }
      //DisplayProcess(eproc);
      }  }  }
现在可以测试一下检测效果了,测试之后就会发现,理论是这样没错,却怎么也检测不到所有线程。不知道为什么,估计原因是线程有不止两种状态,还有转换,备用等状态。测试检测隐藏进程时发现,如果打开一个记事本,一段时间不动它就检测不到了,还有一个更怪的现象是Smss进程我一次都没有检测到过。N次试验后发现,只要进程一段时间没有执行任何代码,比如:就像打开记事本却什么也不做,这种方法就检测不到这个进程了。不过只要进程在近段时间执行过代码,那么不管你怎么隐藏,都会被检测到,后来发现ruk也使用了这种检测方法,但也只是作为一种辅助手段,估计原因就在于此。
上传的附件 hunt.rar