五维思考

学习要加,骄傲要减,机会要乘,懒惰要除。 http://www.5dthink.cn

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::

许多用户都有过用Windows自带的任务管理器查看所有进程的经验,并且很多人都认为在任务管理器中隐藏进程是不可能的。而实际上,进程隐藏是再简单不过的事情了。有许多可用的方法和参考源码可以达到进程隐藏的目的。令我惊奇的是只有很少一部分的木马使用了这种技术。估计1000个木马中仅有1个是进程隐藏的。我认为木马的作者太懒了,因为隐藏进程需要进行的额外工作仅仅是对源代码的拷贝-粘贴。所以我们应该期待即将到来的会隐藏进程的木马。

自然地,也就有必要研究进程隐藏的对抗技术。杀毒软件和防火墙制造商就像他们的产品不能发现隐藏进程一样落后了。在少之又少的免费工具中,能够胜任的也只有Klister(仅运行于Windows 2000平台)了。所有其他公司关注的只有金钱(俄文译者kao注:不完全正确,FSecure的BlackLight Beta也是免费的)。除此之外,所有的这些工具都可以很容易的anti掉。

用程序实现隐藏进程探测技术,我们有两种选择:
* 基于某种探测原理找到一种隐藏的方法;
* 基于某个程序找到一种隐藏的方法,这个要简单一些。

购买商业软件产品的用户不能修改程序,这样可以保证其中绑定的程序的安全运行。因此第2种方法提到的程序就是商业程序的后门(rootkits)(例如hxdef Golden edition)。唯一的解决方案是创建一个免费的隐藏进程检测的开源项目,这个程序使用几种不同的检测方法,这样可以发现使用某一种方法进行隐藏的进程。任何一个用户都可以抵挡某程序的捆绑程序,当然那要得到程序的源代码并且按照自己的意愿进行修改。

在这篇文章中我将讨论探测隐藏进程的基本方法,列出该方法的示例代码,并创建一个能够检测上面我们提到的隐藏进程的程序。

在用户态(ring 3)检测
我们从简单的用户态(ring 3)检测开始,不使用驱动。事实上,每一个进程都会留下某种活动的痕迹,根据这些痕迹,我们就可以检测到隐藏的进程。这些痕迹包括进程打开的句柄、窗口和创建的系统对象。要避开这种检测技术是非常简单的,但是这样做需要留意进程留下所有痕迹,这种模式没有被用在任何一个公开发行的后门(rootkits)上。(不幸的是内部版本没有对我开放)。用户态方法容易实现,使用安全,并且能够得到很好的效果,因此这种方法不应该被忽略。

首先我们定义一下用到的数据,如下:

Code:

type
 PProcList = ^TProcList;
 TProcList = packed record
   NextItem: pointer;
   ProcName: array [0..MAX_PATH] of Char;
   ProcId: dword;
   ParrentId: dword;
 end;


使用ToolHelp API获得所有进程列表
定义一下获得进程列表的函数。我们要比较这个结果和通过其他途径得到的结果:

Code:

{
 Acquiring list of processes by using ToolHelp API.
}
procedure GetToolHelpProcessList(var List: PListStruct);
var
 Snap: dword;
 Process: TPROCESSENTRY32;
 NewItem: PProcessRecord;
begin
  Snap := CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
  if Snap <> INVALID_HANDLE_VALUE then
     begin
      Process.dwSize := SizeOf(TPROCESSENTRY32);
      if Process32First(Snap, Process) then
         repeat
          GetMem(NewItem, SizeOf(TProcessRecord));
          ZeroMemory(NewItem, SizeOf(TProcessRecord));
          NewItem^.ProcessId  := Process.th32ProcessID;
          NewItem^.ParrentPID := Process.th32ParentProcessID;
          lstrcpy(@NewItem^.ProcessName, Process.szExeFile);
          AddItem(List, NewItem);
         until not Process32Next(Snap, Process);
      CloseHandle(Snap);
     end;
end;

很明显,这不会发现任何隐藏进程,所以这个函数只可以用来做探测隐藏进程的参考。

 

通过使用Native API获得进程列表
再深一个层次的扫描我们要通过Native API ZwQuerySystemInformation获得进程列表。虽然在这个级别(ring 0)什么也发现不了,但是我们仍然应该检查一下。(prince注:有点令人费解,原文如下:The next scanning level will be acquisition a list of processes through

ZwQuerySystemInformation (Native API). It is improbable that something will be found out at this level but we should check it

anyway.)

Code:

{
 Acquiring list of processes by using ZwQuerySystemInformation.
}
procedure GetNativeProcessList(var List: PListStruct);
var
 Info: PSYSTEM_PROCESSES;
 NewItem: PProcessRecord;
 Mem: pointer;
begin
  Info := GetInfoTable(SystemProcessesAndThreadsInformation);
  Mem := Info;
  if Info = nil then Exit;
  repeat
   GetMem(NewItem, SizeOf(TProcessRecord));
   ZeroMemory(NewItem, SizeOf(TProcessRecord));
   lstrcpy(@NewItem^.ProcessName,
           PChar(WideCharToString(Info^.ProcessName.Buffer)));
   NewItem^.ProcessId  := Info^.ProcessId;
   NewItem^.ParrentPID := Info^.InheritedFromProcessId;
   AddItem(List, NewItem);
   Info := pointer(dword(info) + info^.NextEntryDelta);
  until Info^.NextEntryDelta = 0;
  VirtualFree(Mem, 0, MEM_RELEASE);
end;


通过进程打开的句柄获得进程列表。
许多隐藏进程无法隐藏他们打开的句柄,因此我们可以通过使用ZwQuerySystemInformation函数枚举打开的句柄来构建进程列表。

Code:

{
  Acquiring the list of processes by using list of opened handles.
  Returns only ProcessId.
}
procedure GetHandlesProcessList(var List: PListStruct);
var
 Info: PSYSTEM_HANDLE_INFORMATION_EX;
 NewItem: PProcessRecord;
 r: dword;
 OldPid: dword;
begin
  OldPid := 0;
  Info := GetInfoTable(SystemHandleInformation);
  if Info = nil then Exit;
  for r := 0 to Info^.NumberOfHandles do
    if Info^.Information[r].ProcessId <> OldPid then
     begin
       OldPid := Info^.Information[r].ProcessId;
       GetMem(NewItem, SizeOf(TProcessRecord));
       ZeroMemory(NewItem, SizeOf(TProcessRecord));
       NewItem^.ProcessId   := OldPid;
       AddItem(List, NewItem);
     end;
  VirtualFree(Info, 0, MEM_RELEASE);
