【原文标题】: FUTo
【原文地址】: http://www.openrce.org/articles/full_view/19
【原文作者】: Peter Silberman,C.H.A.O.S
【译    者】: Ptero
【时    间】: 2007.08.04
【译者声明】: 本人第一次翻译文章,有不足之处大家见谅啊,呵呵。


前言

自从FU出现以来,rootkit世界已经从安装系统钩子转变成为隐藏自身的存在。由于这种攻击方式的改变,产生了一种新的防御手段。新的rootkit检测算法——正如BlackLight所使用的——尝试找出rootkit所隐藏的东西,而不是仅仅检测出rookit钩子的存在。本文将要讨论一种被Blacklight和IceSword使用的检测隐藏进程的算法。本文也会指出现行的检测rootkit这个领域的缺陷,并引入一种更完备的以FUTo作为原型的窃取技术。


感谢:(不翻译了)

Peter would like to thank bugcheck, skape, thief, pedram, F-Secure for doing great research, and all the nologin/research'ers who encourage mind growth. 

C.H.A.O.S. would like to thank Amy, Santa (this work was three hours on Christmas day), lonerancher, Pedram, valerino, and HBG Unit.


绪论

在过去的一两年里,在rootkit世界里出现了几项重大的发展。最近的一些具有里程碑意义的事件包括FU rootkit,它使用了直接操作内核对象技术(DKOM);还有VICE,第一个检测rootkit的程序;Sysinternals的Rootit Revealer以及F-Secure的Blacklight,第一个检测Windows rootkit的主流工具;还有最近出现的Shadow Walker,一个hook内存管理以便隐藏自身的rootkit。

下面进入Blacklight和IceSword的世界。作者决定研究被Blacklight和IceSword同时使用的算法,因为许多人认为这两个工具是这个领域中检测rootkit的最佳工具。芬兰安全公司F-Secure研发出的Blacklight,主要被用于检测隐藏进程。它从不尝试去检测系统钩子;它仅仅关注隐藏进程。IceSword使用了一种和Blacklight很相似的方法。与Blacklight不同的是,这是一个更加强大的工具,它能够让用户查看哪些系统调用被hook了,哪些驱动被隐藏了,以及哪些TCP/UDP端口被打开了,而用普通程序,例如netstat,则无法查出来。


Blicklight

本文主要关注Blacklight,因为它的算法是本文的研究重点。而且,在研究过Blacklight之后,就会发现,IceSword显然使用了一种非常相似的算法。因此,如果找到Blacklight的一个缺陷,那么它很有可能在IceSword上同样存在。

Blacklight使用用户模式来检测进程。尽管很简单,它的算法有效得令人惊奇。Blacklight在开始创建TLS callback table的时候使用了一些非常强健的反调试技术。Blacklight的TLS回调函数使用了在主进程对象完全被创建之前让主进程分叉的方法,试图以此来迷惑调试器。这是可行的,因为TLS回调发生在进程完全初始化之前。Blacklight还另有反调试手段,可以检测出附加到其进程上面的调试器。与其绕过TLS回调函数以及作一些其它修改来破解那些反调试手段,还不如把TLS回调例程给禁用掉。为了达到这个目的,作者使用了LordPE工具。LordPE能够让用户修改PE文件。作者使用这个程序将TLS callback table全部清0。这样就阻止了例程的分叉,作者也就可以使用API Monitor了。需要注意的是,禁用了回调例程,你就可以用调试器去调试,但是当用户在Blacklight图形界面里点击了“扫描”按钮时,Blacklight将会检测到调试器并且退出。与其找出一种方法来绕过其反调试手段,还不如分析Blacklight当中调用的函数。最终作者使用了Rohitabs API Monitor。

