2011年10月12日星期三

[轉載]計算機病毒對消息鉤子的利用與對抗


作 者: dncwbc
時 間: 2011-10-07,23:57:15
鏈接: http://bbs.pediy.com/showthread.php?t=141059

一、消息鉤子的概念
    1、基本概念
   Windows應用程序是基於消息驅動的,任何線程只要註冊窗口類都會有一個消息隊列用於接收用戶輸入的消息和系統消息。為了攔截消息,Windows提出了鉤子的概念。鉤子(Hook)是Windows消息處理機制中的一個監視點,鉤子提供一個回調函數。當在某個程序中安裝鉤子後,它將監視該程序的消息,在指定消息還沒到達窗口之前鉤子程序先捕獲這個消息。這樣就有機會對此消息進行過濾,或者對Windows消息實現監控。
    2、分類
    消息鉤子分為局部鉤子和全局鉤子。局部鉤子是指僅攔截指定一個進程的指定消息,全局鉤子將攔截系統中所有進程的指定消息。
    3、實現步驟
    使用鉤子技術攔截消息通常分為如下幾個步驟:
    設置鉤子回調函數;(攔截到消息後所調用的函數)
    安裝鉤子;(使用SetWindowsHookEx函數)
    卸載鉤子。 (使用UnhookWindowsHookEx函數)
    4、功能
    利用消息鉤子可以實現特效界面、同步消息、監控消息、自啟動等功效。

二、病毒對消息鉤子技術的利用
    計算機病毒經常利用消息鉤子實現兩種功能:
    1、監控用戶按鍵,盜取用戶信息。
    這樣的病毒會啟動一個常駐內存的EXE病毒進程,然後安裝一個全局鍵盤消息鉤子,鉤子回調函數位於病毒進程中,這樣系統中任何有按鍵操作的進程,其按鍵詳細信息都會被病毒進程攔截記錄。
    2、自啟動
    這樣的病毒會將鉤子回調函數放在一個DLL文件中,然後安裝一個全局消息(容易觸發的消息,如WH_CBT、WH_GETMESSAGE等)鉤子,這樣凡響應該消息的進程都會自動加載病毒的DLL,病毒也就跟著自動運行了。

三、消息鉤子病毒的對抗技術(重點)
    1、對抗技術原理
    對付消息鉤子病毒方法很簡單,只要將病毒安裝的鉤子卸載掉即可。 (注意:對於系統中許多進程已經因為全局鉤子而加載了病毒DLL的情況,並不需要去卸載這些DLL,只要安裝的消息鉤子被卸載那麼對應的DLL也都會被在這些進程中自動卸載。 )卸載鉤子有兩種方法:
    (1)、結束掉安裝鉤子的進程
    將設置鉤子的進程結束,進程在退出之前會自行卸載掉該進程安裝的所有消息鉤子。這種方法很適合對付監控用戶按鍵的病毒。
    (2)、獲得消息鉤子句柄,然後調用UnhookWindowsHookEx函數即可將消息鉤子卸載。
    如果病毒單獨啟動了一個病毒進程安裝了一個全局消息鉤子,然後就常駐內存。這時我們將這個病毒進程結束掉即可。但是如果病毒在系統進程中註入代碼而安裝的鉤子,這樣鉤子句柄就位於系統進程中,我們不可以結束系統進程,這時就只能獲取這個消息鉤子句柄,然後調用函數卸載。
    2、對抗技術實現細節
    對於結束掉安裝鉤子進程從而卸載病毒消息鉤子的方法很容易實現,只要找到病毒進程結束即可。而對於獲取病毒消息鉤子句柄,然後調用函數卸載鉤子的方法比較複雜,也是本文重點討論的內容,將在下一個標題中詳細介紹。

