C.4. Интеграция пакетного фильтра с програмным окружением

[+]C.4.1. Активное реагирование на события на примере борьбы с атаками bruteforce на SSH
[+]C.4.1.1. Способы защиты от bruteforce атак
[+]C.4.1.2. Постановка задачи
[+]C.4.1.3. Сбор данных через syslog
[+]C.4.1.4. Блокировка в пакетном фильтре
[+]C.4.1.5. Обработчик событий
[+]C.4.1.6. Скрипт и его применение

C.4.1. Активное реагирование на события на примере борьбы с атаками bruteforce на SSH

Здесь я приведу пример интеграции пакетного фильтра с програмным окружением существующим в BSD, на примере борьбы с атаками brute-force — лобовым подбором паролей к машине. Любая система с запущенным на ней sshd(8) демоном подвергается многочисленным атакам brute-force. Если в вашей системе присутствуют пользователи со слабыми паролями, в виду многочисленности таких атак вас рано или поздно взломают. Существует множество способов защиты от атак brute-force. Их обилие свидетельствует о том, что все они плохи.

C.4.1.1. Способы защиты от bruteforce атак

Перенос сервера sshd(8) на альтернативный порт
Как ни странно, это приводит к существенному снижению нагрузки brute-force. Однако данный метод не очень удобен — надо всем пользователям объяснять, что они должны заходить на нестандартный порт. Большинство программ-клиентов можно обучить заходить на некоторый хост через конкретный порт. Однако администратору придётся всё это объяснять пользователям... Кроме того, некоторые брандмауэры со стороны клиента могут блокировать выход на неизвестные им порты.
Блокировка взламываемого пользователя

Обычно такой метод реализуется через систему PAM. В Linux для этих целей есть специальный модуль — pam_tally. В BSD никаких аналогов я не знаю, однако его не сложно реализовать при помощи модуля pam_exec. Идея состоит в том, что в некоторой базе данных фиксируются попытки неудачного захода от каждого пользователя. Если оказывается превышен некоторый лимит, т.е. пользователь предпринял более 5 неудачных попыток входа, он блокируется и разблокировать его может только суперпользователь.

Данный метод неудачен во всех отношениях: главный его недостаток — идеологический. Модуль наказывает не атакующего, а атакуемого. Задача защиты поставлена с ног на голову. Некоторые brain-damaged администраторы всерьёз предполагают, что таким образом можно защититься от атаки. Это не так:

  1. Этот метод не защищает от атак вообще! Задача атакующего состоит в том, чтобы осуществить наибольшее количество попыток входа в систему. Поэтому, если у атакующего есть словарь из 10000 пользовательских имён, он сможет не варьируя пароль перебрать всех пользователей системы и pam_tally никак этому воспрепятствовать не может.
  2. Если у вас 10000 пользователей и вы выставили предел на количество неправильных попыток ввода пароля в 5, злоумышленник сможет осуществить 50000 попыток взлома, после чего ВСЕ ПОЛЬЗОВАТЕЛИ В ВАШЕЙ СИСТЕМЕ ОКАЖУТСЯ ЗАБЛОКИРОВАНЫ. А злоумышленник будет безнаказан и когда вы всех разблокируете, всё повторится.

У модуля pam_tally есть многочисленные дополнительные опции, которые позволяют сделать его работу более или менее вменяемой. Например, можно запретить блокировку root'а и обеспечить авторазблокирование по истечении некоторого периода времени. Однако модуль неправильно задуман, при его проектировании была сделана неправильная посылка, это неисправимо.

[Замечание]Замечание
SSH может не использовать модули PAM вообще. Проверьте содержимое опции UsePAM в конфигурационном файле /etc/ssh/sshd_config.
[Замечание]Замечание
OpenBSD не использует систему PAM вообще. Похожего функционала в ней можно, вероятно, добиться через пользовательские стили аутентификации, описанные в Раздел F.2, «/etc/login.conf в OpenBSD».
Явный перечень разрешённых адресов клиентов и имён пользователей: PAM, SSH, брандмауэр

В настройках демона sshd(8) можно задать опции AllowUsers, AllowGroups, DenyUsers и DenyGroups. Таким образом, вы можете явно указать какие пользователи могут пользоваться сервисом SSH на вашей системе. Это полезно, если вам надо обеспечить доступ для одного-двух «судоеров» (уполномоченных пользователей). Но если речь идёт о машине предоставляющей shell-хостинг, такая схема не подходит.

