stihl не предоставил(а) никакой дополнительной информации.
Как расширить возможности C2-маяка, не переписывая его код и не подставляясь под антивирусы? В статье разбираем Beacon Object Files — легкий модульный способ добавлять новые функции прямо на лету. С помощью BOF можно проводить постэксплуатацию, закрепление, эксфильтрацию и выполнять любые кастомные задачи без доработки самого маяка.
Например, для проведения фишинговой атаки тебе нужно разработать маяки, которые запускаются на захваченной машине и отстукивают какому‑то C2-серверу. Да, по своей сути маяк просто должен сохранять соединение с C2, и ты получаешь удаленный доступ к тачке. Но, как правило, тебе придется заниматься постэксплуатацией, эксфильтрацией, делать закреп на машине.
Тебе, конечно, не захочется переписывать маяк, раздувать его код и добавлять туда новые функции. Любую активность к тому же может запалить антивирус. Хорошо бы использовать инструмент, который уже будет обладать необходимыми фичами, а еще лучше, чтобы его можно было расширять в режиме онлайн. Именно для этого придумали Beacon Object File, или сокращенно BOF.
Beacon Object File — это концепция, которая впервые засветилась в 2021 году — в Cobalt Strike, самом известном фреймворке для постэксплуатации. Beacon Object File — это скомпилированная программа на языке C, которая может выполняться в процессе работы маяка и использовать внутренние Beacon API. BOF — это способ быстрого расширения агента новыми функциями.
BOF позволяет загружать в память нативный код во время выполнения маяка. Процесс похож на загрузку и выполнение DLL, но позволяет оставаться более незаметным. Сами BOF — это не исполняемые файлы, они представлены в форме Microsoft Common Object File Format (сокращенно COFF).
или Зарегистрируйся
Что ж, давай сядем за клавиатуру и начнем писать свой первый простенький BOF.
Для компиляции нужно использовать MinGWGCC. В Linux я буду компилировать так:
Здесь -c — опция, которая сообщает компилятору о том, что это объектный файл.
Для просмотра ссылки Войдиили Зарегистрируйся
Теперь введу file example.o и получу информацию о том, что это COFF object file.
Для просмотра ссылки Войдиили Зарегистрируйся
Мы получили объектный файл, но он должен что‑то делать. Чтобы он начал работать с маяком, например Cobalt Strike или Для просмотра ссылки Войдиили Зарегистрируйся, нам нужно использовать Beacon API. Откроем документацию Cobalt Strike и скачаем оттуда заголовочный файл Для просмотра ссылки Войди или Зарегистрируйся.
или Зарегистрируйся
или Зарегистрируйся с примером BOF.
Для просмотра ссылки Войдиили Зарегистрируйся
Дальше будем погружаться в API, но начнем с малого. Вот, например, функции, которые, видимо, способны выводить что‑то на экран:
Есть еще несколько удобных штук, но в итоге мы хотим иметь возможность запускать функции Win32 API. Сначала, впрочем, убедимся, что у нас все компилируется и запускается. Предлагаю проверить на обычном Hello World. Для этого будем использовать BeaconPrintf.
В исходном коде example.c нужно включить beacon.h, используя обычный #include. Как можешь заметить, BeaconPrintf() требует как минимум два аргумента: тип (целочисленное число) и сама строка.
Первый аргумент ждет какое‑то число, и эти числа прописаны в beacon.h:
Нам нужно CALLBACK_OUTPUT, а дальше просто вызовем BeaconPrintf(CALLBACK_OUTPUT, "Hello world!"):
или Зарегистрируйся описана каждая функция и есть видео с примером, который может помочь.
Теперь это надо скомпилировать, снова используя MinGW:
Для просмотра ссылки Войдиили Зарегистрируйся
Теперь встает вопрос: как это проверить? Вспомним, что это COFF, а не полный исполняемый файл. Чтобы запустить этот объектный файл без C2, можно использовать Для просмотра ссылки Войдиили Зарегистрируйся, который позволяет выполнить BOF в памяти.
Сначала собираем COFFLoader:
Для просмотра ссылки Войдиили Зарегистрируйся
Тут мы можем увидеть Makefile, и нам достаточно выполнить make.
Для просмотра ссылки Войдиили Зарегистрируйся
После этого получим COFFLoader64.exe. Теперь этот файл надо перенести на Windows и там протестировать свой BOF. Работать с COFFLoader64.exe очень просто: вводим COFFLoader64.exe go example.o и получаем результат.
Для просмотра ссылки Войдиили Зарегистрируйся
или Зарегистрируйся
Например, есть вот такая функция:
Тут указано, что вызывается функция DsGetDcNameA из NetApi32.dll. Разделитель — знак доллара.
Теперь попробуем сделать что‑нибудь очень простое, но с использованием Win32 API. К примеру, вызовем функцию GetCurrentProcessId(). Как с ней работать, посмотрим в Для просмотра ссылки Войдиили Зарегистрируйся.
Для просмотра ссылки Войдиили Зарегистрируйся
Видим, что функция находится в библиотеке kernel32.dll и возвращает данные типа DWORD.
Теперь вернемся в наш исходный код (example.c) и объявим эту функцию с использованием специального синтаксиса DECLSPEC_IMPORT. Так BOF узнает, что мы хотим использовать эту функцию:
DECLSPEC_IMPORT DWORD KERNEL32$GetCurrentProcessId();
Использование %ld означает long int, что не противоречит DWORD.
Для просмотра ссылки Войдиили Зарегистрируйся
И компилируем код:
Для просмотра ссылки Войдиили Зарегистрируйся
Теперь так же проверим через COFFLoader64.exe:
Для просмотра ссылки Войдиили Зарегистрируйся
или Зарегистрируйся, нужно будет сделать скрипт .axs. Изучив код из Для просмотра ссылки Войди или Зарегистрируйся с расширениями Adaptix, я взял за основу экстеншн Для просмотра ссылки Войди или Зарегистрируйся, который использует команду execute bof для запуска BOF.
Для просмотра ссылки Войдиили Зарегистрируйся
В первой части metadata нужно указать имя и описание расширения. Это удобно для управления, и сразу понятно, для чего нужен этот BOF. В нашем случае напишем что‑нибудь простенькое:
Далее создадим команду. Тут будем использовать ax.create_command(). Эта функция создает объект AxCommand, который будет преобразован в команду для агентов Adaptix C2:
AxCommand ax.create_command(string name, string description, string example = "", string message = "");
Дальше я буду использовать функцию PreHook. Это метод объекта AxCommand, обрабатывающий данные между вводом команды и отправкой данных на сервер:
setPreHook(function() handler)
handler(id, cmdline, parsed_json, ...parsed_lines) — это функция AxScript, которая принимает четыре аргумента:
В нашем случае это будет выглядеть так:
Затем создадим переменную bof_path, в которой будет храниться путь до объектного файла example.o:
Далее используем функцию execute_alias. Она передает command обработчику с указанным id. Это будет работать так, как если бы пользователь ввел команду в консоли агента:
void ax.execute_command(string id, string command, handler(task){} = nil);
Чтобы созданные команды можно было использовать в консоли агента, их необходимо объединить в группы команд с помощью функции create_commands_group. Затем регистрируем с помощью функции register_commands_group:
AxCommandsGroup ax.create_commands_group(string name, AxCommand[] commands);
В итоге получаем вот такой код:
Далее в Adaptix C2 перехожу в Script Manager.
Для просмотра ссылки Войдиили Зарегистрируйся
Выбираю Load New и загружаю скрипт.
Для просмотра ссылки Войдиили Зарегистрируйся
Теперь на виртуальной машине с Windows запущу маяк и попробую выполнить BOF.
Для просмотра ссылки Войдиили Зарегистрируйся
В выводе команды help можно увидеть наш BOF.
Для просмотра ссылки Войдиили Зарегистрируйся
Теперь введем команду my_get_pid_bof и получим результат.
Для просмотра ссылки Войдиили Зарегистрируйся
Проверим, что это соответствует действительности.
Для просмотра ссылки Войдиили Зарегистрируйся
Да, так и есть. Маяк запущен с идентификатором процесса 7540, и BOF выводит то же самое.
Например, для проведения фишинговой атаки тебе нужно разработать маяки, которые запускаются на захваченной машине и отстукивают какому‑то C2-серверу. Да, по своей сути маяк просто должен сохранять соединение с C2, и ты получаешь удаленный доступ к тачке. Но, как правило, тебе придется заниматься постэксплуатацией, эксфильтрацией, делать закреп на машине.
Тебе, конечно, не захочется переписывать маяк, раздувать его код и добавлять туда новые функции. Любую активность к тому же может запалить антивирус. Хорошо бы использовать инструмент, который уже будет обладать необходимыми фичами, а еще лучше, чтобы его можно было расширять в режиме онлайн. Именно для этого придумали Beacon Object File, или сокращенно BOF.
Beacon Object File — это концепция, которая впервые засветилась в 2021 году — в Cobalt Strike, самом известном фреймворке для постэксплуатации. Beacon Object File — это скомпилированная программа на языке C, которая может выполняться в процессе работы маяка и использовать внутренние Beacon API. BOF — это способ быстрого расширения агента новыми функциями.
BOF позволяет загружать в память нативный код во время выполнения маяка. Процесс похож на загрузку и выполнение DLL, но позволяет оставаться более незаметным. Сами BOF — это не исполняемые файлы, они представлены в форме Microsoft Common Object File Format (сокращенно COFF).
info
Common Object File Format (COFF) — это формат исполняемых файлов, файлов объектного кода и динамических библиотек.
Я решил проверить свой BOF на C2 Для просмотра ссылки Войдиили Зарегистрируйся, который разработал Для просмотра ссылки Войдиили Зарегистрируйся. Это достойная замена для Cobalt Strike и опенсорсного Для просмотра ссылки Войдиили Зарегистрируйся. Adaptix — это расширяемый фреймворк для постэксплуатации, разработанный специально для пентестеров. Сервер Adaptix написан на Golang, клиентский графический интерфейс — на C++ и Qt, что позволяет использовать его в Linux, Windows и macOS.
www
Для просмотра ссылки ВойдиЧто ж, давай сядем за клавиатуру и начнем писать свой первый простенький BOF.
warning
Статья имеет ознакомительный характер и предназначена для специалистов по безопасности, проводящих тестирование в рамках контракта. Автор и редакция не несут ответственности за любой вред, причиненный с применением изложенной информации. Распространение вредоносных программ, нарушение работы систем и нарушение тайны переписки преследуются по закону.Создаем скелет
Для начала создам директорию, которая называется bofs, и сделаю файл для BOF под названием example.c. Сперва просто создадим функцию, которая будет точкой опоры:
Код:
void go()
{
}
x86_64-w64-mingw32-gcc -c -o example.o example.c
Здесь -c — опция, которая сообщает компилятору о том, что это объектный файл.
Для просмотра ссылки Войди
Теперь введу file example.o и получу информацию о том, что это COFF object file.
Для просмотра ссылки Войди
Мы получили объектный файл, но он должен что‑то делать. Чтобы он начал работать с маяком, например Cobalt Strike или Для просмотра ссылки Войди
www
Для просмотра ссылки ВойдиПишем простой BOF
Открываем beacon.h и видим там полный набор всего необходимого. В самом начале находится Для просмотра ссылки ВойдиДля просмотра ссылки Войди
Дальше будем погружаться в API, но начнем с малого. Вот, например, функции, которые, видимо, способны выводить что‑то на экран:
Код:
DECLSPEC_IMPORT void BeaconOutput(int type, char * data, int len);
DECLSPEC_IMPORT void BeaconPrintf(int type, char * fmt, ...);
Есть еще несколько удобных штук, но в итоге мы хотим иметь возможность запускать функции Win32 API. Сначала, впрочем, убедимся, что у нас все компилируется и запускается. Предлагаю проверить на обычном Hello World. Для этого будем использовать BeaconPrintf.
В исходном коде example.c нужно включить beacon.h, используя обычный #include. Как можешь заметить, BeaconPrintf() требует как минимум два аргумента: тип (целочисленное число) и сама строка.
Первый аргумент ждет какое‑то число, и эти числа прописаны в beacon.h:
Код:
#define CALLBACK_OUTPUT 0x0
#define CALLBACK_OUTPUT_OEM 0x1e
#define CALLBACK_OUTPUT_UTF8 0x20
#define CALLBACK_ERROR 0x0d
Нам нужно CALLBACK_OUTPUT, а дальше просто вызовем BeaconPrintf(CALLBACK_OUTPUT, "Hello world!"):
Код:
#include "beacon.h"
void go()
{
BeaconPrintf(CALLBACK_OUTPUT, "Hello world!");
}
www
В Для просмотра ссылки ВойдиТеперь это надо скомпилировать, снова используя MinGW:
x86_64-w64-mingw32-gcc -c -o example.o example.c
Для просмотра ссылки Войди
Теперь встает вопрос: как это проверить? Вспомним, что это COFF, а не полный исполняемый файл. Чтобы запустить этот объектный файл без C2, можно использовать Для просмотра ссылки Войди
Сначала собираем COFFLoader:
Код:
git clone https://github.com/trustedsec/COFFLoader.git
cd COFFLoader
Для просмотра ссылки Войди
Тут мы можем увидеть Makefile, и нам достаточно выполнить make.
Для просмотра ссылки Войди
После этого получим COFFLoader64.exe. Теперь этот файл надо перенести на Windows и там протестировать свой BOF. Работать с COFFLoader64.exe очень просто: вводим COFFLoader64.exe go example.o и получаем результат.
Для просмотра ссылки Войди
Работа с Win32 API
Теперь наш BOF умеет что‑то делать, а мы научились его запускать. Но мы ведь хотели заняться постэксплуатацией и вызывать функции Win32 API, но BOF — это не совсем исполняемый файл. В нем нет таблицы импортов. Если захотим что‑то импортировать и использовать функции Windows, то BOF нужны Dynamic Function Resolutions. С ними мы сможем вызывать функции системных библиотек. DFR работает, добавляя к импортируемому символу префикс с именем библиотеки, из которой он импортируется.www
Для просмотра ссылки ВойдиНапример, есть вот такая функция:
DECLSPEC_IMPORT DWORD WINAPI NETAPI32$DsGetDcNameA(LPVOID, LPVOID, LPVOID, LPVOID, ULONG, LPVOID);
Тут указано, что вызывается функция DsGetDcNameA из NetApi32.dll. Разделитель — знак доллара.
Теперь попробуем сделать что‑нибудь очень простое, но с использованием Win32 API. К примеру, вызовем функцию GetCurrentProcessId(). Как с ней работать, посмотрим в Для просмотра ссылки Войди
Для просмотра ссылки Войди
Видим, что функция находится в библиотеке kernel32.dll и возвращает данные типа DWORD.
Теперь вернемся в наш исходный код (example.c) и объявим эту функцию с использованием специального синтаксиса DECLSPEC_IMPORT. Так BOF узнает, что мы хотим использовать эту функцию:
DECLSPEC_IMPORT DWORD KERNEL32$GetCurrentProcessId();
- KERNEL32 сообщает, что мы хотим использовать функцию из DLL kernel32.dll;
- DWORD — возвращаемый тип данных.
Код:
#include "beacon.h"
DECLSPEC_IMPORT DWORD KERNEL32$GetCurrentProcessId();
void go()
{
DWORD pid = KERNEL32$GetCurrentProcessId();
BeaconPrintf(CALLBACK_OUTPUT, "[!] Current Process ID: %ld", pid);
}
Использование %ld означает long int, что не противоречит DWORD.
Для просмотра ссылки Войди
И компилируем код:
x86_64-w64-mingw32-gcc -c -o example.o example.c
Для просмотра ссылки Войди
Теперь так же проверим через COFFLoader64.exe:
COFFLoader64.exe go example.o
Для просмотра ссылки Войди
Добавляем BOF в Adaptix C2
Попробуем добавить этот BOF в Adaptix C2. Судя по Для просмотра ссылки ВойдиДля просмотра ссылки Войди
В первой части metadata нужно указать имя и описание расширения. Это удобно для управления, и сразу понятно, для чего нужен этот BOF. В нашем случае напишем что‑нибудь простенькое:
Код:
var metadata = {
name: "my_bof_pid",
description: "my_bof_pid"
};
Далее создадим команду. Тут будем использовать ax.create_command(). Эта функция создает объект AxCommand, который будет преобразован в команду для агентов Adaptix C2:
AxCommand ax.create_command(string name, string description, string example = "", string message = "");
- name — имя консольной команды;
- description — описание команды;
- example — пример ввода;
- message — сообщение, которое будет отображено в консоли агента при отправке команды.
var cmd_my_get_pid = ax.create_command("my_get_pid_bof", "TEST MY BOF", "my_getpid_bof");
Дальше я буду использовать функцию PreHook. Это метод объекта AxCommand, обрабатывающий данные между вводом команды и отправкой данных на сервер:
setPreHook(function() handler)
handler(id, cmdline, parsed_json, ...parsed_lines) — это функция AxScript, которая принимает четыре аргумента:
- id — идентификатор агента, для которого указана команда;
- cmdline — команда отправлена с консоли агента;
- parsed_json — объект JSON, в который была преобразована команда после обработки;
- parsed_lines — массив строк, в который была преобразована команда.
В нашем случае это будет выглядеть так:
cmd_my_get_pid.setPreHook(function (id, cmdline, parsed_json, ...parsed_lines){}
Затем создадим переменную bof_path, в которой будет храниться путь до объектного файла example.o:
let bof_path = ax.script_dir() + "example.o";
Далее используем функцию execute_alias. Она передает command обработчику с указанным id. Это будет работать так, как если бы пользователь ввел команду в консоли агента:
void ax.execute_command(string id, string command, handler(task){} = nil);
- id — идентификатор агента;
- command — команда;
- handler — PostHook.
ax.execute_alias(id, cmdline, execute bof ${bof_path}, "Task: MY GET PID BOF");
Чтобы созданные команды можно было использовать в консоли агента, их необходимо объединить в группы команд с помощью функции create_commands_group. Затем регистрируем с помощью функции register_commands_group:
AxCommandsGroup ax.create_commands_group(string name, AxCommand[] commands);
- name — имя группы команд;
- commands — массив объектов AxCommand.
ax.register_commands_group(AxCommandsGroup group, string[] agents, string[] os, string[] listeners);
- group — объект AxCommandsGroup;
- agents — массив имен агентов, для которых будет доступна группа команд. Доступны варианты вроде beacon, gopher и так далее;
- os — массив строк с названиями операционных систем, для которых будет доступна группа команд. Доступны следующие варианты: windows, linux, macos. Если массив пустой, то группа команд будет зарегистрирована для всех операционных систем;
- listeners— массив строк с именами зарегистрированных листенеров, для которых будет доступна группа команд. Доступны следующие варианты: BeaconHTTP, BeaconSMB и так далее. Если указан пустой массив [], группа команд будет зарегистрирована для всех листенеров.
Код:
var group_test = ax.create_commands_group("my_bof_pid", [cmd_my_get_pid]);
ax.register_commands_group(group_test, ["beacon", "gopher"], ["windows"], []);
В итоге получаем вот такой код:
Код:
var metadata = {
name: "my_bof_pid",
description: "my_bof_pid"
};
var cmd_my_get_pid = ax.create_command("my_get_pid_bof", "TEST MY BOF", "my_getpid_bof");
cmd_my_get_pid.setPreHook(function (id, cmdline, parsed_json, ...parsed_lines) {
let bof_path = ax.script_dir() + "example.o";
ax.execute_alias(id, cmdline, execute bof ${bof_path}, "Task: MY GET PID BOF");
});
var group_test = ax.create_commands_group("my_bof_pid", [cmd_my_get_pid]);
ax.register_commands_group(group_test, ["beacon", "gopher"], ["windows"], []);
Далее в Adaptix C2 перехожу в Script Manager.
Для просмотра ссылки Войди
Выбираю Load New и загружаю скрипт.
Для просмотра ссылки Войди
Теперь на виртуальной машине с Windows запущу маяк и попробую выполнить BOF.
Для просмотра ссылки Войди
В выводе команды help можно увидеть наш BOF.
Для просмотра ссылки Войди
Теперь введем команду my_get_pid_bof и получим результат.
Для просмотра ссылки Войди
Проверим, что это соответствует действительности.
Для просмотра ссылки Войди
Да, так и есть. Маяк запущен с идентификатором процесса 7540, и BOF выводит то же самое.