在测试的时候,你可以看到对API函数OpenProcess的调用失败(使用TLS table已经清0的Blacklight)。Blacklight尝试打开PID为0x1CC,0x1D0,0x1D4,0x1D8等的进程。作者把Blaclight使用的这种方法戏称为PID爆破(PIDB)。Blacklight从0x0到0x4E1C为止,循环使用OpenProcess尝试打开每一个可能的PID所对应的进程。Blacklight使用PIDB,然后对能够打开的进程保存一个列表。接着,Blacklight调用CreateToolhelp32Snapshot函数,它提供给Blacklight一张关于进程的第二张列表。接下来Blacklight就比较这两张表,看看有没有哪些进程在PIDB的列表当中,而不在CreateToolhelp32Snapshot函数返回的列表当中的。如果二者有不同,那么这些进程就被认为是隐藏进程,并被报告给用户。


Windows OpenProcess

在Windows中,OpenProcess函数是对NtOpenProcess调用的一个包装。NtOpenProcess包含在内核模块NTOSKRNL.EXE当中。NtOpenProcess的函数原型是:

NTSTATUS NtOpenProcess (
  OUT PHANDLE ProcessHandle,
  IN ACCESS_MASK DesiredAccess,
  IN POBJECT_ATTRIBUTES ObjectAttributes,
  IN PCLIENT_ID ClientId OPTIONAL);

ClientID参数是OpenProcess传递的实际PID。这个参数是可选的,但是根据我们的观察,OpenProcess在调用NtOpenProcess的时候总是使用这个参数。

NtOpenProcess主要实现3个功能:

1. 它通过调用PsLookupProcessByProcessId函数来验证进程是否存在。
2. 它试图通过调用ObOpenObjectByPointer来打开进程的句柄。
3. 如果打开进程句柄成功,就将句柄返回给调用者。

PsLookupProcessByProcessId很显然是下一个研究目标。一个很显著的问题是,PsLookupProcessByProcessId如何知道一个给定的PID是不是一个有效的进程呢?答案在反汇编出来的头几行里就可以看得很清除了:

PsLookupProcessByProcessId:
  mov edi, edi
  push ebp
  mov ebp, esp
  push ebx
  push esi
  mov eax, large fs:124h
  push [ebp+arg_4]
  mov esi, eax
  dec dword ptr [esi+0D4h]
  push PspCidTable
  call ExMapHandleToPointer

从以上的反汇编结果中,可以清楚地看出ExMapHandleToPointer查询PspCidTable来获取PID。

现在我们对于Blacklight如何检测隐藏进程有了一个完全的了解:

1. Blacklight启动时循环检测从0x0到0x41DC之间的所有有效的PID。
2. Blacklight对每一个可能的PID调用OpenProcess。
3. OpenProcess调用NtOpenProcess。
4. NtOpenProcess调用PsLookupProcessByProcessId来检测进程是否存在。
5. PsLookupProcessByProcessId使用PspCidTable来检测进程是否存在。
6. NtOpenProcess调用ObOpenObjectByPointer来获取进程句柄。
7. 如果OpenProcess成功返回,Blacklight就储存其进程信息,然后继续循环。
8. 一旦所有可能的PID都被循环检测过了,Blacklight就对PIDB列表和CreateToolhelp32Snapshot返回的列表作比较。CreateToolhelp32Snapshot是一个Windows API,它对系统当前运行对进程作一个快照。两张列表之间的差异就表示有隐藏进程存在。这样Blacklight就将其报告出来。


PspCidTable

PspCidTable就是“进程和线程ID的句柄表”。每一个进程的PID都能作PspCidTable中找到它的对应。PspCidTable是一个指向HANDLE_TABLE结构的指针。

typedef struct _HANDLE_TABLE {
  PVOID    p_hTable;
  PEPROCESS  QuotaProcess;
  PVOID    UniqueProcessId;
  EX_PUSH_LOCK  HandleTableLock[4];
  LIST_ENTRY  HandleTableList;
  EX_PUSH_LOCK  HandleContentionEvent;
  PHANDLE_TRACE_DEBUG_INFO  DebugInfo;
  DWORD    ExtraInfoPages;
  DWORD    FirstFree;
  DWORD    LastFree;
  DWORD    NextHandleNeedingPool;
  DWORD    HandleCount;
  DWORD    Flags;
};