end;

到现在我们已经可能发现一些东西了,但是我们不应该依赖于像隐藏进程一样简单的隐藏句柄的检查结果,尽管有些人甚至忘记隐藏他们。


通过列举创建的窗口来得到进程列表。
可以将那在系统中注册窗口的进程用GetWindowThreadProcessId构建进程列表。

Code:

{
  Acquiring the list of processes by using list of windows.
  Returns only ProcessId.
}
procedure GetWindowsProcessList(var List: PListStruct);

 function EnumWindowsProc(hwnd: dword; PList: PPListStruct): bool; stdcall;
 var
  ProcId: dword;
  NewItem: PProcessRecord;
 begin
  GetWindowThreadProcessId(hwnd, ProcId);
   if not IsPidAdded(PList^, ProcId) then
    begin
     GetMem(NewItem, SizeOf(TProcessRecord));
     ZeroMemory(NewItem, SizeOf(TProcessRecord));
     NewItem^.ProcessId   := ProcId;
     AddItem(PList^, NewItem);
  end;
  Result := true;
 end;

begin
 EnumWindows(@EnumWindowsProc, dword(@List));
end;

几乎没有人会隐藏窗口,因此这种检查可以检测某些进程,但是我们不应该相信这种检测。

直接通过系统调用得到进程列表。
在用户态隐藏进程,一个普遍的做法是使用代码注入(code-injection)技术和在所有进程中拦截ntdll.dll中的ZwQuerySystemInformation函数。
ntdll中的函数实际上对应着系统内核中的函数和系统调用(Windows 2000 中的2Eh中断或者Windows XP中的sysenter指令),因此大多数简单又有效的关于那些用户级的隐藏进程的检测方法就是直接使用系统调用而不是使用API函数。

Windows XP中ZwQuerySystemInformation函数的替代函数看起来是这个样子:

Code:

{
 ZwQuerySystemInformation for Windows XP.
}
Function XpZwQuerySystemInfoCall(ASystemInformationClass: dword;
                                 ASystemInformation: Pointer;
                                 ASystemInformationLength: dword;
                                 AReturnLength: pdword): dword; stdcall;
asm
 pop ebp
 mov eax, $AD
 call @SystemCall
 ret $10
 @SystemCall:
 mov edx, esp
 sysenter
end;


由于不同的系统调用机制,Windows 2000的这部分代码看起来有些不同。

Code:

{
  Системный вызов ZwQuerySystemInformation для Windows 2000.
}
Function Win2kZwQuerySystemInfoCall(ASystemInformationClass: dword;
                                    ASystemInformation: Pointer;
                                    ASystemInformationLength: dword;
                                    AReturnLength: pdword): dword; stdcall;
asm
 pop ebp
 mov eax, $97
 lea edx, [esp + $04]
 int $2E
 ret $10
end;


现在有必要使用上面提到的函数而不是ntdll来枚举系统进程了。实现的代码如下:

Code:

{
  Acquiring the list of processes by use of a direct system call
  ZwQuerySystemInformation.
}
procedure GetSyscallProcessList(var List: PListStruct);
var
 Info: PSYSTEM_PROCESSES;
 NewItem: PProcessRecord;
 mPtr: pointer;
 mSize: dword;
 St: NTStatus;
begin
 mSize := $4000;
 repeat
  GetMem(mPtr, mSize);
  St := ZwQuerySystemInfoCall(SystemProcessesAndThreadsInformation,
                              mPtr, mSize, nil);
  if St = STATUS_INFO_LENGTH_MISMATCH then
    begin
      FreeMem(mPtr);
      mSize := mSize * 2;
    end;
 until St <> STATUS_INFO_LENGTH_MISMATCH;
 if St = STATUS_SUCCESS then
  begin
    Info := mPtr;
    repeat
     GetMem(NewItem, SizeOf(TProcessRecord));
     ZeroMemory(NewItem, SizeOf(TProcessRecord));
     lstrcpy(@NewItem^.ProcessName,
             PChar(WideCharToString(Info^.ProcessName.Buffer)));
     NewItem^.ProcessId  := Info^.ProcessId;
     NewItem^.ParrentPID := Info^.InheritedFromProcessId;
     Info := pointer(dword(info) + info^.NextEntryDelta);
     AddItem(List, NewItem);
    until Info^.NextEntryDelta = 0;
  end;
 FreeMem(mPtr);
end;

这种方法能检测几乎100%的用户态的后门(rootkits),例如hxdef的所有版本(包括黄金版)。

通过分析相关的句柄得到进程列表。
基于枚举句柄的方法。这个方法的实质并不是查找进程打开的句柄,而是查找同该进程相关的其他进程的句柄。这些句柄可以是进程句柄也可以是线程句柄。当找到进程句柄,我们就可以用ZwQueryInformationProcess函数得到进程的PID。对于线程句柄,我们可以通过ZwQueryInformationThread得到进程ID。存在于系统中的所有进程都是由某些进程产生的,因此父进程拥有他们的句柄(除了那些已经被关闭的句柄),对于Win32子系统服务器(csrss.exe)来说所有存在的进程的句柄都是可以访问的。另外,Windows NT大量使用Job objects(prince: 任务对象?姑且这么翻译吧,有不妥的地方请指教),任务对象可以关联进程(比如属于某用户或服务的所有进程),因此当找到任务对象的句柄,我们就可以利用它得到与之关联的所有进程的ID。使用QueryInformationJobObject和信息类的函数JobObjectBasicProcessIdList就可以实现上述功能。利用分析进程相关的句柄得到进程列表的实现代码如下:

Code:

{
 Acquiring the list of processes by analyzing handles in other processes.
}
procedure GetProcessesFromHandles(var List: PListStruct; Processes, Jobs, Threads: boolean);
var
 HandlesInfo: PSYSTEM_HANDLE_INFORMATION_EX;
 ProcessInfo: PROCESS_BASIC_INFORMATION;
 hProcess : dword;
 tHandle: dword;
 r, l     : integer;
 NewItem: PProcessRecord;
 Info: PJOBOBJECT_BASIC_PROCESS_ID_LIST;
 Size: dword;
 THRInfo: THREAD_BASIC_INFORMATION;
