VC 訊息鉤子程式設計
阿新 • • 發佈:2018-12-19
一、訊息鉤子的概念1、基本概念 Windows應用程式是基於訊息驅動的,任何執行緒只要註冊視窗類都會有一個訊息佇列用於接收使用者輸入的訊息和系統訊息。為了攔截訊息,Windows提出了鉤子的概念。鉤子(Hook)是Windows訊息處理機制中的一個監視點,鉤子提供一個回撥函式。當在某個程式中安裝鉤子後,它將監視該程式的訊息,在指定訊息還沒到達視窗之前鉤子程式先捕獲這個訊息。這樣就有機會對此訊息進行過濾,或者對Windows訊息實現監控。 2、分類 訊息鉤子分為區域性鉤子和全域性鉤子。區域性鉤子是指僅攔截指定一個程序的指定訊息,全域性鉤子將攔截系統中所有程序的指定訊息。 3、實現步驟 使用鉤子技術攔截訊息通常分為如下幾個步驟: 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。其原型如下。NTSYSAPINTSTATUSNTAPIZwSystemDebugControl( 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, //分別呼叫[email protected]和[email protected] SysDbgSysReadMsr = 16, SysDbgSysWriteMsr = 17, //讀/寫匯流排資料 SysDbgSysReadBusData = 18, SysDbgSysWriteBusData = 19, SysDbgSysCheckLowMemory = 20,// 以下是NT 5.2 新增的 //分別呼叫[email protected]和[email protected] 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了。
- 設定鉤子回撥函式;(攔截到訊息後所呼叫的函式)
- 安裝鉤子;(使用SetWindowsHookEx函式)
- 解除安裝鉤子。(使用UnhookWindowsHookEx函式)