[Замечание]Замечание
Независимо ни от чего, безусловно надо выставлять опцию PermitRootLogin в No. Это сделано по умолчанию во всех системах BSD, но многие Linux-системы по умолчанию допускают bruteforce пользователя root! Особенно это относится к разнообразным Desktop'ам.

Большего функционала можно добиться при помощи модулей PAM. В FreeBSD при помощи модуля pam_login_access можно указать какие пользователи с каких хостов и с каких терминалов могут входить в систему. Эта информация хранится в файле /etc/login.access(5).

Наконец, вы можете в брандмауэре открыть проход на 22-й порт только с некоторых IP-адресов, однако в этом случае у вас не будет информации о логинах. Таким образом, этот метод хуже метода с использованием PAM, так как менее гибок, однако он больше экономит ресурсы системы.

Все эти способы ограничения атак имеют один общий недостаток — они неприменимы в случае, если shell доступ надо предоставить всем, или почти всем и в случае, если пользователи склонны перемещаться в пространстве и заходить в систему из разных мест.

Ограничение частоты коннекта

Сканированием интернета занимаются роботы. Есть сообщения, что некоторые роботы прекращают работу, если брандмауэр блокирует сессию. Мы можем попробовать в брандмауэре блокировать сессии, если они образуются чаще, чем три раза в минуту, например. Т.е. средствами брандмауэра запретить устанавливать сессию чаще чем три раза в минуту. Это может отпугнуть некоторых роботов.

Данное действие надо осуществлять на уровне брандмауэра, а не на уровне модуля PAM. В пакетном фильтре этого функционала можно добиться при помощи опции max-src-conn-rate (см. Раздел C.2.1.4.7, «Опции таблицы состояний»).

Идеологически данный метод плох тем же, чем и метод с pam_tally — наказывается не атакующий, а атакуемый. Кроме того, мне кажется, что эффективность такого метода сильно преувеличена. Когда я блокирую у себя злоумышленника, он продолжает попытки достучаться до сервера ещё в течение получаса.

Блокировка по факту подбора фальшивых имён пользователей

Сервер sshd(8) заносит сообщения в журнал от системы auth уровня важности info. По умолчанию они попадают в журнальный файл /var/log/auth.log:

Apr 19 00:18:22 house sshd[15877]: Invalid user adam from 83.19.31.123
Apr 19 00:18:25 house sshd[15879]: Invalid user stephen from 83.19.31.123
Apr 19 04:19:27 house sshd[22644]: Invalid user test from 86.54.112.218
Apr 19 04:19:28 house sshd[22646]: Invalid user guest from 86.54.112.218
Apr 19 04:19:28 house sshd[22648]: Invalid user admin from 86.54.112.218
Apr 19 04:19:29 house sshd[22650]: Invalid user admin from 86.54.112.218
Apr 19 04:19:35 house sshd[22660]: Invalid user test from 86.54.112.218
                

На основании этих записей можно на лету заносить адреса злоумышленников в чёрный список и блокировать в брандмауэре.

Эта идея тоже не лишена недостатков: за одним IP адресом может скрываться большое количество хостов (выходящих через NAT) и многие из них могут принадлежать добросовестным пользователям. Да и сам источник атаки, возможно, о ней не подозревает... Однако здесь «наказывается» атакующая сторона.

В сети немало готового програмного обеспечения, борющегося с атаками brute-force подобным образом. Как вариант, вы можете попробовать рассмотреть программу sshguard. (В FreeBSD см. порт security/sshguard.) sshguard незлопамятна — она не ведёт никаких собственных баз с вредными IP адресами и не занимается разбором существующих журнальных файлов. Она лишь осуществляет мониторинг текущих событий. Несомненным преимуществом sshguard является то, что она не громоздка — маленькая программка написанная на C, и при этом может взаимодействовать с пакетным фильтром OpenBSD, ipfw(8) FreeBSD и iptables(8) в Linux.

Мы рассмотрим как реализовать эту методику самостоятельно при помощи пакетного фильтра и дополнительного программного обеспечения: Python, SQLite.

C.4.1.2. Постановка задачи

Пусть имеется некоторый хост с запущенной а нём операционной системой BSD, всё равно какой: FreeBSD, OpenBSD, NetBSD или DragonFly BSD. В системе запущен демон sshd(8). Настройки SSH стандартны — демон слушает 22-й порт. На сервере запущен пакетный фильтр от OpenBSD.

Преступление:  Критерий атаки — 8 попыток аутентификации под несуществующим логином или одна попытка аутентификации под метапользователем (root, toor и системные сервисы).

Наказание:  Блокируем в брандмауэре доступ с «проштрафившегося» IP на 22-й порт сервера навсегда.

C.4.1.3. Сбор данных через syslog