四、查找病毒消息鉤子句柄然後卸載的方法實現(重點、難點)
    1、實現原理分析
    系統會將所有安裝的鉤子句柄保存在內核中,要查找病毒安裝的消息鉤子句柄,我們要枚舉所有的消息鉤子句柄。如何枚舉稍後講解,還要解決一個問題,就是在枚舉過程中,我們怎麼知道哪個句柄是病毒安裝的呢?
    通過分析病毒樣本我們通常可以得到病毒安裝鉤子就是為了令其他合法進程加載病毒DLL,所以它會將鉤子回調函數寫在該DLL中。在枚舉消息鉤子句柄時,同時也可以得到該句柄所對應的回調函數所屬的DLL模塊,根據這個DLL模塊是不是病毒的DLL模塊即可找到病毒的消息鉤子句柄,最後將其卸載即可。
    關於如何枚舉系統消息鉤子句柄,對於不同的操作系統方法大不相同,這裡介紹一種用戶層讀內存的方法,此方法僅在2000/XP系統下可用。
    在2000/XP系統下有一個Windows用戶界面相關的應用程序接口User32.dll。它用於包括Windows窗口處理,基本用戶界面等特性,如創建窗口和發送消息。當它被加載到內​​存後,它保存了所有Windows窗口、消息相關的句柄,其中就包括消息鉤子句柄。這些句柄被保存在一塊共享內存段中,通常稱為R3層的GUI TABLE。所以只要我們找到GUI TABLE,然後在其中的句柄中篩選出消息鉤子句柄。 GUI TABLE這塊內存段可以被所有進程空間訪問。 GUI TABLE被定義成如下結構:
typedef struct tagSHAREDINFO {
  struct tagSERVERINFO *pServerInfo; //指向tagSERVERINFO結構的指針
  struct _HANDLEENTRY *pHandleEntry; // 指向句柄表
  struct tagDISPLAYINFO *pDispInfo; //指向tagDISPLAYINFO結構的指針
  ULONG ulSharedDelta;
  LPWSTR pszDllList;
} SHAREDINFO, *PSHAREDINFO;
    tagSHAREDINFO結構體的第一個成員pServerInfo所指向的tagSERVERINFO結構體定義如下。
typedef struct tagSERVERINFO {
    short wRIPFlags ;
    short wSRVIFlags ;
    short wRIPPID ;
    short wRIPError ;
    ULONG cHandleEntries; //句柄表中句柄的個數
}SERVERINFO,*PSERVERINFO;
    可以看出通過tagSERVERINFO結構的cHandleEntries成員即可得到tagSHAREDINFO結構的pHandleEntry成員所指向的句柄表中的句柄數。
    tagSHAREDINFO結構體的第二個成員pHandleEntry是指向_HANDLEENTRY結構體數組起始地址的指針,該數組的一個成員對應一個句柄。句柄結構體_HANDLEENTRY定義如下。
typedef struct _HANDLEENTRY{
    PVOID pObject; //指向句柄所對應的內核對象
    ULONG pOwner;
      BYTE bType; //句柄的類型
    BYTE bFlags;
    short wUniq;
}HANDLEENTRY,*PHANDLEENTRY;
    _HANDLEENTRY結構體成員bType是句柄的類型,通過該變量的判斷可以篩選消息鉤子句柄。 User32中保存的句柄類型通常有如下種類。
typedef enum _HANDLE_TYPE
{
        TYPE_FREE = 0,
        TYPE_WINDOW = 1 ,
        TYPE_MENU = 2, //菜單句柄
     TYPE_CURSOR = 3, //光標句柄
     TYPE_SETWINDOWPOS = 4,
        TYPE_HOOK = 5, //消息鉤子句柄
     TYPE_CLIPDATA = 6 ,
        TYPE_CALLPROC = 7,
        TYPE_ACCELTABLE = 8,
        TYPE_DDEACCESS = 9,
        TYPE_DDECONV = 10,
        TYPE_DDEXACT = 11,
        TYPE_MONITOR = 12,
        TYPE_KBDLAYOUT = 13 ,
        TYPE_KBDFILE = 14 ,
        TYPE_WINEVENTHOOK = 15 ,
        TYPE_TIMER = 16,
        TYPE_INPUTCONTEXT = 17 ,
        TYPE_CTYPES = 18 ,
        TYPE_GENERIC = 255
}HANDLE_TYPE;
    _HANDLEENTRY結構體的成員pObject是指向句柄對應的內核對象的指針。
    這樣只要通過pObject就可以得到句柄的詳細信息(其中包括創建進程,線程、回調函數等信息),通過bType就可以的值句柄的類型。
