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