Windows提供了大量未导出的函数来对PspCidTable进行操作和获取信息。它们包括:

[ExCreateHanleTable]创建非进程对句柄表。句柄表当中对所有对象,除了PspCidTable以外,都指向对象头,而不是指向对象的地址。
[ExDupHandleTable]当产生进程的时候调用。
[ExSweepHandleTable]当进程被销毁时使用。
[ExDestroyHandleTable]当进程退出时调用。
[ExCreateHandle]在句柄表中创建新的条目。
[ExChangeHandle]被用于改变进程的访问掩码。
[ExDestroyHandle]实现了CloseHandle的功能。
[ExMapHandleToPointer]返回句柄对应的对象的地址。
[ExReferenceHandleDubugIn]跟踪句柄。
[ExSnapShotHandleTables]被用于进程搜索(如oh.exe)。

下面的代码使用了未导出的函数来从PspCidTable中删除一个进程对象。它使用了硬编码的地址来使用未导出的函数;然而,rootkit能够动态地找出这些函数地址。

typedef PHANDLE_TABLE_ENTRY (*ExMapHandleToPointerFUNC) ( IN PHANDLE_TABLE HandleTable, IN HANDLE ProcessId);

void HideFromBlacklight(DWORD eproc)
{
  PHANDLE_TABLE_ENTRY CidEntry;
  ExMapHandleToPointerFUNC map;
  ExUnlockHandleTableEntryFUNC umap;
  PEPROCESS p;
  CLIENT_ID ClientId;

  map = (ExMapHandleToPointerFUNC)0x80493285;

  CidEntry = map((PHANDLE_TABLE)0x8188d7c8,
  LongToHandle( *((DWORD*)(eproc+PIDOFFSET)) ) );
  if(CidEntry != NULL)
    CidEntry->Object = 0;
  return;
}

既然PspCidTable的作用是为了跟踪进程和线程,那么rootkit检测程序使用PspCidTable来检测隐藏进程就很合理了。然而,单单依靠这一个数据结构并不是一个强健的算法。如果rootkit改变了这个数据结构,操作系统很其它程序就无从知道隐藏进程是否存在了。因此必须开发出一种新的算法,它并不依赖于单一的数据结构,这样,单方面的改变就不会逃过检测了。


FUTo

为了演示像Blacklight和Icesword这样的rootkit检测程序使用的现行算法的缺陷,作者开发了FUTo。FUTo是FU rootkit的新版本。FUTo增加了不使用任何函数来对PspCidTable进行操作的新功能。它使用了DKOM技术来隐藏PspCidTable当中特定的对象。

在开发FUTo时有一些设计方面的考虑。第一个就是,正如ExMapHandleXXX函数一样,PspCidTable并没有被内核导出。为了克服这一困难,FUTo通过反汇编PsLookupProcessByProcessId并查找其第一个函数调用的方法来自动找到PspCidTable。在写这篇文章的时候,第一个函数调用总是ExMapHandleToPointer。ExMapHandleToPointer使用PspCidTable作为第一个参数。这样,就可以直接找到PspCidTable了。

PsLookupProcessByProcessId:
  mov edi, edi
  push ebp
  mov ebp, esp
  push ebx
  push esi
  mov eax, large fs:124h
  push [ebp+arg_4]
  mov esi, eax
  dec dword ptr [esi+0D4h]
  push PspCidTable
  call ExMapHandleToPointer

还可以写出一种更强健的方法来找出PspCidTable,因为即使是编写内核时作了一点点简单的编译器优化,上面的方法就会失败。Opc0de写了一个更强健的方法来找出未导出的变量,如PspCidTable,PspActiveProcessHead,PspLoadedModuleList,等等。Opc0de的方法并不需要像FUTo正在使用的内存扫描一样。取而代之的是,Opc0de发现,Process Control Region结构的KdVersionBlock域指向一个KDDEBUGGER_DATA32结构。这个结构如下所示:

