检测隐藏进程(二)

来源:互联网 发布:无聊了怎么办 知乎 编辑:程序博客网 时间:2024/04/28 04:25

             OB_TYPE_JOB     : begin
                                  if Jobs then
                                   begin
                                    Size := SizeOf(JOBOBJECT_BASIC_PROCESS_ID_LIST) + 4 * 1000;
                                    GetMem(Info, Size);
                                    Info^.NumberOfAssignedProcesses := 1000;
                                    if QueryInformationJobObject(tHandle, JobObjectBasicProcessIdList,
                                                                 Info, Size, nil) then
                                       for l := 0 to Info^.NumberOfProcessIdsInList - 1 do
                                         if not IsPidAdded(List, Info^.ProcessIdList[l]) then
                                           begin
                                            GetMem(NewItem, SizeOf(TProcessRecord));
                                            ZeroMemory(NewItem, SizeOf(TProcessRecord));
                                            NewItem^.ProcessId   := Info^.ProcessIdList[l];
                                            AddItem(List, NewItem);
                                           end;
                                    FreeMem(Info);
                                   end;
                                  end;

               OB_TYPE_THREAD : begin
                                  if Threads then
                                  if ZwQueryInformationThread(tHandle, THREAD_BASIC_INFO,
                                                              @THRInfo,
                                                              SizeOf(THREAD_BASIC_INFORMATION),
                                                              nil) = STATUS_SUCCESS then
                                    if not IsPidAdded(List, THRInfo.ClientId.UniqueProcess) then
                                     begin
                                       GetMem(NewItem, SizeOf(TProcessRecord));
                                       ZeroMemory(NewItem, SizeOf(TProcessRecord));
                                       NewItem^.ProcessId   := THRInfo.ClientId.UniqueProcess;
                                       AddItem(List, NewItem);
                                     end;
                                 end;

             end;
             CloseHandle(tHandle);
            end;
          CloseHandle(hProcess);
        end;
VirtualFree(HandlesInfo, 0, MEM_RELEASE);
end;

不幸的是,上面提到的这些方法有些只能得到进程ID,而不能得到进程名字。因此,我们还需要通过进程ID得到进程的名称。当然,当这些进

程是隐藏进程的时候我们就不能使用ToolHelp API来实现。所以我们应该访问进程内存通过读取该进程的PEB得到进程名称。PEB地址能用

ZwQueryInformationProcess函数获得。以上所说的功能实现代码如下:

Code:

function GetNameByPid(Pid: dword): string;
var
hProcess, Bytes: dword;
Info: PROCESS_BASIC_INFORMATION;
ProcessParametres: pointer;
ImagePath: TUnicodeString;
ImgPath: array[0..MAX_PATH] of WideChar;
begin
Result := ’’;
ZeroMemory(@ImgPath, MAX_PATH * SizeOf(WideChar));
hProcess := OpenProcess(PROCESS_QUERY_INFORMATION or PROCESS_VM_READ, false, Pid);
if ZwQueryInformationProcess(hProcess, ProcessBasicInformation, @Info,
                              SizeOf(PROCESS_BASIC_INFORMATION), nil) = STATUS_SUCCESS then
begin
   if ReadProcessMemory(hProcess, pointer(dword(Info.PebBaseAddress) + $10),
                        @ProcessParametres, SizeOf(pointer), Bytes) and
      ReadProcessMemory(hProcess, pointer(dword(ProcessParametres) + $38),
                        @ImagePath, SizeOf(TUnicodeString), Bytes) and
      ReadProcessMemory(hProcess, ImagePath.Buffer, @ImgPath,
                        ImagePath.Length, Bytes) then
        begin
          Result := ExtractFileName(WideCharToString(ImgPath));
        end;
   end;
CloseHandle(hProcess);
end;


当然,用户态隐藏进程的检测方法不止这些,还能想一些稍微复杂一点的新方法(比如,用Set视窗系统HookEx函数对可访问进程的注入和当

我们的DLL并成功加载后对进程列表的分析),不过目前我们将用上面提到的方法来解决问题。这些方法的好处是他们能简单地编程实现,并

且除了能检测到用户态的隐藏进程,还能检测到少数的在内核态实现的隐藏进程... 要实现真正可靠的进程隐藏工具我们应该使用视窗系统

未公开的内核数据结构编写内核驱动程式。


