DDoS
Содержание
Опыт противостояния DDoS атаке
В результате DDoS атаки был собран блэклист из участвующих в атаке примерно 140 тысяч IP и 400 западных подсетей по маске /16. Представленные примеры справедливы для ОС FreeBSD 6.x, которая была установлена на подвергшемся атаке сервере. В материале даются только общие рекомендации, о том как настроить FreeBSD для обработки нескольких десятков тысяч одновременных запросов можно прочитать здесь.
Первая стадия атаки
Наводнение UDP пакетами с интенсивностью забивающей внешний канал локального провайдера. Так как кроме web-сервера на атакуемой машине не было других сервисов и локальный rate-limit на UDP делать бесполезно, было принято решение по блокированию всего UDP трафика на атакуемый IP на маршрутизаторе магистрального оператора связи.
Вторая стадия атаки: HTTP наводнение
Различные попытки флуда по HTTP. Методы борьбы см. ниже.
Третья стадия атаки: SYN-flood
Наводнение большим колличеством SYN пакетов (SYN-flood). Машина справлялась нормально, но установленный rate-limit на машрутизаторе привел к 50% потере пакетов.
Решение: выявление через анализ лога tcpdump и блокирование на маршрутизаторе особенно агрессивных подсетей.
Для блокирования адресов участвующих в SYN-flood атаке можно использовать скрипт в котором в цикле запускается tcpdump с опцией "-c N" позволяющей завершить выполнение после получения N пакетов. Затем в скрипте вызывается код анализирующий число SYN запросов с одного IP и при значительном преобладании SYN пакетов, заносящего адрес в черный список.
Следует обратить внимание:
- необходимо позаботиться об исключении ситуации повторного блокирования адресов. Если IP ранее заблокирован, то его динамика запросов в логе tcpdump будет напоминать SYN-flood.
- исключение блокирования валидных адресов при нарушении функционирования сервиса. Порог блокировки должен быть достаточно высок.
Четвертая стадия: DNS-флуд
Флуд DNS серверов через наводнение запросами на резолвинг доменов состоящих из случайного набора символов (например, csdcscsaxas.org). 70% трафика была с фиктивного IP (спуфинг), совпадающего с IP атакуемого сайта, в надежде, что на маршрутизаторе не запрещен прием пакетов с локальным входящим IP, приходящих через внешний интерфейс. В таком случае, DNS сервер не блокировал бы рекурсивные запросы и отправлял ответ на IP сайта. Для внешних IP рекурсивные запросы были запрещены, но и при генерации "отлупов" DNS серверам пришлось не сладко, хотя больше пострадал внешний канал, который был полностью забит DNS-запросами.
Использованные методы противостояния HTTP-флуду
Минимизация трафика: Предположим, флуд направлен на запрос "/", запросы приходят с неотличимыми от реальных пользователей "User Agent" и "Referrer". Вместо / подставляем redir.html:
<html> <head> <script> document.location='http://www.site.ru/idx.html'; </script> </head> <body> </body> </html>
Затем /idx.html перенаправляем на предкешированный в статику индекс, который обновляем, например, раз в 10 мин. Предусмотреть ситуацию когда backend не сможет выполнить запрос (например, в момент изменения тактики флуда) и в кэш могут попасть некорректные данные.
Перенаправление делаем средствами JavaScript, вместо редеректа через HTTP заголовок. Помогает для отсеивания роботов, которые прекрасно обрабатывают переброс по "302 Location:".
Другой вариант, установка при запросе "/", cookie через http header и дальнейшее блокирование запросов /idx.html пришедших без cookie.
Далее по логу http-сервера строим список блокировки, выявляя IP запросившие / больше N раз, но не перешедшие на /idx.html. Можно также учитывать запрос картинки, но есть вероятность заблокировать клиентов с отключенной загрузкой картинок. Поэтому лучше оценивать доп. загрузки .css или .js файлов, если не для них не выставлен большой "expires".
Варианты http-флуда
Запросы только одной страницы (запрашивают только индекс или определенную страницу)
- Страница всегда одна и та же (запрашивают только /).
- Со временем страница меняется (запрашивают /, потом /search, потом /news)
- Определение частоты запросов одного URI и дальнейший перевод его в статику.
- Предусмотреть возможность запроса одного ресурса с разными параметрами, например, открытие страницы поиска с разными ключевыми словами.
Запросы с правильной цепочкой открытия страниц (/ => /idx).
- При каждом запросе отличаются "User Agent", можно использовать этот факт (с одного IP приходят несколько запросов с разными User Agent). Но нужно быть осторожным и не заблокировать по ошибке работающих через NAT
- Превышение допустимого числа запросов в ед. времени: если больше 10 запросов одной страницы в минуту - блокируем.
Написание скрипта для автоматизации
Cкрипт для построения черного списка через периодический анализ лог файла HTTP сервера nginx:
- Блокируем, если с данного IP поступило более N запросов одного рабочего URL и не поступило ни одного запроса транзитного URL, из которого осуществляется переброс на рабочий URL.
- Блокируем, если с данного IP поступило более M запросов URL на который направлен флуд.
- Блокируем, если с данного IP поступило более I запросов контентообразующего URL, но не было ни одного запроса CSS, JS, GIF, JPG и прочих связанных с данным URL файлов.
- Автоматически определяем список URL на которые направлена атака и осуществляем предкэширование их в статику, с промежуточным пробросом через html заглушку-редирект малого размера.
Общие наблюдения
- Если очень много запросов приходят со странных и редких user agent. Можно блокировать.
- Драконовские методы:
- Построения списка подсетей /16 по большому числу запросов из них от разных IP.
- Если трафик в основном местный, подойдет подключение Geo базы с временным блокированием иностранных подсетей.
- Выявление злоумышленника инициировавшего DDoS атаку (надежда на то, что атакующий ошибется и засветит свой IP).
- Слежение за ICMP echo трафиком.
- Выявление странных и типовых проверок, предшествующих очередной волне атаки.
- Введение в заблуждение "странных" IP (например, только для определенного адреса выдавать другие данные в DNS) и наблюдение за реакцией по динамике флуда.
- В системе неплохо предусмотреть выполнение watch-dog деглюкера, запрещающего все кроме родной сети при большой патере пакетов. Нужен чтобы можно было зайти по SSH и проанализировать ситуацию. Блокировать и разрешать только вход администратора, если невозможно загрузить внешний URL с флаговым документом в течении N попыток, как только флаговый документ загрузился - отменять блокировку.
- Выявление фиксированной маски, типичной для данной атаки. Например, странные User Agent, отсутствие Referer или его несответсвтие истинному положению
(например, промежуточная страница-редирект перенаправляет на www.host.ru, а в referer постоянное значение без www), разные User Agent для запроса страницы и последующего запроса сопутствующих файлов.
Разные скрипты и настройки
Скрипты и настройки могут быть неоптимальными, так как написаны в условиях катастрофической нехватки времени. Но они реально помогли противостоять атаке.
block_ipfw.sh - скрипт для быстрой загрузки черного списка, выполняется как "cat черный_список.txt| ./block_ipfw.sh"
#!/bin/sh cat - | while read cnt ip; do echo ipfw table 2 add $ip 1 ipfw table 2 add $ip 1 done
Для составления списка /16 подсетей по черному списку IP можно использовать:
cat block1.txt block2.txt block3.txt | awk '{print $2}' | awk -F \. '{print $1 "." $2 ".0.0/16" }' | sort | uniq -c | sort -gr | head -n 300 > ./block_net.txt
Для разбора лога tcpdump:
cat tcpdump.log |awk '{print $3}'|cut -d'.' -f1,2,3,4|sort|uniq -c |sort -r -n|grep -vE "^111.222"
flood_rotate.sh - скрипт для ротации лога nginx во время флуда с автоматическим обновлением блэклиста (запускается из crontab с периодичностью 1-10 мин).
#!/bin/sh date=`date +"%Y%m%d-%H%M"` log_dir=/usr/local/nginx/logs cd ${log_dir} pid=`cat ${log_dir}/nginx.pid` mv ${log_dir}/access.log ${log_dir}/access.${date}.log kill -USR1 ${pid} # ниже "111.222" - локальная подсеть, фильтруется чтобы не блокировать по ошибке локальных клиентов ./grep_flood_ip.pl ${log_dir}/access.${date}.log |grep -vE "^111.222" > ${log_dir}/last_flood_ip.log cat ${log_dir}/last_flood_ip.log >> /etc/block1.txt cat ${log_dir}/last_flood_ip.log | while read cnt ip; do echo ipfw table 1 add $ip 1 /sbin/ipfw table 1 add $ip 1 done /usr/bin/gzip ${log_dir}/access.${date}.log
./grep_flood_ip.pl - скрипт для выявления участвующих во флуде IP на основе анализа лога. (В процессе работы был создан еще один сткрипт для отсеивания на основании непостоянства User Agent при запросах с одного IP, но этот скрипт где-то потерялся. Принцип работы тот же, но скрипт более требователен к памяти, так как кроме IP держит в памяти еще и User Agent)
#!/usr/bin/perl # Блокируем если более 5 запросов "/" без запроса '/idx.html' или при запросе маски '/search/?str=...' my %flag_hash; while(<>){ my ($s_ip,undef,undef,undef,undef,$s_cmd,$s_req,undef) = split; if ($s_req eq '/' ){ if (! exists $flag_hash{$s_ip}){ $flag_hash{$s_ip}=1; } elsif ($flag_hash{$s_ip} > 0){ $flag_hash{$s_ip}++; } } elsif ($s_req eq '/idx.html'){ $flag_hash{$s_ip}=0; } elsif ($s_req eq '/search/?str=...'){ $flag_hash{$s_ip}=999; } } while (my ($ip, $val) = each %flag_hash){ next if $val < 5; print "$val $ip\n"; }
/boot/loader.conf
vm.kmem_size=500000000 net.inet.tcp.tcbhashsize=2048 net.inet.tcp.syncache.hashsize=1024 net.inet.tcp.syncache.bucketlimit=100 kern.ipc.nmbclusters=131072 kern.maxdsiz="1610612736" kern.dfldsiz="1610612736" kern.maxssiz="1610612736"
/etc/sysctl.conf:
kern.ipc.somaxconn=8192 kern.maxfiles=100000 kern.maxfilesperproc=81000 kern.ipc.maxsockets=80000 net.inet.tcp.maxtcptw=90960 net.inet.tcp.delayed_ack=0 net.inet.udp.maxdgram=57344 # все пакеты на закрытый порт отбрасываются без отсылки RST net.inet.tcp.blackhole=2 net.inet.udp.blackhole=1 #максимальное количество ICMP Unreachable и TCP RST пакетов в секунду net.inet.icmp.icmplim=400 net.local.stream.recvspace=8192 net.local.stream.sendspace=32768 # На прием нет необходимости в большом буфере net.inet.tcp.recvspace=8192 net.inet.udp.recvspace=8192 # На отправку 32k достаточно. net.inet.tcp.sendspace=32768 net.inet.ip.fw.dyn_keepalive=0 net.inet.tcp.msl=7500 kern.polling.enable=1 # Чтобы избежать tamewait-ов при запросе к бэкенду net.inet.tcp.nolocaltimewait=1 net.inet.ip.portrange.randomized=0 net.inet.ip.portrange.first=10000 net.inet.ip.fw.dyn_max=40000
nginx.conf (часть конфига):
location = / { if ($args = %2F){ # Блокируем сразу, по маске параметров '%2F' return 403; break; } if ($http_user_agent ~ (inktomi.com|avastye.com|H010818|ODI3|FunWebProducts|looksmart.net|i.love.teh.cock|googlebawt.com|Microsoft-WebDAV-MiniRedir) ) { Блокируем сразу кривые User Agent return 403; break; } root /usr/local/apache/htdocs; #if (-f $document_root/redir.html) { # Вместо индекса выдаем статику с кодом переброса. rewrite ^.*$ /redir.html break; #} } location = /idx.html { # Второй уровень проброса rewrite ^.*$ /idx2.html break; } location = /idx_old.html { # Блокируем старый URL проброса, который раскусили боты. return 403; break; }
store_log.sh - скрипт для вывода разной системной статистики разом
#!/bin/sh # Динамика: #netstat -w 1 #iostat -w 1 #vmstat -w 1 #gstat echo net.inet.tcp.syncache sysctl net.inet.tcp.syncache echo "======================================" echo vmstat vmstat -z vmstat -i vmstat echo "======================================" echo iostat iostat echo "======================================" echo netstat netstat -m netstat -Lan netstat -s -p tcp #netstat -s -p udp echo "======================================" echo sysctl net.inet.ip.fw.*dyn* sysctl net.inet.ip.fw|grep dyn echo "======================================" echo "counters (connection/stablished/open sockets/open files)" netstat -an|wc netstat -an|grep ESTAB|wc sysctl kern.ipc.numopensockets sysctl kern.openfiles