Введение
В этой статье я хочу рассказать о достаточно очевидном, как мне кажется, способе фильтрации dns amplification attack, и о небольшом модуле, который был написан для реализации идеи.
О том, что такое dns amplification attack, писалось уже не раз, например здесь; многие с этим сталкивались, многие боролись, кто-то более успешно, кто-то менее. Атака строится на отправке DNS запроса к какому либо DNS серверу с подставленным ip адресом источника, равным ip адресу жертвы. Ответ от DNS сервера практически всегда больше, чем запрос, особенно учитывая, что атакующим обычно выполняется ANY запрос. AAAA записи — уже не редкость, SPF и другая информация в TXT записях, все это позволяет достаточно легко получить усиление в 5 и даже более раз. Для атакующего это выглядит очень заманчиво, можно устроить хороший dos, даже не имея большого ботнета. Можно очень долго рассуждать, почему возможен спуфинг ip адресов в Интернете, но реалии таковы, что он все еще возможен, поэтому на сегодняшний день задача затруднить использование своих DNS серверов в проведении подобных атак представляется весьма актуальной. Замечу также, что в данной атаке возможно использовать как авторитативные dns сервера, так и публичные резольверы; предлагаемое решение тоже может использоваться в обоих случаях.
Основные методы борьбы, применимые на dns серверах:
Заблокируем неугодных.
В принципе здесь нет ничего хитрого, возможность блокировать трафик при превышении количества проходящих в секунду пакетов есть во многих фаерволах. Можно блокировать по каким-либо правилам, как например было сделано в вышеупомянутой статье. Если нет желания использовать фаервол на dns серверах, можно раз в какой-то промежуток времени запускать tcpdump, парсить его вывод и направлять роутингом неугодный трафик в /dev/null. На крайний случай можно добавить ip атакующего на loopback интерфейс (данный приём рекомендовался И. Сысоевым на одной из конференций, как способ обойтись без фаервола на FreeBSD). Можно настроить зеркалирование трафика на свиче, анализировать его где-то отдельно и потом отправить результат пограничному роутеру для блокировки. Вариантов много, минус один — мы теряем часть трафика. Не забываем, что мы блокируем подставленный ip, а там вполне может быть что угодно, начиная dns серверами провайдера и заканчивая серверами вашей собственной организации.Попросим на TCP.
Заголовок DNS пакета имеет поле TC флага. Если TC флаг установлен, то клиент должен повторить запрос с использованием TCP, причем все остальные данные ответа игнорируются. Идея данного метода в том, что атакующий не будет переключаться на TCP, для него это не имеет смысла, в то время как честный клиент переключится и получит ответ по TCP. Конечно, TCP для DNS — это медленнее, но, во-первых, ответ должен осесть в кеше рекурсора или клиента, во-вторых, некоторая латентность в данном случае — это меньшее зло. Данный подход уже реализован в некоторых dns серверах: так, в powerdns можно выставить tc флаг для ответов на ANY запросы, что уже является хорошим компромиссом. Но данный вариант также не идеален. Дело в том, что в большом Интернете все еще есть сервера, не совсем следующие RFC или просто неверно настроенные, и, просто выставив TC для всех ответов, никто не даёт гарантии, что все корректно переспросят ответ по TCP. Также не стоит забывать, что, выставив tc флаг, мы, конечно, снизим количество исходящего трафика и следовательно нагрузку на сетевое оборудование, но вот сами сервера все так же будут обрабатывать этот гигантский входящий поток запросов, тратя драгоценные переключения контекста и грея дата-центры.
Собственно, появилась идея сделать что-то новое. Однако в защите от dos, как правило, идеальных средств не бывает, и предложенный вариант тоже не лишен недостатков: например, он совсем не защищает от использования сервера для отражения dns трафика без усиления. Тем не менее, плюсы предлагаемого решения состоят в следующем:
- Мы никого не блокируем, при превышении порога просто форсируем использование tcp для определенных ip адресов, т. е. выставляем tc флаг и отвечаем обрезанным dns ответом.
- Форсирование использования TCP выполняем в ядре.
Так же, хотелось сделать решение максимально простым в плане использования, без необходимости обязательного использования iptables, настройки каких-либо дополнительных правил и т. д. Модуль является Linux специфичным, но думаю, принципиально ничего не мешает реализовать идею и на FreeBSD.
Как это работает?
Считаем количество пришедших пакетов с каждого ip адреса в заданный период времени. Если количество пришедших пакетов с определенного ip превысило порог, то формируем UDP ответ с TC флагом и отбрасываем запрос. Таким образом мы резко снижаем количество переключений контекста, вызванного необходимостью обработки этого трафика приложением DNS сервера. Легитимный клиент, получив по udp ответ с tc флагом, будет вынужден повторить запрос по TCP, и данный трафик уже достигнет dns сервера.
Для эффективной реализации нам очень поможет тот факт, что формат заголовка для dns запроса и ответа одинаков, более того, заголовок — это единственная необходимая часть пакета для того, чтобы dns ответ считался корректным. Посмотрим на dns заголовок подробнее:
Еще одна хорошая новость: dns заголовок имеет фиксированный размер 12 байт. Получается очень простая схема, нам даже не надо полностью парсить dns заголовок. Проверяем, что в пришедшем на 53 UDP порт пакете присутствуют данные размером больше 12 байт, копируем первые 12 байт данных (пока писал, появилась мысль, что, возможно, следует дополнительно проверить остальные поля заголовка) из запроса в новый пакет, выставляем ему TC бит и бит ответа, и отправляем его обратно. Поскольку мы скопировали только заголовок, то желательно также обнулить поле QDCOUNT, в противном случае получим предупреждения парсера на клиентской стороне. Сам же запрос после этого удаляем. Всю эту работу можно провести прямо в NF_INET_LOCAL_IN хуке, в этом же хуке мы должны поместить ip источника в KFIFO очередь для дальнейшего обсчета статистики. Статистику приходящих пакетов будем считать асинхронно, в отдельном потоке, используя красно-черные деревья. Таким образом, мы вносим минимальную задержку в прохождение пакета — KFIFO является lock free структурой данных, к тому же очередь создается на каждый cpu. Правда, тут возникает необходимость конфигурировать интервал в зависимости от ожидаемого pps. Еще есть ограничение на размер памяти, выделяемой для per cpu данных: сейчас оно составляет 32kB, с учетом чего создаётся очередь размером 4096 ip адреса на каждый cpu. Таким образом, выбрав интервал в 100ms, получим возможность обсчитывать до 40960 pps на каждый cpu, что в большинстве случаев представляется достаточным. С другой стороны, переполнение очереди приведет просто к потере части данных для подсчета статистики.
Возникает закономерный вопрос: почему просто не использовать хеш?
К сожалению, неаккуратное использование хешей в таких местах открывает возможность для другого типа атак — атак, нацеленных на вызов коллизий: зная, что в каком-то критичном ко времени исполнения участке кода используется хеш, можно подобрать такие данные, что операция с ними на хеш таблице будет происходить уже за O(n) вместо O(1). Такие атаки неприятны еще и тем, что выявить их бывает достаточно сложно — внешне вроде ничего не произошло, а серверу стало плохо.
Если же pps с заблокированого ip стало меньше порогового значения, то блокировка снимается. Есть возможность сконфигурировать гистерезис, равный по умолчанию 10% от порогового значения.
В конце статьи приведена ссылка на проект; любые конструктивные замечания, пожелания и дополнения приветствуются.
Пример использования
В директории с собранным модулем выполняем
insmod ./kfdns.ko threshold=100 period=100 hysteresis=10
threshold — порог, при превышении которого будем выставлять tc флаг;
period — период подсчета, ms (т.е. в данном случае фильтр сработает, если получили более 100 пакетов с одного ip за 100ms);
hysteresis — разница между порогом срабатывания и порогом отпускания фильтра. Подсказка: если выставить hysteresis = threshold, то после срабатывании блокировки она никогда не будет снята, в некоторых случаях это может быть полезно.
После загрузки модуля в
cat /proc/net/kfdns
можно найти статистику по ip, попавшим под фильтрацию.
Результаты тестирования
Для создания паразитной нагрузки использовался dnsperf (в двух экземплярах, один на соседней виртуальной машине, второй на ноутбуке, и к сожалению этого было даже недостаточно, чтобы загрузить систему до отказа), dns сервер был поднят в виртуальной машине KVM под управлением CentOS, в качестве самого dns сервера использовался pdns-recursor.
На графиках показаны значения счетчиков до активации модуля, после активации, и снова с выгруженным модулем. PPS во время всего эксперимента был на уровне 80kpps.
Итак, произошло то, чего мы добивались — сокращение исходящего трафика. Видно, что после включения модуля исходящий трафик стал даже меньше входящего,
что в принципе логично: не будем забывать, что мы копируем только заголовок.
Резкое уменьшение количества переключений контекста — хорошо.
А вот что при этом происходило с системой: видно заметное сокращение потребления system time, user time. Изменения steal time в данном случае — это влияние виртуализации, что тоже логично. А вот едва заметное увеличение irq time — это интересно и может быть поводом для дальнейших экспериментов.
Что хочется добавить в будущем?
- Работа из PREROUTING хука (или из FORWARDING, но нужно проверять). Это позволит использовать модуль не только на dns серверах, но и, к примеру, на балансерах, или пограничных фаерволах.
- Подготовка пакетов под основные дистрибутивы.
- Документация, best practice
Сам проект:
github.com/dcherednik/kfdns4linux
Пока проект находится в достаточно молодом состоянии, но надеюсь, что в Хабрасообществе найдутся заинтересованные люди, и, возможно, кому-то он будет полезен.
Ссылки и литература:
Linux netfilter Hacking HOWTO
Writing Netfilter Modules
Unreliable Guide To Locking
ISBN 978-5-8459-1779-9 Роберт Лав, Ядро Linux: описание процесса разработки