内核态(Ring 0)的检测
恭喜你,我们终于开始进行内核态隐藏进程的分析。内核态的检测方法同用户态的检测方法的主要差别是所有的进程列表都没有使用API调用而

是直接来自系统内部数据结构。在这些检测方法下隐藏进程要困难得多,因为他们都是基于同视窗系统内核相同的原理实现的,并且从这些内核

数据结构中删除进程将导致该进程完全失效。

内核中的进程是什么?每一个进程都有自己的地址空间,描述符,线程等,内核的数据结构就涉及这些东西。每一个进程都是由EPROCESS结构描述,而所有进程的结构都被一个双向循环链表维护。进程隐藏的一个方法就是改动进程结构链表的指针,使得链表枚举跳过自身达到进程隐藏的目的。避开进程枚举并不影响进程的所有功能。无论怎样,EPROCESS结构总是存在的,对一个进程的正常功能来说他是必要的。在内核态检测隐藏进程的主要方法就是对这个结构的检查。

 

我们应该定义一下将要储存的进程信息的变量格式。这个变量格式应该非常方便地存储来自驱动的数据(附录)。结构定义如下:

Code:

typedef struct _ProcessRecord
{
   ULONG       Visibles;
   ULONG       SignalState;
   BOOLEAN     Present;
   ULONG       ProcessId;
   ULONG       ParrentPID;
   PEPROCESS   pEPROCESS;
   CHAR        ProcessName[256];
} TProcessRecord, *PProcessRecord;

应该为这些结构分配连续的大块的内存,并且不设置最后一个结构的Present标志。

在内核中使用ZwQuerySystemInformation函数得到进程列表。

我们先从最简单的方式开始,通过ZwQuerySystemInformation函数得到进程列表:

Code:

PVOID GetNativeProcessList(ULONG *MemSize)
{
   ULONG PsCount = 0;
   PVOID Info = GetInfoTable(SystemProcessesAndThreadsInformation);
   PSYSTEM_PROCESSES Proc;
   PVOID Mem = NULL;
   PProcessRecord Data;

   if (!Info) return NULL; else Proc = Info;

   do
   {
      Proc = (PSYSTEM_PROCESSES)((ULONG)Proc + Proc->NextEntryDelta);  
      PsCount++;
   } while (Proc->NextEntryDelta);

   *MemSize = (PsCount + 1) * sizeof(TProcessRecord);

   Mem = ExAllocatePool(PagedPool, *MemSize);

   if (!Mem) return NULL; else Data = Mem;
  
   Proc = Info;
   do
   {
      Proc = (PSYSTEM_PROCESSES)((ULONG)Proc + Proc->NextEntryDelta);
      wcstombs(Data->ProcessName, Proc->ProcessName.Buffer, 255);
      Data->Present    = TRUE;
      Data->ProcessId = Proc->ProcessId;
      Data->ParrentPID = Proc->InheritedFromProcessId;
      PsLookupProcessByProcessId((HANDLE)Proc->ProcessId, &Data->pEPROCESS);
      ObDereferenceObject(Data->pEPROCESS);
      Data++;
   } while (Proc->NextEntryDelta);

   Data->Present = FALSE;

   ExFreePool(Info);

   return Mem;
}

以这个函数做参考,所有内核态的隐藏进程都不会被检测出来,不过所有的用户态隐藏进程如hxdef是绝对逃不掉的。

在下面的代码中我们能简单地用GetInfoTable函数来得到信息。为了防止有人问那是什么东西,下面列出完整的函数代码。

Code:

/*
Receiving buffer with results from ZwQuerySystemInformation.
*/
PVOID GetInfoTable(ULONG ATableType)
{
   ULONG mSize = 0x4000;
   PVOID mPtr = NULL;
   NTSTATUS St;
   do
   {
      mPtr = ExAllocatePool(PagedPool, mSize);
      memset(mPtr, 0, mSize);
      if (mPtr)
      {
         St = ZwQuerySystemInformation(ATableType, mPtr, mSize, NULL);
      } else return NULL;
      if (St == STATUS_INFO_LENGTH_MISMATCH)
      {
         ExFreePool(mPtr);
         mSize = mSize * 2;
      }
   } while (St == STATUS_INFO_LENGTH_MISMATCH);
   if (St == STATUS_SUCCESS) return mPtr;
   ExFreePool(mPtr);
   return NULL;
}

我认为这段代码是非常容易理解的...

