Пишем нерезидентный лоадер и билдер к нему

D2

Администратор
Регистрация
19 Фев 2025
Сообщения
4,380
Реакции
0
Автор ru_soft
Источник https://xss.is

Доброго времени суток

Сегодня мы напишем быстрый и маленький нерезидентный лоадер вместе с билдером к нему.
Реализуем возможность собирать лоадер в следующих форматах: EXE x86, EXE x64, DLL x86 (rundll32/regsvr), DLL x64 (rundll32/regsvr)? выход из лоу через спам UAC окном, удаление Zone Identifier у скачанного файла и простую защиту от попадания в онлайн сэндбоксы.
Само собой, будет добавлена поддержка разных видов полезной нагрузки, начиная бинарными пейлоадами (EXE, DLL, шеллкоды) и заканчивая скриптами.
Для простоты понимания код будет написан на C с использованием среды разработки VS 2022, написанный мною код будет работать и на более ранних версиях, однако в том случае, если для вас важна поддержка Windows XP - потребуются соответствующие пакеты.

Для начала нам потребуется создать пустой проект и правильно его настроить. Мы уберём из проекта все лишние зависимости, отключим CRT и сохранение отладочной информации.
После создания проекта переходим в его свойства, и ставим следующие настройки по вкладкам:

Общие:

Набор инструментов платформы - Visual Studio 2017 - Windows XP (v141_xp)

C/C++:

Оптимизация:

Оптимизация: Максимальная оптимизация (приоритет размера) /O1
Предпочитать размер или скорость: предпочитать краткость кода (/Os)
Оптимизация всей программы: нет

Создание кода:

Включить обьединние строк: Нет (/GF-)
Включить C++ исключения: Нет
Библиотека времени выполнения: Многопоточная (/MT)

Компоновщик:

Файл манифеста:

Создавать манифест: Нет (/MANIFEST:NO)

Отладка:

Создавать отладочную информацию: Нет

Система:

Подсистема: /SUBSYSTEM:WINDOWS

Дополнительно:

Точка входа: Entry
Нажмите, чтобы раскрыть...
Применяем настройки и сохраняем их. Мы отвязали проект от CRT, сделали чтобы он собирался статически и задали кастомную точку входа - начнём написание лоадера с ее оформления.
Прототип точки входа будет выглядеть так:
C: Скопировать в буфер обмена
Код:
void Entry() {

}

Как видите, в отличии от приложений использующих CRT она ничего не возвращает, потому что системный загрузчик передаёт управление напрямую на неё.
В случае с CRT управление передаётся на точку входа CRT, внутри которой инициализируется рантайм, а затем происходит прыжок на main, WinMain и аналоги.

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

C: Скопировать в буфер обмена
Код:
#include "dbg.h"
#include <Shlwapi.h>

namespace Debug {
    OutputTypes output_type;
    CRITICAL_SECTION CS = { 0 };
    HANDLE dbg_file_handle = 0;

    bool Init(OutputTypes type) {
        output_type = type;

        if (type == OutputTypes::kFile) {
            InitializeCriticalSection(&CS);

            dbg_file_handle = CreateFileW(L"C:\\ProgramData\\fastldr.log", GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, 0, 0);

            if (dbg_file_handle == INVALID_HANDLE_VALUE) {
                return FALSE;
            }

            WORD bom = 0xFEFF;
            DWORD written = 0;

            WriteFile(dbg_file_handle, &bom, sizeof(bom), &written, NULL);
        }

        return TRUE;
    }

    void OutToDbg(LPCWSTR message_text, va_list args) {
        WCHAR dbg_message[1025] = { 0 };

        wvsprintfW(dbg_message, message_text, args);
        OutputDebugStringW(dbg_message);
    }

    void OutToFile(LPCWSTR message_text, va_list args) {
        EnterCriticalSection(&CS);

        WCHAR dbg_message[1025] = { 0 };
        int char_count = wvsprintfW(dbg_message, message_text, args);
        DWORD written = 0;


        WriteFile(dbg_file_handle, dbg_message, char_count * 2, &written, nullptr);
        WriteFile(dbg_file_handle, L"\r\n", 4, &written, nullptr);
        LeaveCriticalSection(&CS);
    }

    void DebugMsg(LPCWSTR message_text, ...) {
        va_list args = nullptr;

        va_start(args, message_text);

        switch (output_type) {
        case OutputTypes::kDbgConsole:
            OutToDbg(message_text, args);
        case OutputTypes::kFile:
            OutToFile(message_text, args);
        default:
            break;
        }

        va_end(args);
    }
}

Далее мы реализуем функционал извлечения информации из конфига. Наш конфиг будет зашифрованным RC4 блобом. В стаб вшита сигнатура, билдер будет искать эту сигнатуру, формировать конфигурацию и вставлять её на место сигнатуры.
RC4 ключ при каждом ребилде уникален, чтобы конфиг не выглядел статично. В памяти данные будут смотреться так:

RC4 ключ (8 байт) - размер зашифрованного конфига (4 байта) - конфиг

После расшифровки конфига мы получим:

Размер ссылки на файл (4 байта) - ссылка на файл - Тип полезной нагрузки (4 байта) - размер экспортируемой функции (4 байта) - экспортируемая функция - нужно ли принудительно повышать уровень привилегий (1 байт)

Про последнюю опцию - мы введём возможность принудительно поднимать integrity level до High тем же методом флуда UAC окном.
Если процесс запущен под Medium IL, но права отсутствуют, условие отработает и привилегии будут повышаться.

Я вынесу конфиг в глобальную переменную, задав ему размер в 5000 байт, или же 5 килобайт. Добавляем сигнатуру, забивая остальное место после неё нулями:

C: Скопировать в буфер обмена
BYTE config[5000] = { "INSERT_CONFIG_HERE" };

Рассмотрим функционал распаковки конфига. Вышеописанная информация считывается из BLOB'а, дешифруется и распределяется по переменным.
Для дешифровки мы будем использовать алгоритм шифрования ARC4, реализация которого была позаимствована мной из библиотеки mbedtls, Скачать
View hidden content is available for registered users!
 
Сверху Снизу