LIMB
Вход Регистрация

тема

Cobalt Strike — реализация ICMP с использованием External C2

DESTR0Y · · 0 ответов · 278 просмотров

АВТОР СТАТЬИ: Ryan Kleffman
Ссылка на оригинал: 

Cobalt Strike - Implementing ICMP using External C2

Being able to build your own C2 layers has always fascinated me - and you can imagine my excitement when I discovered the ability to do this with Cobalt Strike’s External C2 - so I decided to explore it a bit!
Не удалось прогрузить медиа
ryanq47.github.io

Переведено для LIMB.cc​


Меня всегда завораживала возможность создавать собственные C2-слои, и вы можете представить, какое у меня было волнение, когда я обнаружил, что это можно сделать с помощью External C2 в Cobalt Strike — так что я решил немного поэкспериментировать!

Первоначальная идея:​

Изначально я просто хотел поэкспериментировать с External C2 и изучить, на что он способен. Работая с примером, предоставленным Fortra, у меня появилась идея попробовать создать собственный коммуникационный слой. Я давно хотел построить C2-канал через ICMP — так как в матрице C2 указаны всего две существующие реализации (INNUENDO и Nighthawk) — и этот проект стал идеальной возможностью для этого.

Из всех типов ICMP-сообщений я остановился на Echo Request (Тип 8) и Echo Reply (Тип 0), так как они обладают рядом преимуществ.
​
• Широкое разрешение в сети. Сообщения ICMP Echo обычно разрешены во многих сетевых средах — особенно для исходящего трафика — что делает их менее подверженными блокировке по сравнению с нестандартными TCP или UDP портами
• Встроенные поля. Сообщения Echo Request и Echo Reply содержат простые, но полезные поля, такие как идентификатор, номер последовательности и полезная нагрузка. Их можно использовать для лёгкой передачи сообщений, отслеживания и упорядочивания в простом C2-канале. Хотя структура ограничена, этого достаточно для таких простых задач, как beaconing или опрос команд.
• Поддержка ОС по умолчанию. Практически каждая операционная система — включая Windows, macOS и Linux — имеет встроенный стек ICMP. Для отправки и приёма ICMP-пакетов не нужны специальные библиотеки или драйверы.

Реализация​

Все сверхтехнические детали можно найти здесь, в README моего репозитория

Для справки, вот схема потока External C2, предоставленная Fortra

Не удалось прогрузить медиа
​

На высоком уровне клиент выполняет следующие действия:

1. Запрашивает у Controller бинарный файл Beacon.
2. Загружает Beacon в память и обрабатывает проходящий через него трафик.
3. Осуществляет проксирование трафика Beacon между Controller и TeamServer

Достаточно типичный процесс. Однако отличие здесь заключается в том, что взаимодействие осуществляется по протоколу ICMP:

1. Клиент внедряет данные в сообщение Echo Request и отправляет его контроллеру.
2. Контроллер внедряет ответ в сообщение Echo Reply и возвращает его обратно

Крайне простое решение, однако оно позволяет организовать канал связи, используя исключительно исходящий ICMP-трафик

ICMP_PAYLOAD_SIZE = 1000​

В обоих файлах client_x86.c и controller.py — мы жёстко задаём значения:

C:
#define ICMP_PAYLOAD_SIZE 1000
#define TAG_SIZE          4      // “RQ47”

Это означает, что поле данных каждого ICMP-пакета может содержать до 1000 байт. Из этих 1000 байт первые 4 байта всегда отводятся под TAG (RQ47). Следовательно:

Максимальный объём данных в одном пакете: 996 байт (1000 – 4 байта под TAG)
Если нужны меньшие блоки (например, чтобы имитировать стандартный Windows ping (32 байта) или unix ping (52 байта)), можно изменить значение ICMP_PAYLOAD_SIZE (и обновить как клиент, так и контроллер). Я выбрал 1000, чтобы оптимизировать передачу больших объемов данных и не тратить много времени на отладку.

Итоговый размер пакета на линии: 20 байт (IP-заголовок) + (8 байт ICMP-заголовок) + (1000 байт данных) = 1028 байт