利用EPROCESS结构的双向链表得到进程列表。
我们又进了一步。接下来我们将通过遍历EPROCESS结构的双向链表来得到进程列表。链表的表头是PsActiveProcessHead,因此要想正确地枚举

进程我们需要找到这个并没有被导出的符号。在这之前我们应该知道System进程是所有进程列表中的第一个进程。在DriverEntry例程开始时我

们需要用PsGetCurrentProcess函数得到当前进程的指针(使用SC管理器的API或ZwLoadDriver函数加载的驱动始终都是加载到System进程的

上下文中的),BLink在ActiveProcessLinks中的偏移将指向PsActiveProcessHead。像这样:

Code:

PsActiveProcessHead = *(PVOID *)((PUCHAR)PsGetCurrentProcess + ActiveProcessLinksOffset + 4);


目前就能遍历这个双向链表来创建进程列表了:

Code:

PVOID GetEprocessProcessList(ULONG *MemSize)
{
   PLIST_ENTRY Process;
   ULONG PsCount = 0;
   PVOID Mem = NULL;
   PProcessRecord Data;

   if (!PsActiveProcessHead) return NULL;

   Process = PsActiveProcessHead->Flink;

   while (Process != PsActiveProcessHead)
   {
      PsCount++;
      Process = Process->Flink;
   }

 

   PsCount++;

   *MemSize = PsCount * sizeof(TProcessRecord);

   Mem = ExAllocatePool(PagedPool, *MemSize);
   memset(Mem, 0, *MemSize);

   if (!Mem) return NULL; else Data = Mem;

   Process = PsActiveProcessHead->Flink;

   while (Process != PsActiveProcessHead)
   {
      Data->Present     = TRUE;
      Data->ProcessId   = *(PULONG)((ULONG)Process - ActPsLink + pIdOffset);
      Data->ParrentPID = *(PULONG)((ULONG)Process - ActPsLink + ppIdOffset);
      Data->SignalState = *(PULONG)((ULONG)Process - ActPsLink + 4);
      Data->pEPROCESS   = (PEPROCESS)((ULONG)Process - ActPsLink);
      strncpy(Data->ProcessName, (PVOID)((ULONG)Process - ActPsLink + NameOffset), 16);     
      Data++;
       Process = Process->Flink;
  
   }

   return Mem;
}

为了得到进程名称、ID和父进程ID,我们利用他们在EPROCESS结构中的偏移地址(pIdOffset, ppIdOffset, NameOffset, ActPsLink)。这些

偏移随着视窗系统系统版本的不同而不同,因此我们要在进程检测程式的代码中进行区分后得到他们正确的值(附录)。

所有一个通过API截取方式隐藏的进程都将被上面这个方法检测出来。不过如果进程是通过DKOM(直接处理内核对象 - Direct Kernel Object

Manipulation)方式隐藏,那这个方法就失效了,因为这种进程都被从进程链表中删掉了。

通过列举调度程式(scheduler)中的线程得到进程列表。

对付这种隐藏进程(俄文翻译kao注:这个地方原文写的比较模糊,作者大概的意思应该是“使用DKOM的方式检测隐藏进程”)的其中一种检测

方式是通过调度程式(scheduler)中的线程列表来得到进程列表。视窗系统 2000有三个维护线程的双向链表(KiWaitInListHead,

KiWaitOutListHead, KiDispatcherReadyListHead)。前面两个链表包含等待某种事件的线程,最后面的链表包含的是等待执行的线程。我们

处理这些链表,根据线程链表结构ETHREAD中的偏移就能得到一个线程的ETHREAD指针(俄文翻译kao注:原文中这句话实在是太难懂了,希望

我翻译的正确)。这个结构包括了非常多进程相关指针,也就是结构_KPROCESS *Process(0x44, 0x150)和结构_EPROCESS *ThreadsProcess

(0x22C, 这仅是视窗系统 2000中的偏移量)。前面两个指针对于一个线程的功能性没有所有影响,因此能非常容易修改他们来隐藏进程。相反,

第三个指针是当转换地址空间时调度程式(schedler)使用的指针,所以这个指针是不能修改的。我们就用他来找到拥有某个线程的进程。

Klister就是使用了这种检测方法,他的最大的缺点就是只能在视窗系统 2000平台上工作(不过在这个平台上某个补丁包也会让他失效)。导致

这个情况发生的原因就是这种程式使用了硬编码的线程链表地址,而在每个补丁包中这些地址可能都是不同的。