В журнал syslogd(8) записи от демона sshd(8) попадают от средства auth и имеют уровень важности info. Сообщения о попытке логина от пользователя root выглядят следующим образом:

Mar 22 17:48:13 house sshd[16889]: error: PAM: authentication error for root from 218.26.165.234
Mar 22 17:49:48 house sshd[16893]: error: PAM: authentication error for root from 218.26.165.234
Mar 22 17:49:50 house sshd[16893]: error: PAM: authentication error for root from 218.26.165.234
Mar 22 17:51:38 house sshd[16916]: error: PAM: authentication error for root from 218.26.165.234
Mar 22 17:51:40 house sshd[16916]: error: PAM: authentication error for root from 218.26.165.234
          

Сообщения о попытке логина под несуществующим логином:

Apr 19 00:14:51 house sshd[15782]: Invalid user admin from 83.19.31.123
Apr 19 00:14:58 house sshd[15784]: Invalid user admin from 83.19.31.123
Apr 19 00:15:04 house sshd[15786]: Invalid user admin from 83.19.31.123
Apr 19 00:15:09 house sshd[15799]: Invalid user admin from 83.19.31.123
Apr 19 00:15:31 house sshd[15809]: Invalid user test from 83.19.31.123
Apr 19 00:15:36 house sshd[15811]: Invalid user test from 83.19.31.123
Apr 19 00:15:41 house sshd[15813]: Invalid user webmaster from 83.19.31.123
Apr 19 00:15:55 house sshd[15817]: Invalid user username from 83.19.31.123
          
[Замечание]Замечание
И пусть владельцы этих адресов не обижаются, что попали в Историю :) — логи настоящие.

Отлавливать IP адреса можно при помощи следующего регулярного выражения (здесь и далее применяется язык программирования Python):

# Следующие строки сгенерированы автоматически при помощи awk(1):
# awk -F: '$3<999 {printf("    \"%s\",\n", $1)}' /etc/passwd
# При использовании vim (:!cmd) не забудьте защитить знак '%'.
METAUSERS = [
    "root", "toor", "daemon", "operator", "bin", "tty", "kmem", "games",
    "news", "man", "sshd", "smmsp", "mailnull", "bind", "proxy", "_pflogd",
    "_dhcp", "uucp", "pop", "www", "mailman", "mysql", "alias", "cyrus",
    ]
METAUSERS_SQL = " or ".join(['login = "%s"'%x for x in METAUSERS])

# Регулярное выражение для поиска злоумышленников
SSH_BAD_USER = re.compile(r"""(?xm)
    ^(?P<date>(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\ +\d+\ \d+:\d+:\d+)\s
    house\s
    sshd\[\d+\]:\ (?:I(?:nvalid|llegal)\ user\ (?P<login>\S+)|
                     error:\ PAM:\ authentication\ error\ for\ (?P<metauser>%s))\s
    from\ (?P<ip>\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})
    """%"|".join(METAUSERS))
          

Для того, чтобы реагировать на события немедленно, можно записывать сообщения о событиях не только в файл, но и через pipe в скрипт. Конечно, мы могли бы поместить сценарий реагирующий на события в cron(8), и он бы разбирал журнальные файлы раз в полчаса, однако это привело бы к снижению в оперативности и к неоправданному расходу ресурсов системы. Демон syslogd(8) во многих дистрибутивах Linux умеет записывать только в именованный канал. Возможности syslogd(8) в BSD более привлекательны — он позволяет писать непосредственно на стандартный ввод программе, таким образом, он сам следит за тем, чтобы программа обработчик событий была запущена.

Итак, в файл /etc/syslog.conf(5) помещаем следующие строки:

auth.info;authpriv.info                         /var/log/auth.log
auth.info                                       | exec /root/bin/bfdefender -a
          

Здесь подразумевается, что /root/bin/bfdefender это программа обработчик событий, которая читает стандартный ввод, если вызвана с аргументом -a. Поскольку, команда, после знака | выполняется syslogd(8) демоном через sh(1), мы применили команду exec. Она замещает процесс sh(1) на bfdefender.

Вот как выглядит дерево процессов при использовании ключевого слова exec:

$ pgrep python2.5
90433
$ pstree -p 90433
-+= 00000 root [swapper]
 \-+= 00001 root /sbin/init --
   \-+= 25867 root /usr/sbin/syslogd -l /var/run/log -l /var/named/var/run/log 
     \--= 90433 root /usr/local/bin/python2.5 /root/bin/bfdefender -a
          

А вот как оно выглядит, если про команду exec забыть:

$ pgrep python2.5
90385
$ pstree -p 90385
-+= 00000 root [swapper]
 \-+= 00001 root /sbin/init --
   \-+= 25867 root /usr/sbin/syslogd -l /var/run/log -l /var/named/var/run/log 
     \-+= 90383 root sh -c  /root/bin/bfdefender -a
       \--= 90385 root /usr/local/bin/python2.5 /root/bin/bfdefender -a
          

C.4.1.4. Блокировка в пакетном фильтре

В пакетном фильтре необходимо создать две таблицы: таблицу с «хорошими» адресами, дабы случайно не заблокировать что-то не то, и таблицу с плохими адресами:

table <crackers> persist file "/etc/crackers"
table <trusted_ip> persist { \
   62.117.108.0/26\
   212.57.97.252\
}
.....
pass in quick on $ext_if inet proto tcp from <trusted_ip> \
    to $ext_if port ssh keep state
block in log quick on $ext_if inet proto tcp from <crackers> \
    to $ext_if port ssh
          

Таблицу <crackers> мы будем на лету модифицировать при помощи скрипта, обрабатывающего журнальные события.

# pfctl -t crackers -Ta 218.26.165.234 83.19.31.123
2/2 addresses added.
          

C.4.1.5. Обработчик событий

Что же должен делать обработчик событий?

  1. Обработчик должен вести некоторую базу в удобном (читай быстром) для себя формате.
  2. Он должен уметь инициализировать эту базу. Т.е. прочитать журнальные файлы и занести в базу уже накопленную информацию. При этом необходимо, чтобы база сохранялась от сеанса к сеансу.
  3. Обработчик должен уметь добавлять к базе новые события.
  4. Обработчик должен принимать решения о блокировке IP-адреса и заносить нарушителей на лету в таблицу пакетного фильтра при помощи pfctl(8).
  5. Наконец, обработчик должен заносить адреса в файл /etc/crackers из которого будет брать информацию пакетный фильтр при перезапуске.

В каком виде обработчик должен хранить промежуточную информацию? Сценарий на python(1) может хранить информацию в различных форматах. В виде конфигурационных файлов, в виде сериализованных объектов, во внешних базах данных, таких как MySQL и её аналоги. (Забегая вперёд, скажу, что сам я предпочёл вариант с SQLite.)

Недостатком конфигурационных файлов является то, что их трудно разбирать, это значит, что либо мы будем тратить несколько драгоценных секунд (а может быть и минут) при каждом запуске сценария, либо мы должны хранить уже «пережёванную» информацию.

Дело в том, что, согласно поставленной задаче, мы должны сохранять информацию не только о том, кто проштрафился (это список из нескольких сот IP адресов), но и информацию о том, под каким именем и сколько раз к нам подсоединялись злоумышленники. Это усложнит строение конфигурационного файла.

Пожалуй лучшее решение, которое тут можно предложить — на лету писать сценарий на python(1) и подгружать его в качестве модуля. Т.е. использовать сам python(1) в качестве парсера конфигурационного файла. Это и решит, отчасти, проблему со скоростью, и упростит резервное копирование. Однако при этом мы будем мусорить по диску прекомпилированными файлами .pyc и .pyo, которые будут содержать в себе секретную информацию, и нам придётся за всем этим безобразием следить. Не лучший вариант.

Мы можем хранить информацию при помощи сериализованных объектов python(1). Это позволит ещё сильнее ускорить запуск. Однако работать с таким файлом не очень удобно — его формат непрозрачен. Человек, разбирающийся с python(1) легко с ним сладит, но не многие сочтут такой способ удобным. (Хотя в том, что таковые найдутся — не сомневаюсь.)

Наконец, можно хранить данные во внешней базе данных.

Для этого мы можем использовать СУБД, типа MySQL или PostgreSQL. Однако такое решение представляется слишком громоздким и опасным — при выходе из строя базы данных (конструкция, как ни крути, сложная и капризная) наш програмный комплекс сломается.

Более разумным представляется использование сисемы SQLite. SQLite не требует никакого демона, Python будет обращаться непосредственно файлу с базой данных. Это достаточно быстро и весьма надёжно.

C.4.1.6. Скрипт и его применение

Я не могу здесь рассказывать о том, как программировать на Python. Эта задача явно выходит за рамки данного учебника. Здесь будет приведено описание принципов работы скрипта и HOWTO: как его применять. Сам скрипт можно найти по следующей ссылке: bfdefender — скрипт для защиты от bruteforce атак.

Для работы данного скрипта нам понадобится установить следующие порты: lang/python25, databases/sqlite3 и databases/py-sqlite3.