_HANDLEENTRY結構體的其他成員可以忽略不看。
    (知識要點補充:如何在用戶層程序中讀取內核內存)
    需要注意的是,pObject指針指向的是內核內存,不可以在用戶層直接訪問內核內存。後面還有些地方也同樣是內核內存,需要加以注意。應該把內核內存的數據讀取到用戶層內存才可以訪問。且不可以直接訪問,畢竟不是在驅動中。
    在用戶層讀取內核內存使用ZwSystemDebugControl函數,它是一個Native API。其原型如下。
NTSYSAPI
NTSTATUS
NTAPI
ZwSystemDebugControl(
    IN DEBUG_CONTROL_CODE Con​​trolCode,//控制代碼
   IN PVOID InputBuffer OPTIONAL, //輸入內存
   IN ULONG InputBufferLength, //輸入內存長度
   OUT PVOID OutputBuffer OPTIONAL, //輸出內存
   IN ULONG OutputBufferLength, //輸出內存長度
   OUT PULONG ReturnLength OPTIONAL //實際輸出的長度);
ZwSystemDebugControl函數可以用於讀/寫內核空間、讀/寫MSR、讀/寫物理內存、讀/寫IO端口、讀/寫總線數據、KdVersionBlock等。由第一個參數ControlCode控制其功能,可以取如下枚舉值。
    typedef enum _SYSDBG_COMMAND {
    //以下5個在Windows NT各個版本上都有
    SysDbgGetTraceInformation = 1,
      SysDbgSetInternalBreakpoint = 2,
      SysDbgSetSpecialCall = 3,
      SysDbgClearSpecialCalls = 4,
      SysDbgQuerySpecialCalls = 5,
    // 以下是NT 5.1 新增的
    SysDbgDbgBreakPointWithStatus = 6,
    //獲取KdVersionBlock
      SysDbgSysGetVersion = 7,
    //從內核空間複製到用戶空間,或者從用戶空間複製到用戶空間
    //但是不能從用戶空間複製到內核空間
    SysDbgCopyMemoryChunks_0 = 8,
   //SysDbgReadVirtualMemory = 8,
    //從用戶空間複製到內核空間,或者從用戶空間複製到用戶空間
    //但是不能從內核空間複製到用戶空間
    SysDbgCopyMemoryChunks_1 = 9,
    //SysDbgWriteVirtualMemory = 9,
    //從物理地址複製到用戶空間,不能寫到內核空間
    SysDbgCopyMemoryChunks_2 = 10,
    //SysDbgReadVirtualMemory = 10,
    //從用戶空間複製到物理地址,不能讀取內核空間
    SysDbgCopyMemoryChunks_3 = 11,
    //SysDbgWriteVirtualMemory = 11,
    //讀/寫處理器相關控制塊
    SysDbgSysReadControlSpace = 12,
    SysDbgSysWriteControlSpace = 13,
    //讀/寫端口
    SysDbgSysReadIoSpace = 14,
      SysDbgSysWriteIoSpace = 15,
    //分別調用RDMSR@4和_WRMSR@12
      SysDbgSysReadMsr = 16,
      SysDbgSysWriteMsr = 17,
    //讀/寫總線數據
    SysDbgSysReadBusData = 18,
      SysDbgSysWriteBusData = 19,
      SysDbgSysCheckLowMemory = 20,
// 以下是NT 5.2 新增的
    //分別調用_KdEnableDebugger@0和_KdDisableDebugger@0
      SysDbgEnableDebugger = 21,
      SysDbgDisableDebugger = 22,
    //獲取和設置一些調試相關的變量
    SysDbgGetAutoEnableOnEvent = 23,
      SysDbgSetAutoEnableOnEvent = 24,
      SysDbgGetPitchDebugger = 25,
      SysDbgSetDbgPrintBufferSize = 26,
      SysDbgGetIgnoreUmExceptions = 27,
      SysDbgSetIgnoreUmExceptions = 28
    } SYSDBG_COMMAND, *PSYSDBG_COMMAND;
    我們這裡要讀取內核內存,所以參數ControlCode應取值為SysDbgReadVirtualMemory。
當ControlCode取值為SysDbgReadVirtualMemory時,ZwSystemDebugControl函數的第4個參數和第5個參數被忽略,使用時傳入0即可。第二個參數InputBuffer是一個指向結構體_MEMORY_CHUNKS的指針,該結構體定義如下。
typedef struct _MEMORY_CHUNKS {
    ULONG Address; //內核內存地址指針(要讀的數據)
    PVOID Data; //用戶層內存地址指針(存放讀出的數據)
    ULONG Length; //讀取的長度
}MEMORY_CHUNKS, *PMEMORY_CHUNKS;
第三個參數InputBufferLength是_MEMORY_CHUNKS結構體的大小。使用sizeof運算符得到即可。
SysDbgReadVirtualMemory函數執行成功將返回0。否則返回錯誤代碼。
為了方便使用,我們可以封裝一個讀取內核內存的函數GetKernelMemory,實現如下:
#define SysDbgReadVirtualMemory 8
//定義ZwSystemDebugControl函數指針類型
typedef DWORD (WINAPI *ZWSYSTEMDEBUGCONTROL)(DWORD,PVOID,
DWORD,PVOID,DWORD,PVOID);
BOOL GetKernelMemory(PVOID pKernelAddr, PBYTE pBuffer, ULONG uLength)
{
    MEMORY_CHUNKS mc ;
    ULONG uReaded = 0;
    mc.Address=(ULONG)pKernelAddr; //內核內存地址
    mc.pData = pBuffer;//用戶層內存地址
    mc.Length = uLength; //讀取內存的長度
    ULONG st = -1 ;
  //獲得ZwSystemDebugControl函數地址
  ZWSYSTEMDEBUGCONTROL ZwSystemDebugControl = (ZWSYSTEMDEBUGCONTROL) GetProcAddress(
    GetModuleHandle("ntdll.dll"), "ZwSystemDebugControl");
  //讀取內核內存數據到用戶層
    st = ZwSystemDebugControl(SysDbgReadVirtualMemory, &mc, sizeof(mc), 0, 0, &uReaded);
    return st == 0;
}


    對於不同類型的句柄,其內核對象所屬內存對應的結構體不同,對於消息鉤子句柄,它的內核對象所屬內存對應的結構體實際上是_HOOK_INFO類型,其定義如下。
typedef struct _HOOK_INFO
{
  HANDLE hHandle; //鉤子的句柄
  DWORD Unknown1;
  PVOID Win32Thread; //一個指向win32k!_W32THREAD 結構體的指針
  PVOID Unknown2;
  PVOID SelfHook; //指向結構體的首地址
  PVOID NextHook; //指向下一個鉤子結構體
  int iHookType; //鉤子的類型。
  DWORD OffPfn; //鉤子函數的地址偏移,相對於所在模塊的偏移
  int iHookFlags; //鉤子標誌
  int iMod; //鉤子函數做在模塊的索引號碼,利用它可以得到模塊基址
  PVOID Win32ThreadHooked; //被鉤的線程結構指針
} HOOK_INFO,*PHOOK_INFO;
由上可以看出,得到鉤子內核對像數據後,該數據對應HOOK_INFO結構體信息。其中:
hHandle是鉤子句柄,使用它就可以卸載鉤子。
iHookType是鉤子的類型,消息鉤子類型定義如下。
typedef enum _HOOK_TYPE{
        MY_WH_MSGFILTER = -1,
        MY_WH_JOURNALRECORD = 0,
        MY_WH_JOURNALPLAYBACK = 1,
        MY_WH_KEYBOARD = 2,
        MY_WH_GETMESSAGE = 3,
        MY_WH_CALLWNDPROC = 4,
        MY_WH_CBT = 5,
        MY_WH_SYSMSGFILTER = 6,
        MY_WH_MOUSE = 7,
        MY_WH_HARDWARE =​​ 8,
        MY_WH_DEBUG = 9,
        MY_WH_SHELL = 10,
        MY_WH_FOREGROUNDIDLE = 11,
        MY_WH_CALLWNDPROCRET = 12,
        MY_WH_KEYBOARD_LL = 13,
        MY_WH_MOUSE_LL = 14
}HOOK_TYPE;
    OffPfn是鉤子回調函數的偏移地址,該偏移地址是相對於鉤子函數所在模塊基址的偏移。
   Win32Thread是指向_W32THREAD結構體的指針,通過這個結構體可以獲得鉤子所在進程ID和線程ID。該結構體定義如下。
typedef struct _W32THREAD
{
    PVOID pEThread ; //該指針用以獲得進程ID和線程ID
    ULONG RefCount ;
    ULONG ptlW32 ;
    ULONG pgdiDcattr ;
    ULONG pgdiBrushAttr ;
    ULONG pUMPDObjs ;
    ULONG pUMPDHeap ;
    ULONG dwEngAcquireCount ;
    ULONG pSemTable ;
    ULONG pUMPDObj ;
    PVOID ptl;
    PVOID ppi; //該指針用以獲得模塊基址
}W32THREAD, *PW32THREAD;
    _W32THREAD結構體第一個參數pEThread指向的內存偏移0x01EC處分別保存著進程ID和線程ID。注意pEThread指針指向的內存是內核內存。
   _W32THREAD結構體最後一個參數ppi指向的內存偏移0xA8處是所有模塊基址的地址表, _HOOK_INFO結構體的iMod成員就標識了本鉤子所屬模塊基址在此地址表中的位置。 (每個地址佔4個字節)所以通常使用ppi+0xa8+iMod*4定位模塊基址的地址。注意ppi指向的內存是內核內存。
    2、實現細節
    首先編寫程序枚舉消息鉤子句柄,需要得到GUI TABLE,它的地址實際上存儲於User32.dll的一個全局變量中,該模塊導出的函數UserRegisterWowHandlers將返回該全局變量的值。所以我們只要調用這個函數就能夠得到GUI TABLE。然而UserRegisterWowHandlers是一個未公開的函數,不確定它的函數原型,需要反彙編猜出它的原型。筆者反彙編後得到的原型如下。
typedef PSHAREDINFO (__stdcall *USERREGISTERWOWHANDLERS) (PBYTE ,PBYTE );
僅知道它兩個參數是兩個指針,但是不知道它的兩個參數的含義,所以我們無法構造出合理的參數。如果隨便構造參數傳進去又會導致user32.dll模塊發生錯誤。所以通過調用這個函數接收其返回值的方法就不能用了。再次反彙編該函數的實現可以看出,在不同操作系統下該函數的最後三行代碼如下。
2K系統:(5.0.2195.7032)
:77E3565D B880D2E477 mov eax, 77E4D280
:77E35662 C20800 ret 0008
XP系統:(5.1.2600.2180)
:77D535F5 B88000D777 mov eax, 77D70080
:77D535FA 5D pop ebp
:77D535FB C20800 ret 0008
2003系統:(5.2.3790.1830)
:77E514D9 B8C024E777 mov eax, 77E724C0
:77E514DE C9 leave
:77E514DF C2080000 ret 0008
可以看到共同點,該函數的倒數第三行代碼就是將保存GUI TABLE指針的全局變量值賦值給寄存器EAX,只要我們想辦法搜索到這個值即可。能夠看出無論是哪個版本的函數實現中,都有C20800代碼,含義是ret 0008。我們可以自UserRegisterWowHandlers函數的入口地址開始一直搜索到C20800,找到它以後再向前搜索B8指令,搜到以後B8指令後面的四個字節數據就是我們需要的數據。代碼如下。
//獲得UserRegisterWowHandlers函數的入口地址
DWORD UserRegisterWowHandlers = (DWORD) GetProcAddress(LoadLibrary("user32.dll"), "UserRegisterWowHandlers");
PSHAREDINFO pGUITable; //保存GUITable地址的指針
for(DWORD i=UserRegisterWowHandlers; i<UserRegisterWowHandlers+1000; i++)
{
  if((*(USHORT*)i==0x08c2)&&*(BYTE *)(i+2)== 0x00)
  { //已找到ret 0008指令,然後往回搜索B8
    for (int j=i; j>UserRegisterWowHandlers; j--)
    { //找到B8它後面四個字節保存的數值即為GUITable地址
      if (*(BYTE *)j == 0xB8)
      {
        pGUITable = (PSHAREDINFO)*(DWORD *)(j+1);
        break;
      }
    }break;
  }
}
    得到SHAREDINFO結構指針後,它的成員pServerInfo的成員cHandleEntries就是句柄的總個數,然後循環遍歷每一個句柄,找到屬於指定模塊的消息鉤子句柄。代碼如下。
int iHandleCount = pGUITable->pServerInfo->cHandleEntries;
HOOK_INFO HookInfo;
DWORD dwModuleBase;
struct TINFO
{
  DWORD dwProcessID;
  DWORD dwThreadID;
};
char cModuleName[256] = {0};
for (i=0; i<iHandleCount; i++)
{ //判斷句柄類型是否為消息鉤子句柄
  if (pGUITable->pHandleEntry[i].bType == TYPE_HOOK)
  {
    DWORD dwValue = (DWORD)pGUITable->pHandleEntry[i].pObject;
    //獲得消息鉤子內核對像數據
    GetKernelMemory(pGUITable->pHandleEntry[i].pObject, (BYTE *)&HookInfo, sizeof(HookInfo));
    W32THREAD w32thd;
    if( GetKernelMemory(HookInfo.pWin32Thread,(BYTE *)&w32thd , sizeof(w32thd)) )
    { //獲取鉤子函數所在模塊的基址
      if (!GetKernelMemory((PVOID)((ULONG)w32thd.ppi+0xA8+4*HookInfo.iMod),
        (BYTE *)&dwModuleBase, sizeof(dwModuleBase)))
      {
        continue;
      }
      TINFO tInfo;
      //獲取鉤子所屬進程ID和線程ID
      if (!GetKernelMemory((PVOID)((ULONG)w32thd.pEThread+0x1ec),
        (BYTE *)&tInfo, sizeof(tInfo)))
      {
        continue;
      }
      HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, tInfo.dwProcessID);
      if (hProcess == INVALID_HANDLE_VALUE)
      {
        continue;
      }
      //根據模塊基址,獲取鉤子函數所屬模塊的名稱
      if (GetModuleFileNameEx(hProcess, (HMODULE)dwModuleBase, cModuleName, 256))
      {
        OutputDebugString(cModuleName);
        OutputDebugString("\r\n");
      }
    }
  }
}

    利用上面的代碼就可以找到所屬病毒DLL的消息鉤子句柄,然後調用UnhookWindowsHookEx函數卸載這個消息鉤子就OK了​​。

    文章寫完了,寫點題外話。上述文章內容大部分摘自王倍昌的《計算機病毒揭秘與對抗》一書,該書已於2011年10月1日出版。該書揭秘了Windows系統下計算機病毒常用的技術和對抗技術。如:隱藏注入技術、SPI網絡劫持技術、DLL劫持技術、瀏覽器綁架技術、服務劫持技術、感染型病毒技術、加殼實現與靜態脫殼技術、反病毒虛擬機技術、主動防禦技術等。歡迎閱讀此書,並提出寶貴意見。關於此書的更多內容請查看www.safe163.com網站。

没有评论:

发表评论