在程式中使用硬编码地址是非常糟糕的解决方案,操作系统的升级就会使你的程式无法正常工作,要尽量避免使用这种检测方法。所以应该通过

分析那些使用了这些链表的内核函数来动态地得到他们的地址。

首先我们试试看在视窗系统 2000平台上找出KiWaitInListHead和KiWaitOutListHead.使用链表地址的函数KeWaitForSingleObject代码如下:

Code:

.text:0042DE56                 mov     ecx, offset KiWaitInListHead
.text:0042DE5B                 test    al, al
.text:0042DE5D                 jz      short loc_42DE6E
.text:0042DE5F                 cmp     byte ptr [esi+135h], 0
.text:0042DE66                 jz      short loc_42DE6E
.text:0042DE68                 cmp     byte ptr [esi+33h], 19h
.text:0042DE6C                 jl      short loc_42DE73
.text:0042DE6E                 mov     ecx, offset KiWaitOutListHead

我们使用反汇编器(用我写的LDasm)反汇编KeWaitForSingleObject函数来获得这些地址。当索引(pOpcode)指向指令“mov ecx,

KiWaitInListHead”,(pOpcode + 5)指向的就是指令“test al, al”,(pOpcode + 24)指向的就是“mov ecx, KiWaitOutListHead”。

这样我们就能通过索引(pOpcode + 1)和(pOpcode + 25)正确地得到KiWaitInListHead和KiWaitOutListHead的地址了。搜索地址的代码

如下:

Code:

void Win2KGetKiWaitInOutListHeads()
{
   PUCHAR cPtr, pOpcode;
   ULONG Length;
  
   for (cPtr = (PUCHAR)KeWaitForSingleObject;
        cPtr < (PUCHAR)KeWaitForSingleObject + PAGE_SIZE;
             cPtr += Length)
   {
      Length = SizeOfCode(cPtr, &pOpcode);

      if (!Length) break;
     
      if (*pOpcode == 0xB9 && *(pOpcode + 5) == 0x84 && *(pOpcode + 24) == 0xB9)
      {
         KiWaitInListHead = *(PLIST_ENTRY *)(pOpcode + 1);
         KiWaitOutListHead = *(PLIST_ENTRY *)(pOpcode + 25);
         break;
      }
   }

   return;
}


在视窗系统 2000平台下我们能用同样的方法得到KiDispatcherReadyListHead, 搜索KeSetAffinityThread函数:

Code:

.text:0042FAAA                 lea     eax, KiDispatcherReadyListHead[ecx*8]
.text:0042FAB1                 cmp     [eax], eax


搜索KiDispatcherReadyListHead函数的代码:

Code:

void Win2KGetKiDispatcherReadyListHead()
{
   PUCHAR cPtr, pOpcode;
   ULONG Length;
  
   for (cPtr = (PUCHAR)KeSetAffinityThread;
        cPtr < (PUCHAR)KeSetAffinityThread + PAGE_SIZE;
             cPtr += Length)
   {
      Length = SizeOfCode(cPtr, &pOpcode);

      if (!Length) break;     

      if (*(PUSHORT)pOpcode == 0x048D && *(pOpcode + 2) == 0xCD && *(pOpcode + 7) == 0x39)
      {
         KiDispatcherReadyListHead = *(PVOID *)(pOpcode + 3);
         break;
      }
   }

   return;
}

不幸的是,视窗系统 XP内核完全不同于视窗系统 2000内核。XP下的调度程式(scheduler)只有两个线程链表:KiWaitListHead和KiDispatcherReadyListHead。我们能通过搜索KeDelayExecutionThread函数来查找KeWaitListHead:

Code:

.text:004055B5                 mov     dword ptr [ebx], offset KiWaitListHead
.text:004055BB                 mov     [ebx+4], eax


搜索代码如下:

Code:

void XPGetKiWaitListHead()
{
   PUCHAR cPtr, pOpcode;
   ULONG Length;

   for (cPtr = (PUCHAR)KeDelayExecutionThread;
        cPtr < (PUCHAR)KeDelayExecutionThread + PAGE_SIZE;
             cPtr += Length)
   {
      Length = SizeOfCode(cPtr, &pOpcode);

      if (!Length) break;

      if (*(PUSHORT)cPtr == 0x03C7 && *(PUSHORT)(pOpcode + 6) == 0x4389)
      {
         KiWaitInListHead = *(PLIST_ENTRY *)(pOpcode + 2);
         break;
      }
   }

   return;
}

 
原创粉丝点击