VC++消息鉤子編程 |
發(fā)布時間: 2012/9/11 16:55:31 |
一、消息鉤子的概念1、基本概念Windows應用程序是基于消息驅(qū)動的,任何線程只要注冊窗口類都會有一個消息隊列用于接收用戶輸入的消息和系統(tǒng)消息。為了攔截消息,Windows提出了鉤子的概念。鉤子(Hook)是Windows消息處理機制中的一個監(jiān)視點,鉤子提供一個回調(diào)函數(shù)。當在某個程序中安裝鉤子后,它將監(jiān)視該程序的消息,在指定消息還沒到達窗口之前鉤子程序先捕獲這個消息。這樣就有機會對此消息進行過濾,或者對Windows消息實現(xiàn)監(jiān)控。
2、分類消息鉤子分為局部鉤子和全局鉤子。局部鉤子是指僅攔截指定一個進程的指定消息,全局鉤子將攔截系統(tǒng)中所有進程的指定消息。 3、實現(xiàn)步驟使用鉤子技術攔截消息通常分為如下幾個步驟:設置鉤子回調(diào)函數(shù);(攔截到消息后所調(diào)用的函數(shù)) 安裝鉤子;(使用SetWindowsHookEx函數(shù)) 卸載鉤子。(使用UnhookWindowsHookEx函數(shù)) 4、功能利用消息鉤子可以實現(xiàn)特效界面、同步消息、監(jiān)控消息、自啟動等功效。 二、病毒對消息鉤子技術的利用計算機病毒經(jīng)常利用消息鉤子實現(xiàn)兩種功能:1、監(jiān)控用戶按鍵,盜取用戶信息。 這樣的病毒會啟動一個常駐內(nèi)存的EXE病毒進程,然后安裝一個全局鍵盤消息鉤子,鉤子回調(diào)函數(shù)位于病毒進程中,這樣系統(tǒng)中任何有按鍵操作的進程,其按鍵詳細信息都會被病毒進程攔截記錄。 2、自啟動這樣的病毒會將鉤子回調(diào)函數(shù)放在一個DLL文件中,然后安裝一個全局消息(容易觸發(fā)的消息,如WH_CBT、WH_GETMESSAGE等)鉤子,這樣凡響應該消息的進程都會自動加載病毒的DLL,病毒也就跟著自動運行了。 三、消息鉤子病毒的對抗技術(重點) 1、對抗技術原理對付消息鉤子病毒方法很簡單,只要將病毒安裝的鉤子卸載掉即可。(注意:對于系統(tǒng)中許多進程已經(jīng)因為全局鉤子而加載了病毒DLL的情況,并不需要去卸載這些DLL,只要安裝的消息鉤子被卸載那么對應的DLL也都會被在這些進程中自動卸載。)卸載鉤子有兩種方法:(1)、結(jié)束掉安裝鉤子的進程將設置鉤子的進程結(jié)束,進程在退出之前會自行卸載掉該進程安裝的所有消息鉤子。這種方法很適合對付監(jiān)控用戶按鍵的病毒。 (2)、獲得消息鉤子句柄,然后調(diào)用UnhookWindowsHookEx函數(shù)即可將消息鉤子卸載。 如果病毒單獨啟動了一個病毒進程安裝了一個全局消息鉤子,然后就常駐內(nèi)存。這時我們將這個病毒進程結(jié)束掉即可。但是如果病毒在系統(tǒng)進程中注入代碼而安裝的鉤子,這樣鉤子句柄就位于系統(tǒng)進程中,我們不可以結(jié)束系統(tǒng)進程,這時就只能獲取這個消息鉤子句柄,然后調(diào)用函數(shù)卸載。 2、對抗技術實現(xiàn)細節(jié)對于結(jié)束掉安裝鉤子進程從而卸載病毒消息鉤子的方法很容易實現(xiàn),只要找到病毒進程結(jié)束即可。而對于獲取病毒消息鉤子句柄,然后調(diào)用函數(shù)卸載鉤子的方法比較復雜,也是本文重點討論的內(nèi)容,將在下一個標題中詳細介紹。 四、查找病毒消息鉤子句柄然后卸載的方法實現(xiàn)(重點、難點) 1、實現(xiàn)原理分析系統(tǒng)會將所有安裝的鉤子句柄保存在內(nèi)核中,要查找病毒安裝的消息鉤子句柄,我們要枚舉所有的消息鉤子句柄。如何枚舉稍后講解,還要解決一個問題,就是在枚舉過程中,我們怎么知道哪個句柄是病毒安裝的呢? 通過分析病毒樣本我們通?梢缘玫讲《景惭b鉤子就是為了令其他合法進程加載病毒DLL,所以它會將鉤子回調(diào)函數(shù)寫在該DLL中。在枚舉消息鉤子句柄時,同時也可以得到該句柄所對應的回調(diào)函數(shù)所屬的DLL模塊,根據(jù)這個DLL模塊是不是病毒的DLL模塊即可找到病毒的消息鉤子句柄,最后將其卸載即可。 關于如何枚舉系統(tǒng)消息鉤子句柄,對于不同的操作系統(tǒng)方法大不相同,這里介紹一種用戶層讀內(nèi)存的方法,此方法僅在2000/XP系統(tǒng)下可用。 在2000/XP系統(tǒng)下有一個Windows用戶界面相關的應用程序接口User32.dll.它用于包括Windows窗口處理,基本用戶界面等特性,如創(chuàng)建窗口和發(fā)送消息。當它被加載到內(nèi)存后,它保存了所有Windows窗口、消息相關的句柄,其中就包括消息鉤子句柄。這些句柄被保存在一塊共享內(nèi)存段中,通常稱為R3層的GUI TABLE.所以只要我們找到GUI TABLE,然后在其中的句柄中篩選出消息鉤子句柄。GUI TABLE這塊內(nèi)存段可以被所有進程空間訪問。GUI TABLE被定義成如下結(jié)構(gòu):typedef struct tagSHAREDINFO { struct tagSERVERINFO *pServerInfo; //指向tagSERVERINFO結(jié)構(gòu)的指針struct _HANDLEENTRY *pHandleEntry; // 指向句柄表struct tagDISPLAYINFO *pDispInfo; //指向tagDISPLAYINFO結(jié)構(gòu)的指針ULONG ulSharedDelta;LPWSTR pszDllList;} SHAREDINFO, *PSHAREDINFO;tagSHAREDINFO結(jié)構(gòu)體的第一個成員pServerInfo所指向的tagSERVERINFO結(jié)構(gòu)體定義如下。 typedef struct tagSERVERINFO { short wRIPFlags ;short wSRVIFlags ;short wRIPPID ;short wRIPError ;ULONG cHandleEntries; //句柄表中句柄的個數(shù)}SERVERINFO,*PSERVERINFO;可以看出通過tagSERVERINFO結(jié)構(gòu)的cHandleEntries成員即可得到tagSHAREDINFO結(jié)構(gòu)的pHandleEntry成員所指向的句柄表中的句柄數(shù)。 tagSHAREDINFO結(jié)構(gòu)體的第二個成員pHandleEntry是指向_HANDLEENTRY結(jié)構(gòu)體數(shù)組起始地址的指針,該數(shù)組的一個成員對應一個句柄。句柄結(jié)構(gòu)體_HANDLEENTRY定義如下。 typedef struct _HANDLEENTRY{ PVOID pObject; //指向句柄所對應的內(nèi)核對象ULONG pOwner;BYTE bType; //句柄的類型BYTE bFlags;short wUniq;}HANDLEENTRY,*PHANDLEENTRY;_HANDLEENTRY結(jié)構(gòu)體成員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結(jié)構(gòu)體的成員pObject是指向句柄對應的內(nèi)核對象的指針。 這樣只要通過pObject就可以得到句柄的詳細信息(其中包括創(chuàng)建進程,線程、回調(diào)函數(shù)等信息),通過bType就可以的值句柄的類型。 _HANDLEENTRY結(jié)構(gòu)體的其他成員可以忽略不看。 。ㄖR要點補充:如何在用戶層程序中讀取內(nèi)核內(nèi)存) 需要注意的是,pObject指針指向的是內(nèi)核內(nèi)存,不可以在用戶層直接訪問內(nèi)核內(nèi)存。后面還有些地方也同樣是內(nèi)核內(nèi)存,需要加以注意。應該把內(nèi)核內(nèi)存的數(shù)據(jù)讀取到用戶層內(nèi)存才可以訪問。且不可以直接訪問,畢竟不是在驅(qū)動中。 在用戶層讀取內(nèi)核內(nèi)存使用ZwSystemDebugControl函數(shù),它是一個Native API.其原型如下。 NTSYSAPI NTSTATUS NTAPI ZwSystemDebugControl(IN DEBUG_CONTROL_CODE ControlCode,//控制代碼IN PVOID InputBuffer OPTIONAL, //輸入內(nèi)存IN ULONG InputBufferLength, //輸入內(nèi)存長度OUT PVOID OutputBuffer OPTIONAL, //輸出內(nèi)存IN ULONG OutputBufferLength, //輸出內(nèi)存長度OUT PULONG ReturnLength OPTIONAL //實際輸出的長度);ZwSystemDebugControl函數(shù)可以用于讀/寫內(nèi)核空間、讀/寫MSR、讀/寫物理內(nèi)存、讀/寫IO端口、讀/寫總線數(shù)據(jù)、KdVersionBlock等。由第一個參數(shù)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,//從內(nèi)核空間復制到用戶空間,或者從用戶空間復制到用戶空間//但是不能從用戶空間復制到內(nèi)核空間SysDbgCopyMemoryChunks_0 = 8,//SysDbgReadVirtualMemory = 8,//從用戶空間復制到內(nèi)核空間,或者從用戶空間復制到用戶空間//但是不能從內(nèi)核空間復制到用戶空間SysDbgCopyMemoryChunks_1 = 9,//SysDbgWriteVirtualMemory = 9,//從物理地址復制到用戶空間,不能寫到內(nèi)核空間SysDbgCopyMemoryChunks_2 = 10,//SysDbgReadVirtualMemory = 10,//從用戶空間復制到物理地址,不能讀取內(nèi)核空間SysDbgCopyMemoryChunks_3 = 11,//SysDbgWriteVirtualMemory = 11,//讀/寫處理器相關控制塊SysDbgSysReadControlSpace = 12,SysDbgSysWriteControlSpace = 13,//讀/寫端口SysDbgSysReadIoSpace = 14,SysDbgSysWriteIoSpace = 15,//分別調(diào)用RDMSR@4和_WRMSR@12 SysDbgSysReadMsr = 16,SysDbgSysWriteMsr = 17,//讀/寫總線數(shù)據(jù)SysDbgSysReadBusData = 18,SysDbgSysWriteBusData = 19,SysDbgSysCheckLowMemory = 20,// 以下是NT 5.2 新增的//分別調(diào)用_KdEnableDebugger@0和_KdDisableDebugger@0 SysDbgEnableDebugger = 21,SysDbgDisableDebugger = 22,//獲取和設置一些調(diào)試相關的變量SysDbgGetAutoEnableOnEvent = 23,SysDbgSetAutoEnableOnEvent = 24,SysDbgGetPitchDebugger = 25,SysDbgSetDbgPrintBufferSize = 26,SysDbgGetIgnoreUmExceptions = 27,SysDbgSetIgnoreUmExceptions = 28 } SYSDBG_COMMAND, *PSYSDBG_COMMAND;我們這里要讀取內(nèi)核內(nèi)存,所以參數(shù)ControlCode應取值為SysDbgReadVirtualMemory.當ControlCode取值為SysDbgReadVirtualMemory時,ZwSystemDebugControl函數(shù)的第4個參數(shù)和第5個參數(shù)被忽略,使用時傳入0即可。第二個參數(shù)InputBuffer是一個指向結(jié)構(gòu)體_MEMORY_CHUNKS的指針,該結(jié)構(gòu)體定義如下。 typedef struct _MEMORY_CHUNKS { ULONG Address; //內(nèi)核內(nèi)存地址指針(要讀的數(shù)據(jù)) PVOID Data; //用戶層內(nèi)存地址指針(存放讀出的數(shù)據(jù)) ULONG Length; //讀取的長度}MEMORY_CHUNKS, *PMEMORY_CHUNKS;第三個參數(shù)InputBufferLength是_MEMORY_CHUNKS結(jié)構(gòu)體的大小。使用sizeof運算符得到即可。 SysDbgReadVirtualMemory函數(shù)執(zhí)行成功將返回0.否則返回錯誤代碼。 為了方便使用,我們可以封裝一個讀取內(nèi)核內(nèi)存的函數(shù)GetKernelMemory,實現(xiàn)如下:#define SysDbgReadVirtualMemory 8 //定義ZwSystemDebugControl函數(shù)指針類型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; //內(nèi)核內(nèi)存地址mc.pData = pBuffer;//用戶層內(nèi)存地址mc.Length = uLength; //讀取內(nèi)存的長度ULONG st = -1 ;//獲得ZwSystemDebugControl函數(shù)地址ZWSYSTEMDEBUGCONTROL ZwSystemDebugControl = (ZWSYSTEMDEBUGCONTROL) GetProcAddress(GetModuleHandle("ntdll.dll"), "ZwSystemDebugControl");//讀取內(nèi)核內(nèi)存數(shù)據(jù)到用戶層st = ZwSystemDebugControl(SysDbgReadVirtualMemory, &mc, sizeof(mc), 0, 0, &uReaded);return st == 0;} 對于不同類型的句柄,其內(nèi)核對象所屬內(nèi)存對應的結(jié)構(gòu)體不同,對于消息鉤子句柄,它的內(nèi)核對象所屬內(nèi)存對應的結(jié)構(gòu)體實際上是_HOOK_INFO類型,其定義如下。 typedef struct _HOOK_INFO { HANDLE hHandle; //鉤子的句柄DWORD Unknown1;PVOID Win32Thread; //一個指向 win32k!_W32THREAD 結(jié)構(gòu)體的指針PVOID Unknown2;PVOID SelfHook; //指向結(jié)構(gòu)體的首地址PVOID NextHook; //指向下一個鉤子結(jié)構(gòu)體int iHookType; //鉤子的類型。 DWORD OffPfn; //鉤子函數(shù)的地址偏移,相對于所在模塊的偏移int iHookFlags; //鉤子標志int iMod; //鉤子函數(shù)做在模塊的索引號碼,利用它可以得到模塊基址PVOID Win32ThreadHooked; //被鉤的線程結(jié)構(gòu)指針} HOOK_INFO,*PHOOK_INFO;由上可以看出,得到鉤子內(nèi)核對象數(shù)據(jù)后,該數(shù)據(jù)對應HOOK_INFO結(jié)構(gòu)體信息。其中: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是鉤子回調(diào)函數(shù)的偏移地址,該偏移地址是相對于鉤子函數(shù)所在模塊基址的偏移。 Win32Thread是指向_W32THREAD結(jié)構(gòu)體的指針,通過這個結(jié)構(gòu)體可以獲得鉤子所在進程ID和線程ID.該結(jié)構(gòu)體定義如下。 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結(jié)構(gòu)體第一個參數(shù)pEThread指向的內(nèi)存偏移0x01EC處分別保存著進程ID和線程ID.注意pEThread指針指向的內(nèi)存是內(nèi)核內(nèi)存。 _W32THREAD結(jié)構(gòu)體最后一個參數(shù)ppi指向的內(nèi)存偏移0xA8處是所有模塊基址的地址表, _HOOK_INFO結(jié)構(gòu)體的iMod成員就標識了本鉤子所屬模塊基址在此地址表中的位置。(每個地址占4個字節(jié))所以通常使用ppi+0xa8+iMod*4定位模塊基址的地址。注意ppi指向的內(nèi)存是內(nèi)核內(nèi)存。 2、實現(xiàn)細節(jié)首先編寫程序枚舉消息鉤子句柄,需要得到GUI TABLE,它的地址實際上存儲于User32.dll的一個全局變量中,該模塊導出的函數(shù)UserRegisterWowHandlers將返回該全局變量的值。所以我們只要調(diào)用這個函數(shù)就能夠得到GUI TABLE.然而UserRegisterWowHandlers是一個未公開的函數(shù),不確定它的函數(shù)原型,需要反匯編猜出它的原型。筆者反匯編后得到的原型如下。 typedef PSHAREDINFO (__stdcall *USERREGISTERWOWHANDLERS) (PBYTE ,PBYTE );僅知道它兩個參數(shù)是兩個指針,但是不知道它的兩個參數(shù)的含義,所以我們無法構(gòu)造出合理的參數(shù)。如果隨便構(gòu)造參數(shù)傳進去又會導致user32.dll模塊發(fā)生錯誤。所以通過調(diào)用這個函數(shù)接收其返回值的方法就不能用了。再次反匯編該函數(shù)的實現(xiàn)可以看出,在不同操作系統(tǒng)下該函數(shù)的最后三行代碼如下。 2K系統(tǒng):(5.0.2195.7032) 。77E3565D B880D2E477 mov eax, 77E4D280:77E35662 C20800 ret 0008 XP系統(tǒng):(5.1.2600.2180) 。77D535F5 B88000D777 mov eax, 77D70080:77D535FA 5D pop ebp:77D535FB C20800 ret 0008 2003系統(tǒng):(5.2.3790.1830) 。77E514D9 B8C024E777 mov eax, 77E724C0:77E514DE C9 leave:77E514DF C2080000 ret 0008可以看到共同點,該函數(shù)的倒數(shù)第三行代碼就是將保存GUI TABLE指針的全局變量值賦值給寄存器EAX,只要我們想辦法搜索到這個值即可。能夠看出無論是哪個版本的函數(shù)實現(xiàn)中,都有 C20800代碼,含義是ret 0008.我們可以自UserRegisterWowHandlers函數(shù)的入口地址開始一直搜索到C20800,找到它以后再向前搜索B8指令,搜到以后B8指令后面的四個字節(jié)數(shù)據(jù)就是我們需要的數(shù)據(jù)。代碼如下。 //獲得UserRegisterWowHandlers函數(shù)的入口地址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它后面四個字節(jié)保存的數(shù)值即為GUITable地址if (*(BYTE *)j == 0xB8) { pGUITable = (PSHAREDINFO)*(DWORD *)(j+1);break;} }break;}得到SHAREDINFO結(jié)構(gòu)指針后,它的成員pServerInfo的成員cHandleEntries就是句柄的總個數(shù),然后循環(huán)遍歷每一個句柄,找到屬于指定模塊的消息鉤子句柄。代碼如下。 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;//獲得消息鉤子內(nèi)核對象數(shù)據(jù)GetKernelMemory(pGUITable->pHandleEntry[i].pObject, (BYTE *)&HookInfo, sizeof(HookInfo));W32THREAD w32thd;if( GetKernelMemory(HookInfo.pWin32Thread,(BYTE *)&w32thd , sizeof(w32thd)) ) { //獲取鉤子函數(shù)所在模塊的基址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;} //根據(jù)模塊基址,獲取鉤子函數(shù)所屬模塊的名稱if (GetModuleFileNameEx(hProcess, (HMODULE)dwModuleBase, cModuleName, 256)) { OutputDebugString(cModuleName);OutputDebugString("\r\n");} 利用上面的代碼就可以找到所屬病毒DLL的消息鉤子句柄,然后調(diào)用UnhookWindowsHookEx函數(shù)卸載這個消息鉤子就OK了。 本文出自:億恩科技【1tcdy.com】 |