typedef struct _KDDEBUGGER_DATA32 {

  DBGKD_DEBUG_DATA_HEADER32  Header;
  ULONG  KernBase;
  ULONG  BreakpointWithStatus;  // address of breakpoint
  ULONG  SavedContext;
  USHORT  ThCallbackStack;   // offset in thread data
  USHORT  NextCallback;  // saved pointer to next callback frame
  USHORT  FramePointer;  // saved frame pointer
  USHORT  PaeEnabled:1;
  ULONG  KiCallUserMode;  // kernel routine
  ULONG  KeUserCallbackDispatcher;  // address in ntdll

  ULONG  PsLoadedModuleList;
  ULONG  PsActiveProcessHead;
  ULONG  PspCidTable;

  ULONG  ExpSystemResourcesList;
  ULONG  ExpPagedPoolDescriptor;
  ULONG  ExpNumberOfPagedPools;

  [...]

  ULONG  KdPrintCircularBuffer;
  ULONG  KdPrintCircularBufferEnd;
  ULONG  KdPrintWritePointer;
  ULONG  KdPrintRolloverCount;

  ULONG  MmLoadedUserImageList;

} KDDEBUGGER_DATA32, *PKDDEBUGGER_DATA32;

正如读者看见的一样,这个结构包含许多很有用的未导出的变量。这是一个找出PspCidTable和其它类似的未导出变量的更加强健的方法。

第二个设计上的考虑要麻烦一点。当FUTo从PspCidTable中删除一个对象的时候,句柄项被用NULL替代,这意味着这个进程“并不存在”。当进程被隐藏(在PspCidTable里面找不到)和关闭当时候问题就产生了。当系统想关闭进程当时候,它将找到PspCidTable并且得到一个NULL对象指针,这将导致蓝屏。解决方法很简单,但是并不优雅。首先,FUTo通过调用PsSetCreateProcessNotifyRoutine来安装一个进程通知例程。当有进程被创建时,这个回调函数将被调用,但更重要的是,当进程被删除时,它也会被调用。回调函数在进程被中止之前调用;也就是说,它在系统崩溃之前会被调用。当FUTo检测到对这种流氓进程对象的引用时,FUTo就把这个值保存下来以便以后使用。当进程被关闭时,FUTo会在进程结束之前恢复对象,这就允许系统解除对有效对象的引用。


结语

2005年的一句妙语是:“我们为rootkit检测增加了障碍。”希望读者能对顶尖rootkit检测程序是怎样检测隐藏进程的以及能够如何改进有更深入的了解。有些读者可能会问:“我该怎样做呢?”嗯,最简单的方法就是不要连到Internet,但是把Blacklight,IceSword和Rootkit Revealer联合起来使用将帮助你极大地减小中rootkit的几率。一个新的叫做RAIDE(Rootkit分析验证以及去除)的工具将在几个月之内在Blackhat Amsterdam揭开她的神秘面纱。这个新的工具将不再受本文提到的问题影响。


参考文献

[1]Blacklight Homepage. F-Secure Blacklight http://www.f-secure.com/blacklight/
[2]FU Project Page. FU http://www.rootkit.com/project.php?id=12
[3]IceSword Homepage. IceSword http://www.xfocus.net/tools/200505/1032.html
[4]LordPE Homepage. LordPE Info http://mitglied.lycos.de/yoda2k/LordPE/info.htm
[5]Opc0de. 2005. How to get some hidden kernel variables without scanning http://www.rootkit.com/newsread.php?newsid=101
[6]Rohitabs API Monitor. API Monitor - Spy on API calls http://www.rohitab.com/apimonitor/
[7]Russinovich, Solomon. Microsoft Windows Internals Fourth Edition.
[8]Silberman. RAIDE:Rootkit Analysis Identification Elimination http://www.blackhat.com/html/bh-euro...html#Silberman