Для начала, скрипт надо запустить с опцией -i:

# ./bfdefender -i
          

Это приведёт к тому, что скрипт проанализирует все журнальные файлы /var/log/auth.log*, включая сжатые bzip2, и создаст базу данных /var/db/crackers. С этой базой в дальнейшем можно общаться при помощи утилиты-клиента sqlite3(1). Например, если нас одолеет любопытство, когда к нам пытались зайти используя логин закачивающийся на root и сколькко раз к нам пытались зайти под именем admin, мы можем сделать к базе SQL следующие запросы:

# sqlite3 /var/db/crackers
SQLite version 3.3.15
Enter ".help" for instructions
sqlite> .explain on
sqlite> .width 20 15 15
sqlite>  SELECT * FROM crackers WHERE login LIKE "%_root" LIMIT 10;
date                  ip               login          
--------------------  ---------------  ---------------
2007-03-28 14:14:56   89.149.195.132   11root         
2007-03-28 14:16:17   89.149.195.132   wwwroot        
2007-03-28 14:16:37   89.149.195.132   xxxroot        
2006-11-22 08:12:45   217.160.173.110  nfsroot        
2006-11-22 08:14:15   217.160.173.110  webroot        
2007-02-19 13:12:39   221.254.131.203  gamroot        
2007-01-25 02:15:24   148.244.79.94    cvsroot        
2007-01-25 02:15:27   148.244.79.94    cvsroot        
2006-10-23 23:36:17   208.57.150.227   gamroot        
2006-10-13 13:48:34   208.57.150.227   gamroot        
sqlite> SELECT COUNT(*) FROM crackers WHERE login = "admin";
COUNT(*)            
--------------------
1862                
sqlite> .exit
          

С помощью утилиты sqlite3(1) можно делать резервные копии базы, однако этот механизм в нашем случае внедрён прямо в скрипт.

Следующее действие — мы модифицируем файлы /etc/pf.conf и /etc/syslog.conf как было показано выше.

/etc/pf.conf:

table <crackers> persist file "/etc/crackers"
table <trusted_ip> persist { \
   62.117.108.0/26\
   212.57.97.252\
}
.....
pass in quick on $ext_if inet proto tcp from <trusted_ip> \
    to $ext_if port ssh keep state
block in log quick on $ext_if inet proto tcp from <crackers> \
    to $ext_if port ssh
          

/etc/syslog.conf:

auth.info;authpriv.info                         /var/log/auth.log
auth.info                                       | exec /root/bin/bfdefender -a
          

В crontab(8) помещаем следующее задание:

@monthly                                /root/bin/bfdefender -b
          

Это задание будет ежемесячно изготавливать резервную копию с нашей базы данных. Сама база, как говорилось, находится в файле /var/db/crackers, а резервная копия в файле /var/db/crackers.sql.bz2 — это обычный текстовый файл. Восстановить базу можно командой

# ./bfdefender -r
          

Это всё, что касается установки скрипта. Дополнительные возможности по его использованию, можно узнать передав скрипту параметр -h или --help. Скрипт позволяет собирать некоторую статистическую информацию:

# ./bfdefender -s
===========================================================================
                             bfdefender report                             
===========================================================================
Detected 62309 attacks
user root: 161
user toor: 0
user daemon: 0
user operator: 0
user bin: 0
user tty: 0
user kmem: 0
user games: 0
user news: 0
user man: 0
user sshd: 0
user smmsp: 0
user mailnull: 0
user bind: 0
user proxy: 0
user _pflogd: 0
user _dhcp: 0
user uucp: 0
user pop: 0
user www: 0
user mailman: 0
user mysql: 0
user alias: 0
user cyrus: 0
===========================================================================
2006 May:  3981 attacks
2006 Jun:  2098 attacks
2006 Jul:  2094 attacks
2006 Aug:  6397 attacks
2006 Sep: 11202 attacks
2006 Oct: 11982 attacks
2006 Nov:    12 attacks
2006 Dec:  8921 attacks
2007 Jan:  4861 attacks
2007 Feb:  8693 attacks
2007 Mar:  2068 attacks
===========================================================================
poplar logins: 20 top
admin ................ 1862
test .................. 985
guest ................. 558
apache ................ 427
user .................. 403
info .................. 295
tester ................ 277
oracle ................ 272
webmaster ............. 271
ftp ................... 267
web ................... 252
sales ................. 239
adm ................... 231
postgres .............. 230
testing ............... 219
ftpuser ............... 218
administrator ......... 216
alex .................. 193
student ............... 168
paul .................. 165
          

Наслаждайтесь :)