begin
 HandlesInfo := GetInfoTable(SystemHandleInformation);
 if HandlesInfo <> nil then
 for r := 0 to HandlesInfo^.NumberOfHandles do
   if HandlesInfo^.Information[r].ObjectTypeNumber in [OB_TYPE_PROCESS, OB_TYPE_JOB, OB_TYPE_THREAD] then
    begin
      hProcess  := OpenProcess(PROCESS_DUP_HANDLE, false,
                               HandlesInfo^.Information[r].ProcessId);
                              
      if DuplicateHandle(hProcess, HandlesInfo^.Information[r].Handle,
                         INVALID_HANDLE_VALUE, @tHandle, 0, false,
                         DUPLICATE_SAME_ACCESS) then
            begin
             case HandlesInfo^.Information[r].ObjectTypeNumber of
               OB_TYPE_PROCESS : begin
                     if Processes and (HandlesInfo^.Information[r].ProcessId = CsrPid) then
                     if ZwQueryInformationProcess(tHandle, ProcessBasicInformation,
                                            @ProcessInfo,
                                            SizeOf(PROCESS_BASIC_INFORMATION),
                                            nil) = STATUS_SUCCESS then
                     if not IsPidAdded(List, ProcessInfo.UniqueProcessId) then
                        begin
                        GetMem(NewItem, SizeOf(TProcessRecord));
                        ZeroMemory(NewItem, SizeOf(TProcessRecord));
                        NewItem^.ProcessId   := ProcessInfo.UniqueProcessId;
                        NewItem^.ParrentPID  := ProcessInfo.InheritedFromUniqueProcessId;
                        AddItem(List, NewItem);
                        end;
                     end;

               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;


当然,用户态隐藏进程的检测方法不止这些,还可以想一些稍微复杂一点的新方法(比如,用SetWindowsHookEx函数对可访问进程的注入和当我们的DLL并成功加载后对进程列表的分析),但是现在我们将用上面提到的方法来解决问题。这些方法的优点是他们可以简单地编程实现,并且除了可以检测到用户态的隐藏进程,还可以检测到少数的在内核态实现的隐藏进程... 要实现真正可靠的进程隐藏工具我们应该使用Windows未公开的内核数据结构编写内核驱动程序。


内核态(Ring 0)的检测
恭喜你,我们终于开始进行内核态隐藏进程的分析。内核态的检测方法同用户态的检测方法的主要区别是所有的进程列表都没有使用API调用而是直接来自系统内部数据结构。在这些检测方法下隐藏进程要困难得多,因为它们都是基于同Windows内核相同的原理实现的,并且从这些内核数据结构中删除进程将导致该进程完全失效。

内核中的进程是什么?每一个进程都有自己的地址空间,描述符,线程等,内核的数据结构就涉及这些东西。每一个进程都是由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)。这些偏移随着Windows系统版本的不同而不同,因此我们要在进程检测程序的代码中进行区分后得到他们正确的值(附录)。

任何一个通过API截取方式隐藏的进程都将被上面这个方法检测出来。但是如果进程是通过DKOM(直接处理内核对象 - Direct Kernel Object Manipulation)方式隐藏,那这个方法就失效了,因为这种进程都被从进程链表中删掉了。

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

对付这种隐藏进程(俄文翻译kao注:这个地方原文写的比较模糊,作者大概的意思应该是“使用DKOM的方式检测隐藏进程”)的其中一种检测方式是通过调度程序(scheduler)中的线程列表来得到进程列表。Windows 2000有三个维护线程的双向链表(KiWaitInListHead, KiWaitOutListHead, KiDispatcherReadyListHead)。前面两个链表包含等待某种事件的线程,最后面的链表包含的是等待执行的线程。我们处理这些链表,根据线程链表结构ETHREAD中的偏移就可以得到一个线程的ETHREAD指针(俄文翻译kao注:原文中这句话实在是太难懂了,希望我翻译的正确)。这个结构包括了很多进程相关指针,也就是结构_KPROCESS *Process(0x44, 0x150)和结构_EPROCESS *ThreadsProcess(0x22C, 这仅是Windows 2000中的偏移量)。前面两个指针对于一个线程的功能性没有任何影响,因此可以很容易修改它们来隐藏进程。相反,第三个指针是当切换地址空间时调度程序(schedler)使用的指针,所以这个指针是不能修改的。我们就用它来找到拥有某个线程的进程。

Klister就是使用了这种检测方法,它的最大的缺点就是只能在Windows 2000平台上工作(但是在这个平台上某个补丁包也会让它失效)。导致这个情况发生的原因就是这种程序使用了硬编码的线程链表地址,而在每个补丁包中这些地址可能都是不同的。

在程序中使用硬编码地址是很糟糕的解决方案,操作系统的升级就会使你的程序无法正常工作,要尽量避免使用这种检测方法。所以应该通过分析那些使用了这些链表的内核函数来动态地得到它们的地址。

首先我们试试看在Windows 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;
}


在Windows 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;
}

不幸的是,Windows XP内核完全不同于Windows 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;
}

最困难的是查找KiDispatcherReadyListHead。主要的问题是KiDispatcherReadyListHead的地址并没有被任何一个导出的函数使用。因此就要

用更加复杂的搜索算法搞定它。就从KiDispatchInterrupt函数开始,我们感兴趣的地方只有这里:

Code:

.text:00404E72                 mov     byte ptr [edi+50h], 1
.text:00404E76                 call    sub_404C5A
.text:00404E7B                 mov     cl, 1
.text:00404E7D                 call    sub_404EB9


这段代码中的第一个函数调用指向的就是包含KiDispatcherReadyListHead引用的函数。尽管如此,搜索KiDispatcherReadyListHead的地址却

变的更加复杂,因为这个函数的相关代码在Windows XP SP1和SP2中是不同的。在SP2中它是这个样子:

Code:

.text:00404CCD                 add     eax, 60h
.text:00404CD0                 test    bl, bl
.text:00404CD2                 lea     edx, KiDispatcherReadyListHead[ecx*8]
.text:00404CD9                 jnz     loc_401F12
.text:00404CDF                 mov     esi, [edx+4]


