VC++消息鈎子編程


一、消息鈎子的概念
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 ControlCode,//控制代碼
   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了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM