DDoS

Материал из OpenWiki
Перейти к: навигация, поиск

Опыт противостояния 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:

  1. Блокируем, если с данного IP поступило более N запросов одного рабочего URL и не поступило ни одного запроса транзитного URL, из которого осуществляется переброс на рабочий URL.
  2. Блокируем, если с данного IP поступило более M запросов URL на который направлен флуд.
  3. Блокируем, если с данного IP поступило более I запросов контентообразующего URL, но не было ни одного запроса CSS, JS, GIF, JPG и прочих связанных с данным URL файлов.
  4. Автоматически определяем список 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`

#test
echo "Moving ${log_dir}/access.log to ${log_dir}/access.${date}.log"
echo "Sending USR1 to nginx with pid ${pid}"
echo "Archiving ${log_dir}/access.${date}.log"

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 в памяти.

#!/usr/bin/perl
# Блокируем если более 5 запросов "/" без запроса '/idx.html' или при запросе маски '/search/?str=...'
my %flag_hash=();
my %ua_hash=();
while(<>){
    my ($s_ip,undef,undef,undef,undef,$s_cmd,$s_req,undef) = split(/\s/,$_);
    if ($s_req eq '/' ){
        if (! defined $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;
    }
}
foreach my $ip (keys %flag_hash){
    if ($flag_hash{$ip} < 5){next;}
    print "$flag_hash{$ip} $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