And in SP1:
SP1中是这样的:

Code:

.text:004180FE                 add     eax, 60h
.text:00418101                 cmp     [ebp+var_1], bl
.text:00418104                 lea     edx, KiDispatcherReadyListHead[ecx*8]
.text:0041810B                 jz      loc_418760
.text:00418111                 mov     esi, [edx]

仅仅查找一个“lea”指令是不可靠的,因此我们也应该检查“lea”后面的指令(LDasm中的IsRelativeCmd函数)。搜索

KiDispatcherReadyListHead的全部代码如下:

Code:

void XPGetKiDispatcherReadyListHead()
{
   PUCHAR cPtr, pOpcode;
   PUCHAR CallAddr = NULL;
   ULONG Length;

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

      if (!Length) return;

      if (*pOpcode == 0xE8 && *(PUSHORT)(pOpcode + 5) == 0x01B1)
      {
         CallAddr = (PUCHAR)(*(PULONG)(pOpcode + 1) + (ULONG)cPtr + Length);
         break;
      }
   }

   if (!CallAddr || !MmIsAddressValid(CallAddr)) return;

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

      if (!Length) return;

      if (*(PUSHORT)pOpcode == 0x148D && *(pOpcode + 2) == 0xCD && IsRelativeCmd(pOpcode + 7))
      {
         KiDispatcherReadyListHead = *(PLIST_ENTRY *)(pOpcode + 3);
         break;
      }
   }

   return;
}


找到线程链表地址之后我们就可以非常简单地枚举出那些进程了,代码如下:

Code:

void ProcessListHead(PLIST_ENTRY ListHead)
{
   PLIST_ENTRY Item;

   if (ListHead)
   {
      Item = ListHead->Flink;

      while (Item != ListHead)
      {
         CollectProcess(*(PEPROCESS *)((ULONG)Item + WaitProcOffset));
         Item = Item->Flink;
      }
   }

   return;
}

CollectProcess是一个非常有用的函数,它可以增加一个进程到进程列表中去。


通过拦截系统调用得到进程列表。

任何一个进程都要通过API来和系统进行交互,而大多数交互都通过系统调用传递给了内核。当然,进程也可以不使用任何API而存在,但是这

样一来它也就不能做任何有用(或有害)的事情。一般而言,我们的思路是使用系统调用管理器拦截系统调用,然后得到管理器中当前进程的

EPROCESS指针。应该在某段时间收集指针列表,这个表不会包含信息收集时没有使用任何系统调用的进程(比如,进程的线程都处于等待状态

)。

Windows 2000平台使用2Eh中断进行系统调用,因此我们需要修改IDT中的相应的中断描述符来拦截系统调用,这就要用sidt指令得到IDT在内存

中的位置。该指令返回这样一个结构:

Code:

typedef struct _Idt
{
   USHORT Size;
   ULONG  Base;
} TIdt;


修改2Eh中断向量的代码如下:

Code:

void Set2kSyscallHook()
{
   TIdt Idt;
   __asm
   {
      pushad
      cli
      sidt [Idt]
      mov esi, NewSyscall
      mov ebx, Idt.Base
      xchg [ebx + 0x170], si
      rol esi, 0x10
      xchg [ebx + 0x176], si
      ror esi, 0x10
      mov OldSyscall, esi
      sti
      popad
   }
}


当然在卸载驱动之前还要保存原始状态的信息:

Code:

void Win2kSyscallUnhook()
{
   TIdt Idt;
   __asm
   {
      pushad
      cli
      sidt [Idt]
      mov esi, OldSyscall
      mov ebx, Idt.Base
      mov [ebx + 0x170], si
      rol esi, 0x10
      mov [ebx + 0x176], si
      sti
      xor eax, eax
      mov OldSyscall, eax
      popad
   }
}


Windows XP使用sysenter/sysexit指令(出现在Pentium 2处理器中)实现系统调用。这些指令的功能由model-specific registers(MSR)控制

。系统调用管理器的地址保存在MSR寄存器,SYSENTER_EIP_MSR(0x176)中。用rdmsr指令读取MSR寄存器,同时设置ECX = 要读取的寄存器的号

码,结果保存在两个积存器EDX:EAX中。在我们这里,SYSENTER_EIP_MSR积存器是32位积存器,所以EDX为0,EAX内是系统调用管理器的地址。

同样地,我们也可以用wrmsr指令写MSR积存器。有一个地方需要注意:当写32位MSR积存器的时候,EDX应该被清空,否则将引起异常并且导致

系统立即崩溃。

考虑到所有的事情之后,替代系统调用管理器的代码如下:

Code:

void SetXpSyscallHook()
{
   __asm
   {
      pushad
      mov ecx, 0x176
      rdmsr
      mov OldSyscall, eax
      mov eax, NewSyscall
      xor edx, edx
      wrmsr
      popad
   }
}


恢复原始的系统调用管理器代码:

Code:

void XpSyscallUnhook()
{
   __asm
   {
      pushad
      mov ecx, 0x176
      mov eax, OldSyscall
      xor edx, edx
      wrmsr
      xor eax, eax
      mov OldSyscall, eax
      popad
   }
}


Windows XP的另外一个特性是它既可以使用sysenter也可以使用int 2Eh来进行系统调用,所以我们要替换这两种情况下的系统调用管理器。

我们的新的系统调用管理器应该得到当前进程的EPROCESS指针,并且如果是一个新的进程,我们要把这个新的进程加到我们的进程列表中。
新的系统调用管理器代码如下:

Code:

void __declspec(naked) NewSyscall()
{
   __asm
   {
      pushad
      pushfd
      push fs
      mov di, 0x30
      mov fs, di
      mov eax, fs:[0x124]
      mov eax, [eax + 0x44]
      push eax
      call CollectProcess
      pop fs
      popfd
      popad
      jmp OldSyscall
   }
}


得到进程列表的这段代码应该在某个时间段内工作,所以我们有这样的问题:如果在列表中的进程被删除掉,在随后的时间内我们将保留一些

无效指针,结果就是检测隐藏进程失败或者导致系统BSOD。解决这个问题的办法是,用PsSetCreateProcessNotifyRoutine函数注册我们的回调

函数,这个回调函数将会在系统创建或者销毁一个进程的时候被调用。当进程被销毁时,我们也应该把它从我们的表中删除掉。
回调函数的原型如下:

Code:

VOID
(*PCREATE_PROCESS_NOTIFY_ROUTINE) (
    IN HANDLE  ParentId,
    IN HANDLE  ProcessId,
    IN BOOLEAN  Create
    );

安装回调函数的代码如下:

Code:

 PsSetCreateProcessNotifyRoutine (NotifyRoutine, FALSE);

取消回调函数的代码:

Code:

 PsSetCreateProcessNotifyRoutine (NotifyRoutine, TRUE);


这里有一个问题,回调函数总是在系统被销毁的时候创建,因此我们不可能直接在这个回调函数中删除进程列表中的相应进程。这样我们就要

用系统的work items,首先调用IoAllocateWorkItem函数为work item分配内存,然后调用IoQueueWorkItem函数(俄文翻译者kao注:这一句我

不太确定...)将任务放置到工作线程队列中。在处理过程中我们不仅仅从进程列表中删除掉已经终止的进程,而且还要加入新创建的线程。处

理代码如下:

Code:

void WorkItemProc(PDEVICE_OBJECT DeviceObject, PWorkItemStruct Data)
{
   KeWaitForSingleObject(Data->pEPROCESS, Executive, KernelMode, FALSE, NULL);
 
   DelItem(&wLastItem, Data->pEPROCESS);

   ObDereferenceObject(Data->pEPROCESS);

   IoFreeWorkItem(Data->IoWorkItem);

   ExFreePool(Data);

   return;
}


void NotifyRoutine(IN HANDLE  ParentId,
                   IN HANDLE  ProcessId,
                   IN BOOLEAN Create)
{
   PEPROCESS       process;
   PWorkItemStruct Data;

   if (Create)
   {
      PsLookupProcessByProcessId(ProcessId, &process);

      if (!IsAdded(wLastItem, process)) AddItem(&wLastItem, process);

      ObDereferenceObject(process);

   } else
   {
      process = PsGetCurrentProcess();
      
      ObReferenceObject(process);

      Data = ExAllocatePool(NonPagedPool, sizeof(TWorkItemStruct));

      Data->IoWorkItem = IoAllocateWorkItem(deviceObject);
     
      Data->pEPROCESS  = process;

      IoQueueWorkItem(Data->IoWorkItem, WorkItemProc, DelayedWorkQueue, Data);
   }

   return;
}


这是一个相对可靠的隐藏进程的检测方式,然而虽然没有进程能够不倚赖系统调用,但还是有一些进程可以在很长一段时间处于等待状态不进

行系统调用,我们无法检测出这样的进程。

只要想做,躲避开这种检测方式还是很容易的。想要做到这一点,那就需要改变隐藏进程的系统调用方式(重定向到另外一个中断或者GDT中的

调用门)。在Windows XP下做这个工作是相当简单的,因为可以给ntdll.dll中的KiFastSystemCall函数打补丁和创建一个相应的系统调用门。

在Windows 2000平台下就稍微有点难度了,因为int 2Eh调用分散遍及整个ntdll,但是找到并patch所有的地方也并不是很复杂。综上所述,依

赖于这种检测方式可不是聪明之举。

通过遍历句柄表得到进程列表。
如果你曾经尝试过利用删除PsActiveProcesses链表中的进程节点来隐藏进程,可能你会注意到当你调用ZwQuerySystemInformation函数枚举句

柄的时候,隐藏进程的句柄也会被枚举出来,并且还能被检测出它的ProcessId。这是因为为了方便枚举句柄,所有的句柄表都是由一个双向链

表HandleTableList维护的。Windows 2000下HANDLE_TABLE结构在链表中的偏移等于0x054,Windows XP下为0x01C,链表由

HandleTableListHead开始。HANDLE_TABLE结构包括它的宿主进程的指针(QuotaProcess),Windows 2000下这个偏移等于0x00C,Windows XP

下这个偏移为0x004。通过遍历这个句柄链表我们就可以构建进程列表了。

首先我们得找到HandleTableListHead。反汇编内核显示它的引用定位在函数的深处,所以前面我们用过的反汇编代码的方法已经不能在这里使

用了。要找到HeadleTableListHead,我们要注意到HandleTableListHead是一个全局的内核变量,因此它一定是在内核文件的某一个段

(Section)里面,并且HandleTableList的其他成员是在动态分配的内存中,所以总是受到内核地址空间的限制。根据这些,我们需要得到任

何一个进程的HandleTable的指针,然后遍历链表直到找到定位在这个内核地址空间的成员,那么这个成员就是HandleTableListHead了。

我们使用ZwQuerySystemInformation和SystemModuleInformation类计算系统内核的基址和大小。它将返回一个所有已经加载了的模块的描述符

表,并且这个表的第一个成员始终是"system"。综上所述,查找HandleTableListHead的代码如下:

Code:

void GetHandleTableListHead()
{
   PSYSTEM_MODULE_INFORMATION_EX Info = GetInfoTable(SystemModuleInformation);
   ULONG NtoskrnlBase = (ULONG)Info->Modules[0].Base;
   ULONG NtoskrnlSize = Info->Modules[0].Size;
   PHANDLE_TABLE HandleTable = *(PHANDLE_TABLE *)((ULONG)PsGetCurrentProcess() + HandleTableOffset);
   PLIST_ENTRY HandleTableList = (PLIST_ENTRY)((ULONG)HandleTable + HandleTableListOffset);
   PLIST_ENTRY CurrTable;

   ExFreePool(Info);

   for (CurrTable = HandleTableList->Flink;
        CurrTable != HandleTableList;
        CurrTable = CurrTable->Flink)
   {
      if ((ULONG)CurrTable > NtoskrnlBase && (ULONG)CurrTable < NtoskrnlBase + NtoskrnlSize)
      {
         HandleTableListHead = CurrTable;
         break;
      }
   }  
}

这段代码是非常通用的,它可以运行于任何Windows NT版本的系统上,并且不仅可以用来查找HandleTableListHead,也可以用于其他类似的结

构。

得到HandleTableListHead地址后我们就可以遍历句柄表并基于这些信息来构建进程列表了。

Code:

void ScanHandleTablesList()
{
   PLIST_ENTRY CurrTable;
   PEPROCESS QuotaProcess;

   for (CurrTable =  HandleTableListHead->Flink;
        CurrTable != HandleTableListHead;
        CurrTable =  CurrTable->Flink)
   {
      QuotaProcess = *(PEPROCESS *)((PUCHAR)CurrTable - HandleTableListOffset + QuotaProcessOffset);
      if (QuotaProcess) CollectProcess(QuotaProcess);
   }
}

F-Secure Black Light和KProcCheck的最后一个版本用的就是这种检测方法。我想你将会很轻松地找到对付这种检测的方法。

通过扫描PspCidTable得到进程列表。

有一件有趣的事情需要注意:如果仅仅把进程节点从PsActiveProcesses链表中删除,它不能够防止使用API函数OpenProcess打开进程。这样就

有一种检测进程的方法就是尝试穷举Pid然后调用OpenProcess。我不推荐这个方法,因为它没有任何优点,我甚至想说这是一种“狗屁”方案

。不过它的存在意味着在系统中除了通过PsActiveProcesses得到进程列表之外还可以通过调用OpenProcess。当穷举ProcessId的时候我们会注

意到一个进程可以被几个不同的Pid打开,这暗示可能存在着有点像HANDLE_TABLE的另一个进程列表。为了证明这个的猜想,我们来看看

ZwOpenProcess函数:

Code:

PAGE:0049D59E ; NTSTATUS __stdcall NtOpenProcess(PHANDLE ProcessHandle, ACCESS_MASK DesiredAccess,
                                                 POBJECT_ATTRIBUTES ObjectAttributes,PCLIENT_ID ClientId)
PAGE:0049D59E                 public NtOpenProcess
PAGE:0049D59E NtOpenProcess   proc near            
PAGE:0049D59E
PAGE:0049D59E ProcessHandle   = dword ptr  4
PAGE:0049D59E DesiredAccess   = dword ptr  8
PAGE:0049D59E ObjectAttributes= dword ptr  0Ch
PAGE:0049D59E ClientId        = dword ptr  10h
PAGE:0049D59E
PAGE:0049D59E                 push    0C4h
PAGE:0049D5A3                 push    offset dword_413560 ; int
PAGE:0049D5A8                 call    sub_40BA92
PAGE:0049D5AD                 xor     esi, esi
PAGE:0049D5AF                 mov     [ebp-2Ch], esi
PAGE:0049D5B2                 xor     eax, eax
PAGE:0049D5B4                 lea     edi, [ebp-28h]
PAGE:0049D5B7                 stosd
PAGE:0049D5B8                 mov     eax, large fs:124h
PAGE:0049D5BE                 mov     al, [eax+140h]
PAGE:0049D5C4                 mov     [ebp-34h], al
PAGE:0049D5C7                 test    al, al
PAGE:0049D5C9                 jz      loc_4BE034
PAGE:0049D5CF                 mov     [ebp-4], esi
PAGE:0049D5D2                 mov     eax, MmUserProbeAddress
PAGE:0049D5D7                 mov     ecx, [ebp+8]
PAGE:0049D5DA                 cmp     ecx, eax
PAGE:0049D5DC                 jnb     loc_520CDE
PAGE:0049D5E2 loc_49D5E2:                            
PAGE:0049D5E2                 mov     eax, [ecx]
PAGE:0049D5E4                 mov     [ecx], eax
PAGE:0049D5E6                 mov     ebx, [ebp+10h]
PAGE:0049D5E9                 test    bl, 3
PAGE:0049D5EC                 jnz     loc_520CE5
PAGE:0049D5F2 loc_49D5F2:                          
PAGE:0049D5F2                 mov     eax, MmUserProbeAddress
PAGE:0049D5F7                 cmp     ebx, eax
PAGE:0049D5F9                 jnb     loc_520CEF
PAGE:0049D5FF loc_49D5FF:                          
PAGE:0049D5FF                 cmp     [ebx+8], esi
PAGE:0049D602                 setnz   byte ptr [ebp-1Ah]
PAGE:0049D606                 mov     ecx, [ebx+0Ch]
PAGE:0049D609                 mov     [ebp-38h], ecx
PAGE:0049D60C                 mov     ecx, [ebp+14h]
PAGE:0049D60F                 cmp     ecx, esi
PAGE:0049D611                 jz      loc_4CCB88
PAGE:0049D617                 test    cl, 3
PAGE:0049D61A                 jnz     loc_520CFB
PAGE:0049D620 loc_49D620:                          
PAGE:0049D620                 cmp     ecx, eax
PAGE:0049D622                 jnb     loc_520D0D
PAGE:0049D628 loc_49D628:
PAGE:0049D628                 mov     eax, [ecx]
PAGE:0049D62A                 mov     [ebp-2Ch], eax
PAGE:0049D62D                 mov     eax, [ecx+4]
PAGE:0049D630                 mov     [ebp-28h], eax
PAGE:0049D633                 mov     byte ptr [ebp-19h], 1
PAGE:0049D637 loc_49D637:                    
PAGE:0049D637                 or      dword ptr [ebp-4], 0FFFFFFFFh
PAGE:0049D63B loc_49D63B:                    
PAGE:0049D63B                                      
PAGE:0049D63B                 cmp     byte ptr [ebp-1Ah], 0
PAGE:0049D63F                 jnz     loc_520D34
PAGE:0049D645 loc_49D645:                          
PAGE:0049D645                 mov     eax, PsProcessType
PAGE:0049D64A                 add     eax, 68h
PAGE:0049D64D                 push    eax
PAGE:0049D64E                 push    dword ptr [ebp+0Ch]
PAGE:0049D651                 lea     eax, [ebp-0D4h]
PAGE:0049D657                 push    eax
PAGE:0049D658                 lea     eax, [ebp-0B8h]
PAGE:0049D65E                 push    eax
PAGE:0049D65F                 call    SeCreateAccessState
PAGE:0049D664                 cmp     eax, esi
PAGE:0049D666                 jl      loc_49D718
PAGE:0049D66C                 push    dword ptr [ebp-34h] ; PreviousMode
PAGE:0049D66F                 push    ds:stru_5B6978.HighPart
PAGE:0049D675                 push    ds:stru_5B6978.LowPart ; PrivilegeValue
PAGE:0049D67B                 call    SeSinglePrivilegeCheck
PAGE:0049D680                 test    al, al
PAGE:0049D682                 jnz     loc_4AA7DB
PAGE:0049D688 loc_49D688:                        
PAGE:0049D688                 cmp     byte ptr [ebp-1Ah], 0
PAGE:0049D68C                 jnz     loc_520D52
PAGE:0049D692                 cmp     byte ptr [ebp-19h], 0
PAGE:0049D696                 jz      loc_4CCB9A
PAGE:0049D69C                 mov     [ebp-30h], esi
PAGE:0049D69F                 cmp     [ebp-28h], esi
PAGE:0049D6A2                 jnz     loc_4C1301
PAGE:0049D6A8                 lea     eax, [ebp-24h]
PAGE:0049D6AB                 push    eax
PAGE:0049D6AC                 push    dword ptr [ebp-2Ch]
PAGE:0049D6AF                 call    PsLookupProcessByProcessId
PAGE:0049D6B4 loc_49D6B4:                          


