Пример: MCR UDP
Это пошаговый пример, как написать контрмеру MCR с аутентификацией по UDP (описание протокола), чтобы разобраться с программированием контрмеры BPF. Готовая программа займет менее 100 строк.
Вам понадобятся:
- MITIGATOR v20.06 и выше.
- Умение запускать команды из терминала.
- Минимальные знания языка C (будут пояснения по ходу текста).
- Знание, что у пакетов есть протоколы, заголовки, данные.
Настройка окружения
Подойдет любая операционная система, рекомендуется Linux. Нужен текстовый редактор для написания кода и компилятор. В качестве редактора можно взять Visual Studio Code или любой другой. Для компиляции в EBPF нужно установить Clang. На Debian и Ubuntu это можно сделать командой:
Первая программа
Создайте каталог для программ
и скачайте в него mitigator_bpf.h (ссылка).
В этом же каталоге создайте mcr.c с таким текстом:
ENTRYPOINT отмечает функцию, с которой начнется выполнение. Она принимает
контекст фильтрации ctx, его нужно передавать в большинство функций,
которые вызывает программа. Программа возвращает желаемое действие над пакетом,
в данном случае — сбросить его (RESULT_DROP). То есть наша программа
сбрасывает все пакеты. Имя функции не имеет значения, но это не может быть
main().
PROGRAM_DISPLAY_ID(...) задает короткую строку, которая будет отображаться
в интерфейсе пользователя при загрузке программы. Она обязательна, формат
произвольный. По ней пользователь понимает, какая программа загружена.
Чтобы скомпилировать программу, откройте терминал в каталоге с ней и введите:
Если все в порядке, никакого вывода не будет, но появится mcr.o — объектный
файл.
Файл mcr.o можно загрузить в контрмеру BPF и включить её. Если теперь
пустить в политику любой трафик, он будет сброшен. Захватив его функцией
PCAP, можно убедиться, что это сделано контрмерой BPF, то есть нашей
программой.
Автоматизация разработки
Чтобы не вводить и не искать команду каждый раз, можно поместить ее в скрипт
build.sh (Linux и Mac) или build.bat (Windows). На Linux и Mac скрипт
требуется сделать исполняемым:
Более профессионально сделать Makefile следующего содержания (для отступов
используются TAB, а не пробелы — это важно):
После этого сборку можно запускать командой make. Некоторые редакторы
обнаруживают Makefile автоматически и включают «горячие клавиши» для сборки.
Совет
У реальных программ обычно будет много версий, исправлений, вариантов.
Вместо размножения файлов типа mcr_1.c, mcr_2_fix.c и т. п. рекомендуем
изучить систему контроля версий Git (не требуется для продолжения).
Пропуск по таблице проверенных клиентов
Контрмера должна пропускать пакеты только с IP клиентов, прошедших проверку MCR. Для этого нужно хранить список (таблицу) проверенных клиентов.
Каждой программе MITIGATOR предоставляет таблицу на 1 млн. записей. У записи есть 8-байтовый ключ, 8-байтовое значение и время последнего обновления. По ключу записи можно находить, а в значении хранить связанные данные. Также по ключу можно добавить или обновить запись, при этом время последнего обновления актуализируется.
Зачем нужно время обновления? Дело в том, что записи удаляются из таблицы только в фоновом режиме, когда они слишком долго не обновлялись («время жизни» записей настраивается для политики защиты). Программа должна регулярно обновлять записи, которые требуется сохранять в таблице, даже если их данные не меняются.
Чтобы пропускать только пакеты с IP, находящихся в таблице, нужно:
- Извлечь из пакета адрес отправителя.
- Найти этот адрес в таблице.
- Если адрес найден, пропустить пакет, иначе сбросить его.
Получение адреса отправителя
Адрес отправителя находится в заголовке IPv4, Mitigator предоставляет программам функцию, чтобы получить этот заголовок:
Как и все остальные функции, packet_network_header() описана
в документации.
Он описывается структурой:
Добавим в начало функции mcr() получение заголовка:
Информация
Мы сразу используем packet_network_header(), потому что обрабатываем
только трафик IPv4. Иначе нужно было бы сначала проверить
packet_network_proto() и работать с IPv4 или IPv6 в зависимости от ответа.
Поиск в таблице
Для поиска записи по ключу есть две функции:
Они отличаются тем, что table_get() освежает время обновления записи, если
она найдена. Именно это и нужно в MCR: запись должна сохраняться, пока есть
пакеты от клиента.
Контекст уже есть, в качестве ключа будет адрес отправителя (TableKey — это
целое 64-битное число). Если запись с таким ключом есть в таблице, функция
вернет true и запишет в переменную типа struct TableRecord найденное
значение и время последнего обновления. В этом случае нужно вернуть из
программы RESULT_PASS — указание пропустить пакет. Функция приобретает
вид:
Скомпилируйте программу и загрузите новую версию в MITIGATOR.
Тестирование фильтрации по таблице
Наша программа пока не добавляет проверенных клиентов в таблицу, но можно
сделать это вручную через интерфейс контрмеры BPF. Ключ записи вводится
как набор байтов в шестнадцатеричной форме. Например, нужно добавить
адрес 192.0.2.1, его байты: 0a 00 02 01. Значение можно оставить нулевым,
оно не используется программой.
Удобные команды для конвертации:
- Если адрес набирается вручную:
printf "%02x %02x %02x %02x\n" 192 0 2 1 - Если адрес можно вставить или если есть список адресов:
tr -d \n | tr . ' ' | xargs -n4 printf "%02x %02x %02x %02x\n"
Если теперь направить трафик с добавленного адреса, он будет пропускаться: а любой другой — сбрасываться.
Информация
При загрузке новой программы старая таблица сохраняется. Это позволяет поменять логику работы без сброса списка проверенных клиентов. Если это не нужно, например, при отладке, таблицу можно и нужно сбрасывать вручную.
Классификация пакетов
Пакеты, которые обрабатывает контрмера, попадают под один из пяти случаев:
-
Если протокол не UDP, проверить IP отправителя и пропустить либо сбросить.
-
Если протокол UDP:
- Если длина не 400, проверить по таблице (см. выше).
- Если данные начинаются с
MCRH3110, отправить обратно пакет с испытанием. - Если данные начинаются с
MCRR, проверить ответ на испытание. - Иначе проверить по таблице (см. выше).
Наметим структуру программы: будем обрабатывать каждый случай в отдельной
функции. Имеющаяся программа уже реализует проверку по таблице. Так как эта
логика повторяется, она вынесена в функцию process_other().
Макрос LOCAL необходим для всех вспомогательных функций,
иначе компилятор может сгенерировать код, который не пройдет валидацию.
Протокол проверяется packet_transport_proto() вместо анализа заголовка IPv4.
Это более универсально, немного быстрее и демонстрирует еще одну функцию.
При обработке UDP необходимо анализировать длину и данные пакета (payload), для получения которых есть специальная функция:
Сначала проверим длину данных:
Для удобства запишем сигнатуры сообщений MCR в константы:
Чтобы не сравнивать каждый байт сигнатуры с соответствующим байтом данных,
применим трюк. Будем считать, что payload указывает не на 8 байт сигнатуры,
а на единое 8-байтовое число (uint64_t), аналогично для массива MCRH3110.
Далее сравним эти числа, то есть 8-байтовые блоки:
Аналогично для 4-байтовой сигнатуры MCRR:
Если ни одна из сигнатур не обнаружена, пакет обрабатывается в общем порядке:
Тестирование классификации пакетов
Скомпилируйте программу и загрузите её в Mitigator.
Можно заметить, что отображаемая версия поменялась на ...v0.2.
Пакеты ping должны всегда сбрасываться. Если этого не происходит,
убедитесь, что таблица пуста.
Для проверки можно запустить на защищаемом сервере скрипт, имитирующий проверяющую сторону MCR:
Тогда скрипт-клиент MCR должен быть способен авторизоваться, так как на данном этапе программа клиентские пакеты MCR пропускает (192.0.2.1 — адрес сервера):
Реализация протокола MCR
Выдача испытания
Опишем сообщение с испытанием MCR в виде структуры:
Через эту структуру будем заполнять содержимое ответа:
Первые четыре байта нужно заполнить сигнатурой MCRC:
Теперь нужно сгенерировать cookie:
- привязанное к клиенту, то есть к его потоку UDP;
- действующее ограниченное время;
- включающее секретные данные, чтобы его нельзя было подделать.
Это типовая задача для механизмов типа challenge-response, поэтому для нее MITIGATOR предоставляет специальную функцию:
Структура Flow описывает поток данных, к которому относится пакет, например,
для UDP это IP отправителя и получателя, исходящий и целевой порт. Здесь flow
определяет привязку к клиенту. В случае MCR UDP можно использовать все признаки
потока. Напротив, если бы клиент мог менять порт при ответе, его не нужно
было бы учитывать. Чтобы cookie не зависело от части значений struct Flow,
эти поля необходимо занулить. Тип Cookie — 32-битное целое число, то есть
uint32_t, как и нужно в пакете.
Для формирования struct Flow есть функция:
Заполним cookie в ответе:
Пакет нужно отправить обратно, то есть результат функции — RESULT_BACK.
Однако перед этим нужно обрезать его до длины 8 байтов:
Проверка ответа
Аналогично пакету испытания опишем пакет ответа для process_mcr_response():
Сигнатура ответа уже проверена в process_udp(). Нужно проверить, что cookie
из ответа соответствует значению, которое было сгенерировано cookie_make()
для этого клиента (для его struct Flow). Для этого предназначена функция
cookie_check():
Далее необходимо проверить результат испытания, используя секретный ключ. Для простоты сначала сделаем ключ константным и фиксированной длины:
Выполняем расчеты, согласно описанию протокола:
В качестве значения используется 0, так как оно не учитывается при проверке.
Параметры программы
Менять константный ключ сложно: надо перекомпилировать программу и заново загрузить её в MITIGATOR. Для параметров каждой программе предоставляется 1 килобайт памяти, указатель на который можно получить так:
После этого изменения секретный ключ можно задавать настройками контрмеры BPF, не меняя программу. Если параметры не заданы или программа считывать больше данных, чем было задано в параметрах, недостающая часть заполняется нулями.
Тестирование работы MCR
Скомпилируйте программу и загрузите в контрмеру, включите её.
Задайте параметры программы через настройки контрмеры. Как и ключи таблицы,
параметры задаются как байты в шестнадцатеричном виде. Например, mysecret
записывается как 6D 79 73 65 63 72 65 74. Команда для преобразования:
Если запустить ping 192.0.2.1 (адрес сервера), все пакеты будут сброшены.
Если запустить скрипт-клиент с правильным ключом, IP-адрес клиента будет
добавлен в таблицу проверенных. Повторная команда ping покажет, что пакеты
с адреса клиента пропускаются.