Давайте упростим:

Как работают клиент и контроллер, шаг за шагом:
Примечание: В этом примере используется значение по умолчанию для размера полезной нагрузки — 1000 байт. Если вы измените размер полезной нагрузки на 32, то каждый чанк будет всего 32 байта (или 28, если вычесть тег).
Инициализация клиента (запрос маячка)


Клиент открывает сырой ICMP сокет и готовится к связи с контроллером.
Чтобы запросить новый маячок, клиент отправляет пакетные ICMP Echo Request, буквально говоря: «Я ХОЧУ ПОЛЕЗНУЮ НАГРУЗКУ» (пожалуйста, измените это, если используете данный код и хотите быть тихими, я добавил это, потому что считал это забавным)

1. Seq 0 — пакет «только длина»:
Code:
[ ICMP Echo Request (Type 8), Seq=0 ]
Payload = [ “RQ47” (4 bytes) ] [ 4-byte big-endian integer: len("I WANT A PAYLOAD") ]

2. Seq 1 — пакет «команда»:
Code:
[ ICMP Echo Request (Type 8), Seq=1 ]
Payload = [ “RQ47” (4 bytes) ] [ “I WANT A PAYLOAD” (17 bytes) ]
Слушатель контроллера сначала увидит Seq 0 и зафиксирует, что ожидается 17 байт. Сразу после этого Seq 1 передает команду «I WANT A PAYLOAD».

Контроллер получает запрос
Сниффер контроллера обнаруживает ICMP Echo Request с Seq 0:
Code:
[ ICMP Echo Request (Type 8), Seq=0 ]
Payload starts with “RQ47”, next 4 bytes = 17

Он замечает, что после пакета с seq 0 должно последовать указанное количество байт (в данном случае 17). Для этого IP клиента и ICMP ID создается (или обновляется) обработчик.
Этот обработчик ждёт следующий пакет (или пакеты), пока не получит ровно 17 байт (особую команду).

Контроллер читает «I WANT A PAYLOAD» (Seq 1)
Обработчик видит ICMP Echo Request с Seq 1:
Code:
[ ICMP Echo Request (Type 8), Seq=1 ]
Payload starts with “RQ47”, next 17 bytes = "I WANT A PAYLOAD"

После распознавания точной строки «I WANT A PAYLOAD» контроллер подключается к TeamServer Cobalt Strike, выполняет рукопожатие и получает бинарный файл маячка (Beacon).
После получения байтов маячка контроллер готов отправить их обратно клиенту.

Контроллер → Клиент: Отправка маячка (Seq 0 → Seq N)
Перед передачей фактических данных маячка обратно клиенту контроллер объявляет его общий размер:

1. Seq 0 (Echo Reply)
Code:
[ ICMP Echo Reply (Type 0), Seq=0 ]
Payload = [ “RQ47” (4 bytes) ] [ 4-byte big-endian integer: beacon_length ]
Далее контроллер разбивает маячок на чанки размером до 996 байт каждый (потому что 1000 байт всего минус 4 байта тег). Для каждого чанка с номером i = 1…N:

1. Seq i (Echo Reply)
Code:
[ ICMP Echo Reply (Type 0), Seq=i ]
Payload = [ “RQ47” (4 bytes) ] [ up to 996 bytes of Beacon data (chunk i) ]
Например, если маячок длиной 1456 байт:
    Seq 0 → [RQ47][0x000005B0] (0x5B0 = 1456)
    Seq 1 → [RQ47][первые 996 байт]
    Seq 2 → [RQ47][оставшиеся 460 байт]

Клиент собирает и запускает маячок
Клиент отправляет ICMP Echo Request и слушает ICMP Echo Reply (которые содержат данные маячка). При получении ответа с Seq 0:
Code:
[ ICMP Echo Reply (Type 0), Seq=0 ]
Payload starts with “RQ47”, next 4 bytes = total_size (e.g., 1456)

Он выделяет буфер точной длины и вычисляет, сколько чанков нужно получить (ceil(общий_размер / 996)).
По мере поступления каждого следующего Echo Reply (Seq 1, Seq 2, …) клиент удаляет 4-байтовый тег и копирует данные в правильное место в буфере. Когда все куски собраны, у клиента есть полный бинарный файл маячка.
Затем клиент выделяет исполняемую память, копирует туда байты маячка, меняет разрешения на выполнение и запускает поток для выполнения маячка полностью в памяти.
Если клиента поймает антивирус или EDR — скорее всего, это будет здесь.
Проксирование маячка (Продолжающаяся коммуникация, Seq > 0)
После запуска маячка он общается через именованный канал (например, \\.\pipe\foobar). Каждый раз, когда маячок записывает кадр в этот канал (длину + полезную нагрузку), клиент снова использует тот же механизм передачи данных по чанкам через ICMP для обмена данными туда и обратно:
Seq 0 – Объявляет «длину следующего кадра»

Code:
[ ICMP Echo Request (Type 8), Seq=0 ]
Payload = [ “RQ47” (4 bytes) ] [ 4-byte big-endian integer: frame_length ]

Seq 1…M – Отправляет до 996 байт данных кадра в каждом пакете:
Code:
[ ICMP Echo Request (Type 8), Seq=i ]
Payload = [ “RQ47” (4 bytes) ] [ up to 996 bytes of pipe data ]

Каждый обработчик на стороне контроллера читает эти фрагменты, собирает полный кадр и пересылает его на TeamServer по TCP.
Любой ответ от TeamServer немедленно разбивается на чанки(≤ 996 байт), и контроллер отправляет их обратно как ICMP Echo Reply с соответствующими номерами последовательности.
Клиент собирает эти ответы таким же образом и записывает их в именованный канал маячка.
Этот цикл продолжается до тех пор, пока маячок или клиент не будут завершены.

Трудности​

Разбиение на чанки:
Разбиение на чанки было самой сложной частью всего проекта. Поиск хорошего протокольного решения, которое позволило бы двунаправленную связь по ICMP с переменной длиной полезной нагрузки, занял большую часть времени.
Изначальный прототип отправлял только один ICMP-пакет за раз, что ограничивало максимальную длину полезной нагрузки 1472 байтами (MTU 1500 минус 20 байт заголовка IPv4 и 8 байт заголовка ICMP). Это работало для небольших передач, таких как выполнение команды ls, но для более сложных задач, например запуска Mimikatz, не подходило, так как данные, отправляемые маячку, превышали лимит одного пакета. Помимо этого, такой подход создавал сильный шум, мог привести к ошибкам типа “ping of death” с фрагментацией и был вообще не гибким.
К счастью, после тщательного обдумывания и помощи от ChatGPT, удалось реализовать описанный выше протокол разбиения на части, который прекрасно работает. Он полностью прозрачен для маячка и достаточно гибок, особенно благодаря доступным опциям конфигурации.

Демонстрация работы​

Меня всегда раздражает, когда в README-файлах или блогах не показывают работу инструмента на практике — поэтому вот видео, демонстрирующее, как это работает:

Не удалось прогрузить медиа

Планы на будущее:​

1. Укреплённый клиент с техниками обхода
Я хочу создать более устойчивого клиента, который будет включать проверенные техники обхода. В настоящее время клиент не пытается скрываться и быстро обнаруживается современными решениями EDR. Добавление хотя бы базовых техник обхода, таких как anti-debug проверки и использование динамического разрешения функций, значительно улучшит текущий дизайн.

2. Расширение Cobalt Strike для нестандартных протоколов
Также я хочу превратить эту работу в полноценное расширение для Cobalt Strike, которое будет поддерживать более широкий спектр протоколов помимо стандартных. Встроенные каналы покрывают большинство сценариев, но зачастую кажутся предсказуемыми и легко поддаются фингерпринтингу. Добавление нишевых или кастомных протоколов — это интересный проект, который может действительно найти применение.

Переведено для LIMB.cc​


Ресурсы​

Ссылка на репозиторий этого проекта здесь:
Вот дополнительные ресурсы, которые были полезны в процессе работы над проектом:
👍12 😁2 👏2

Гостевой просмотр. Чтобы отвечать и ставить реакции, войдите или зарегистрируйтесь.