正如你看到的,这段代码拷贝给定的指针,检查是否指向用户地址空间,核对访问权限和是否有“SetDebugPrivilege”的权限,然后从

CLIENT_ID结构中找到ProcessId并传递给PsLookupProcessByProcessId函数,PsLookupProcessByProcessId的功能是得到ProcessId的EPROCESS

。函数的其余部分对我们来说没什么用,现在我们来看看PsLookupProcessByProcessId:

Code:

PAGE:0049D725                 public PsLookupProcessByProcessId
PAGE:0049D725 PsLookupProcessByProcessId proc near  
PAGE:0049D725                                      
PAGE:0049D725
PAGE:0049D725 ProcessId       = dword ptr  8
PAGE:0049D725 Process         = dword ptr  0Ch
PAGE:0049D725
PAGE:0049D725                 mov     edi, edi
PAGE:0049D727                 push    ebp
PAGE:0049D728                 mov     ebp, esp
PAGE:0049D72A                 push    ebx
PAGE:0049D72B                 push    esi
PAGE:0049D72C                 mov     eax, large fs:124h
PAGE:0049D732                 push    [ebp+ProcessId]
PAGE:0049D735                 mov     esi, eax
PAGE:0049D737                 dec     dword ptr [esi+0D4h]
PAGE:0049D73D                 push    PspCidTable
PAGE:0049D743                 call    ExMapHandleToPointer
PAGE:0049D748                 mov     ebx, eax
PAGE:0049D74A                 test    ebx, ebx
PAGE:0049D74C                 mov     [ebp+ProcessId], STATUS_INVALID_PARAMETER
PAGE:0049D753                 jz      short loc_49D787
PAGE:0049D755                 push    edi
PAGE:0049D756                 mov     edi, [ebx]
PAGE:0049D758                 cmp     byte ptr [edi], 3
PAGE:0049D75B                 jnz     short loc_49D77A
PAGE:0049D75D                 cmp     dword ptr [edi+1A4h], 0
PAGE:0049D764                 jz      short loc_49D77A
PAGE:0049D766                 mov     ecx, edi
PAGE:0049D768                 call    sub_4134A9
PAGE:0049D76D                 test    al, al
PAGE:0049D76F                 jz      short loc_49D77A
PAGE:0049D771                 mov     eax, [ebp+Process]
PAGE:0049D774                 and     [ebp+ProcessId], 0
PAGE:0049D778                 mov     [eax], edi
PAGE:0049D77A loc_49D77A:                                                                
PAGE:0049D77A                 push    ebx
PAGE:0049D77B                 push    PspCidTable
PAGE:0049D781                 call    ExUnlockHandleTableEntry
PAGE:0049D786                 pop     edi
PAGE:0049D787 loc_49D787:                          
PAGE:0049D787                 inc     dword ptr [esi+0D4h]
PAGE:0049D78D                 jnz     short loc_49D79A
PAGE:0049D78F                 lea     eax, [esi+34h]
PAGE:0049D792                 cmp     [eax], eax
PAGE:0049D794                 jnz     loc_52388A
PAGE:0049D79A loc_49D79A:                                                            
PAGE:0049D79A                 mov     eax, [ebp+ProcessId]
PAGE:0049D79D                 pop     esi
PAGE:0049D79E                 pop     ebx
PAGE:0049D79F                 pop     ebp
PAGE:0049D7A0                 retn    8


以上我们所看到的,证实了存在像HANDLE_TABLE一样组织结构的第2个进程列表。这个表叫做PspCidTable,它包括进程和线程的列表,

PsLookupProcessThreadByCid函数和PsLookupThreadByThreadId函数都用到了这个表。我们看到,句柄和句柄表的指针被传递给了

ExMapHandleToPointer函数,该函数(在句柄有效的情况下)返回一个指向描述给定句柄的表的一个元素 - HANDLE_TABLE_ENTRY。当我们用

PDBdump分析完ntoskrnl.pdb并且得到分析日志后,会得到如下结果:

Code:

struct _HANDLE_TABLE_ENTRY {
 // static data ------------------------------------
 // non-static data --------------------------------
  /*<thisrel this+0x0>*/ /*|0x4|*/ void* Object;
  /*<thisrel this+0x0>*/ /*|0x4|*/ unsigned long ObAttributes;
  /*<thisrel this+0x0>*/ /*|0x4|*/ struct _HANDLE_TABLE_ENTRY_INFO* InfoTable;
  /*<thisrel this+0x0>*/ /*|0x4|*/ unsigned long Value;
  /*<thisrel this+0x4>*/ /*|0x4|*/ unsigned long GrantedAccess;
  /*<thisrel this+0x4>*/ /*|0x2|*/ unsigned short GrantedAccessIndex;
  /*<thisrel this+0x6>*/ /*|0x2|*/ unsigned short CreatorBackTraceIndex;
  /*<thisrel this+0x4>*/ /*|0x4|*/ long NextFreeTableEntry;
  };// <size 0x8>


We can recover HANDLE_TABLE_ENTRY structure from this:
我们还原一下HANDLE_TABLE_ENTRY结构的C代码:

Code:

typedef struct _HANDLE_TABLE_ENTRY
{
    union
    {
        PVOID                    Object;
        ULONG                    ObAttributes;
        PHANDLE_TABLE_ENTRY_INFO InfoTable;
        ULONG                    Value;
    };

    union
   {
       union
       {
          ACCESS_MASK GrantedAccess;
          struct
          {
              USHORT GrantedAccessIndex;
              USHORT CreatorBackTraceIndex;
          };
       };

       LONG NextFreeTableEntry;
   };
} HANDLE_TABLE_ENTRY, *PHANDLE_TABLE_ENTRY;


怎么使用它呢?首先,我们比较感兴趣的是Object域的内容,它是被句柄描述的目标指针和这个表的给定元素的用法标志(我将稍后解释这句

话)。GrantedAccess域指定了通过这个句柄对目标的访问权限许可,这个很有趣。比如,以只读方式打开一个文件,修改这个域之后就可以写

这个文件了。这个方法可以用在对一些正在被读/写的文件的访问上(比如,正在被其他进程锁定的文件)。应该回到我们的问题上来了 - 通

过对PspCidTable的分析得到进程句柄列表。

要分析它我们得了解句柄表的格式,这样才能遍历这个列表。在这个地方Windows 2000和Windows XP有着巨大的不同。由于句柄表格式不尽相

同,所以我们应该把操作系统分类进行分析。

因为Windows 2000的句柄表相对简单一些,所以我们先分析它。先来看看ExMapHandleToPointer函数:

Code:

PAGE:00493285 ExMapHandleToPointer proc near        
PAGE:00493285                                      
PAGE:00493285
PAGE:00493285 HandleTable     = dword ptr  8
PAGE:00493285 Handle          = dword ptr  0Ch
PAGE:00493285
PAGE:00493285                 push    esi
PAGE:00493286                 push    [esp+Handle]
PAGE:0049328A                 push    [esp+4+HandleTable]
PAGE:0049328E                 call    ExpLookupHandleTableEntry
PAGE:00493293                 mov     esi, eax
PAGE:00493295                 test    esi, esi
PAGE:00493297                 jz      short loc_4932A9
PAGE:00493299                 push    esi
PAGE:0049329A                 push    [esp+4+HandleTable]
PAGE:0049329E                 call    ExLockHandleTableEntry
PAGE:004932A3                 neg     al
PAGE:004932A5                 sbb     eax, eax
PAGE:004932A7                 and     eax, esi
PAGE:004932A9 loc_4932A9:                          
PAGE:004932A9                 pop     esi
PAGE:004932AA                 retn    8
PAGE:004932AA ExMapHandleToPointer endp


这里我们调用搜索HANDLE_TABLE的函数ExMapHandleToPointer以及设置Lock Bit的ExLockHandleTableEntry函数。要了解句柄表的内部结构我

们必须反汇编这些函数。先从ExpLookupHandleTableEntry函数开始:

Code:

PAGE:00493545 ExpLookupHandleTableEntry proc near    
PAGE:00493545                                      
PAGE:00493545
PAGE:00493545 HandleTable     = dword ptr  0Ch
PAGE:00493545 Handle          = dword ptr  10h
PAGE:00493545
PAGE:00493545                 push    esi
PAGE:00493546                 push    edi
PAGE:00493547                 mov     edi, [esp+Handle]
PAGE:0049354B                 mov     eax, 0FFh
PAGE:00493550                 mov     ecx, edi
PAGE:00493552                 mov     edx, edi
PAGE:00493554                 mov     esi, edi
PAGE:00493556                 shr     ecx, 12h
PAGE:00493559                 shr     edx, 0Ah
PAGE:0049355C                 shr     esi, 2
PAGE:0049355F                 and     ecx, eax
PAGE:00493561                 and     edx, eax
PAGE:00493563                 and     esi, eax
PAGE:00493565                 test    edi, 0FC000000h
PAGE:0049356B                 jnz     short loc_49358A
PAGE:0049356D                 mov     eax, [esp+HandleTable]
PAGE:00493571                 mov     eax, [eax+8]
PAGE:00493574                 mov     ecx, [eax+ecx*4]
PAGE:00493577                 test    ecx, ecx
PAGE:00493579                 jz      short loc_49358A
PAGE:0049357B                 mov     ecx, [ecx+edx*4]
PAGE:0049357E                 test    ecx, ecx
PAGE:00493580                 jz      short loc_49358A
PAGE:00493582                 lea     eax, [ecx+esi*8]
PAGE:00493585 loc_493585:                            
PAGE:00493585                 pop     edi
PAGE:00493586                 pop     esi
PAGE:00493587                 retn    8
PAGE:0049358A loc_49358A:                                                      
PAGE:0049358A                 xor     eax, eax
PAGE:0049358C                 jmp     short loc_493585
PAGE:0049358C ExpLookupHandleTableEntry endp


除此之外,我们来看看从ntoskrnl.pdb中得到的HANDLE_TABLE结构:

Code:

struct _HANDLE_TABLE {
  // static data ------------------------------------
  // non-static data --------------------------------
  /*<thisrel this+0x0>*/ /*|0x4|*/ unsigned long Flags;
  /*<thisrel this+0x4>*/ /*|0x4|*/ long HandleCount;
  /*<thisrel this+0x8>*/ /*|0x4|*/ struct _HANDLE_TABLE_ENTRY*** Table;
  /*<thisrel this+0xc>*/ /*|0x4|*/ struct _EPROCESS* QuotaProcess;
  /*<thisrel this+0x10>*/ /*|0x4|*/ void* UniqueProcessId;
  /*<thisrel this+0x14>*/ /*|0x4|*/ long FirstFreeTableEntry;
  /*<thisrel this+0x18>*/ /*|0x4|*/ long NextIndexNeedingPool;
  /*<thisrel this+0x1c>*/ /*|0x38|*/ struct _ERESOURCE HandleTableLock;
  /*<thisrel this+0x54>*/ /*|0x8|*/ struct _LIST_ENTRY HandleTableList;
  /*<thisrel this+0x5C>*/ /*|0x10|*/ struct _KEVENT HandleContentionEvent;
}; // <size 0x6c>


根据这些数据我们用C语言还原这个结构:

Code:

typedef struct _WIN2K_HANDLE_TABLE
{
   ULONG                 Flags;
   LONG                  HandleCount;
   PHANDLE_TABLE_ENTRY **Table;
   PEPROCESS             QuotaProcess;
   HANDLE                UniqueProcessId;
   LONG             &

posted on 2008-10-15 13:11  五维思考  阅读(1001)  评论(0编辑  收藏  举报

QQ群:1. 全栈码农【346906288】2. VBA/VSTO【2660245】