Андрей Бирюков
Пишем систему динамической защиты ресурсов сети
Аудит журналов безопасности операционной системы – неотъемлемая часть работы системного администратора. Если обнаружены попытки проникновения, на них необходимо немедленно отреагировать. Автоматизируем данный процесс с помощью сценария Perl.
Что собираемся защищать
Серверные журналы, проще говоря протоколы, содержат в себе информацию о различных событиях, происходящих в системе. Например, информацию об ошибках приложений или о попытках несанкционированного проникновения в сеть. Автоматизация «реакции» на эти сообщения является важным элементом в обеспечении бесперебойного функционирования всей сети. Конечно, каждый администратор, придя на работу, первым делом должен проверить протоколы своих серверов, не произошло ли что-нибудь в течение ночи или за выходные, и в случае необходимости принять соответствующие меры.
Однако иногда такая реакция может оказаться запоздалой, особенно это касается попыток осуществления несанкционированного входа в систему. Если какие-либо ресурсы вашей сети (например, электронная почта, веб-сервер или доступ по протоколу SSH) опубликованы в Интернете, то вам наверняка приходилось наблюдать записи в протоколах, свидетельствующие о попытке осуществления несанкционированного доступа к ресурсам сети. Конечно, это попытки взлома с помощью «грубой силы» (brute force, подбор пароля по словарю), такой способ, как правило, не приносит результата. Хотя иногда начинающие хакеры с упорством, достойным лучшего применения, «натравливают» словари на учетную запись root.
Но тут следует заметить, что зачастую наш потенциальный противник охотится вовсе не за паролем к учетной записи пользователя, а, к примеру, ему необходимо определить существует ли пользователь с таким именем на сервере или нет. Такая информация будет весьма полезна для спамеров. Ведь на очень многих почтовых серверах имя пользователя совпадает с именем почтового ящика, точнее, с той его частью, которая идет до знака @ (например, user@mydomain.ru), а база корпоративных электронных адресов стоит дороже чем база адресов с бесплатных почтовых серверов. Для того чтобы избежать подобного рода сканирования, можно просто закрыть соответствующие порты от использования снаружи. Но в большинстве современных компаний сотрудники хотят иметь доступ к своей почте из дома или находясь в командировке, поэтому вариант с запретом нам не подходит.
Из всего этого делаем вывод, что нам необходимо средство, которое могло бы обнаруживать и адекватно реагировать на попытки подбора логинов/паролей к ресурсам нашей сети.
Существует масса решений, как программных, так и аппаратных, получивших название Intrusion Detection System, IDS, системы обнаружения вторжения, которые позволяют выполнить данную задачу. Но зачастую они либо слишком громоздки в управлении, либо слишком дорого стоят.
Попробуем самостоятельно разработать простейшую IDS. В качестве инструментов будем использовать сценарии Perl под ОС Linux.
Как собираемся защищать
В качестве основы для реализации я использовал сценарий, находящийся на cpan.org [1]. Идея довольно проста: через определенные промежутки времени сканируется файл /var/log/messages. В случае обнаружения сообщений, информирующих о попытках несанкционированного доступа с определенного IP-адреса, по протоколу ssh в iptables добавляется строка, блокирующая получение пакетов с данного адреса. Список заблокированных IP-адресов автор предлагает передавать в сообщениях syslog.
Обратите внимание на то, что перед запуском сценария необходимо создать iptables chain Block и поместить текущую конфигурацию iptables в файл /etc/sysconfig/iptables. Тут следует сразу отметить, что пример сценария (см. листинг 1) ориентирован на использование в дистрибутивах Fedora/Red Hat. Для применения с другими дистрибутивами Linux необходимо будет произвести небольшие изменения.
Листинг 1. Сценарий, блокирующий атаки на ssh
#!/usr/bin/perl -w
use Sys::Syslog; # модуль для работы с syslog
$max=10; # максимально разрешенное число попыток
# файл журнала, с которым мы и работаем
$watchfile= '/var/log/messages';
# путь к iptables (для разных дистрибутивов Linux он может различаться)
$iptables= '/sbin/iptables';
# тоже iptables, но после сохранения изменений
$iptables_save= '/sbin/iptables-save';
# исходные значения iptables
$iptables_restore= '/sbin/iptables-restore';
# файл конфигураций iptables
$cfgfile= '/etc/sysconfig/iptables';
open(LFILE, "<$watchfile"); # открываем лог на чтение
%tries=(); # количество попыток для IP
%blocked=(); # уже заблокированных IP
# восстанавливаем конфигурацию, это нужно для того, чтобы «устаревшие» IP-адреса не оставались запрещенными
`$iptables_restore < $cfgfile`;
# получаем уже заблокированные IP-адреса из iptables
open(IPTPIPE, "$iptables -L -v -n|");
$blockChain=0;
while (<IPTPIPE>){
$blockChain=1 if (/^Chain block \(\d+ references\)$/);
next unless $blockChain;
last if (/^$/ );
$blocked{$1}=1 if (/(\d+\.\d+\.\d+\.\d+)/);
}
close IPTPIPE;
# преобразуем в строку
$blk_ips=join(", ",keys(%blocked));
# отправляем сообщение syslog
syslog('warning',"sshwatch.pl started. currently blocked ip's are: $blk_ips");
# просматриваем /var/log/messages
while (1) {
for ($curpos = tell(LFILE); $_ = <LFILE>; $curpos = tell(LFILE)) {
# искомая строка
if (/sshd\[\d+\]: Failed password for .+ from \D+(\d+\.\d+\.\d+\.\d+)/) {
$ip=$1;
next if defined($blocked{$ip});
$tries{$ip}+=1; #увеличиваем счетчик
if ($tries{$ip} eq $max){
# если превышено максимальное значение,
# пакеты с данного адреса должны быть заблокированы
`$iptables -I block -s $ip -j DROP ; $iptables_save > $cfgfile`;
$blocked{$ip}=1;
# снова сообщение syslog
syslog('warning', "IP $ip has been blocked !");
}
}
}
sleep 1;
seek(LFILE, $curpos, 0);
}
Как видите, сценарий довольно прост, мы всего лишь отслеживаем нужные строки в файле и реагируем на них заданным образом, блокируя доступ к сети для IP-адреса атакующего.
Расширяем функциональность
Попробуем расширить функциональность сценария, написанного нашим коллегой.
Во-первых, допустим, у нас открыты наружу не только ssh, но и, к примеру, POP3.
И во-вторых, рассмотрим различные варианты построения сети: как с использованием демилитаризованной зоны (DMZ), так и без нее. Суть DMZ заключается в том, что она не входит непосредственно ни во внутреннюю, ни во внешнюю сеть и доступ к ней может осуществляться только по заранее заданным правилам межсетевого экрана. В DMZ нет пользователей – там располагаются только серверы. Демилитаризованная зона, как правило, служит для предотвращения доступа из внешней сети к хостам внутренней сети за счет выноса из локальной сети в особую зону всех сервисов, требующих доступа извне. Фактически получается, что эта зона будет являться отдельной подсетью с публичными адресами, защищенной (или отделенной) от публичных и корпоративных сетей межсетевыми экранами.
При попытке получения доступа к почтовому ящику по протоколу POP3, в случае неверного указания логина/пароля получаем сообщение:
Apr 22 21:25:24 MexBSD qpopper[782]: [AUTH] Failed attempted
login to admin from host (TestBSD) 172.29.1.19
|
Здесь в качестве POP3-демона использовался qpopper, но и для других серверов вид сообщения будет аналогичен.
По поводу топологии сети будем предполагать, что у нас имеется единственный сервер и необходимо блокировать пакеты, непосредственно приходящие на него.
Также сценарий, предназначенный для защиты от подбора протокола POP3, применялся на FreeBSD, поэтому вместо iptables воспользуемся утилитой ipfw, с помощью которой создаются правила фильтрации. Для запрета доступа у ipfw существуют директивы deny и reject.
Директива deny сводит к минимуму риск определенных атак типа «отказ в обслуживании» и усложняет взломщику задачу сканирования системы. В то же время директива reject позволяет скрыть факт наличия брандмауэра, так как она заставляет систему вернуть отправителю сообщение о том, что хост или порт недоступен (как если бы маршрутизатор не смог найти хост или на нем отсутствует приложение, контролирующее заданный порт).
В нашем случае предпочтительнее будет указать директиву deny, потому что она заставляет систему игнорировать пакет, чтобы отправитель думал, будто пакет потерялся или хост-адресат выключен. Я бы еще добавил, что такая реакция сильно замедляет автоматическое сканирование.
Например, команда, добавляющая правило, которое запрещает доступ к нашему серверу 172.29.1.1 для атакующего хоста 172.29.1.19, будет выглядеть следующим образом:
ipfw add deny ip from 172.29.1.19 to 172.29.1.1
В отсутствии явно указанного индекса (положения правила в списке), который присваивается каждому правилу, он будет присвоен автоматически, причем значение этого индекса будет на 100 больше чем номер самого последнего правила, за исключением правила по умолчанию (имеет номер 65535).
Не забудьте проверить список настроек ipfw с помощью команды «ipfw list». Это необходимо для того, чтобы ваше запрещающее правило не лишилось смысла из-за стоящего перед ним уже есть разрешающего. В случае если у вас уже используются разрешающие правила для данного трафика, проследите за тем, чтобы запрет оказался выше по списку.
Можно, конечно, ограничиться только запретом tcp, но лучше, чтобы все выглядело, как будто сервер не просто недоступен, а выключен. Такого результата легче добиться, запретив любой IP-трафик. Вообще утилита ipfw обладает довольно большими возможностями, подробно ознакомиться с которыми можно в справочнике [2].
Итак, мы определились с тем, какие записи в журнале событий нам необходимо просматривать и как на них реагировать. Для разнообразия будем список заблокированных IP-адресов с указанием времени блокирования отправлять на почту системному администратору. Хотелось бы сразу предупредить, что этих писем будет довольно много и поэтому необходимо заранее создать в почтовом клиенте соответствующую папку и правило для перенаправления писем.
Листинг 2. Сценарий, блокирующий атаки на POP3
#!/usr/bin/perl -w
use Mail::Sendmail;
# максимально разрешенное число попыток
$max=10;
# файл журнала, с которым мы и работаем
$watchfile= '/var/log/messages';
$ipfw= '/sbin/ipfw';
# то же ipfw но после сохранения изменений
$ipfw_save= '/sbin/ipfw_save';
# исходные значения ipfw
$ipfw_restore= '/sbin/ipfw-restore';
# файл конфигураций ipfw
$cfgfile= '/etc/sysconfig/ipfw'
# открываем лог на чтение
open(LFILE, "<$watchfile");
# количество попыток для IP
%tries=();
# уже заблокированных IP
%blocked=();
# восстанавливаем конфигурацию, это нужно для того, чтобы «устаревшие» IP-адреса не оставались запрещенными
`$ipfw_restore < $cfgfile`;
`$ipfw list > out`;
# получаем уже заблокированные IP-адреса из iptables
open(IPTPIPE, "out");
$blockChain=0;
while (<IPTPIPE>){
$blockChain=1 if (/^Chain block \(\d+ references\)$/);
next unless $blockChain;
last if (/^$/ );
$blocked{$1}=1 if (/(\d+\.\d+\.\d+\.\d+)/);
}
close IPTPIPE;
# просматриваем /var/log/messages
while (1) {
for ($curpos = tell(LFILE); $_ = <LFILE>; $curpos = tell(LFILE)) {
# искомая строка
if (/qpopper\[\d+\]: Failed attempted login to .+ from \D+(\d+\.\d+\.\d+\.\d+)/) {
$ip=$1;
next if defined($blocked{$ip});
$tries{$ip}+=1; #увеличиваем счетчик
if ($tries{$ip} eq $max){
# если превышено максимальное значение,
# пакеты с данного адреса должны быть заблокированы
`$ipfw deny $ip; $ipfw_save > $cfgfile`;
$blocked{$ip}=1;
# отправляем письмо
%mail = ( To => 'admin@test.local',
From => 'firewall@test.local',
Message => "IP $ip has been blocked !",
SMTP => 'smtp.mail.ru'
);
sendmail(%mail) or die $Mail::Sendmail::error;
}
}
}
sleep 1;
seek(LFILE, $curpos, 0);
}
Пример в целом аналогичен предыдущему, но содержит ряд отличий, свойственных утилите ipfw и формату тех записей, которые ищет данный сценарий.
Защита по периметру
Мы рассмотрели упрощенный случай топологии сети, когда у нас имеется сервер, не защищенный какими-либо дополнительными аппаратными брандмауэрами, и единственной защитой для него являются те средства, которые на нем установлены. Но очень часто сеть имеет более сложную структуру. Ресурсы, доступ к которым необходим как из внутренней сети, так и из Интернета, помещают в демилитаризованную зону. Примерная топология сети на рис. 1. Рассмотрим случай, когда наш почтовый сервер находится в этой DMZ. Как уже упоминалось, к нему есть доступ из глобальной сети. При такой топологии лучше всего отсекать трафик атакующего еще на брандмауэре.
Рисунок 1. Размещение серверов в DMZ
Будем предполагать, что в качестве межсетевого экрана у нас используется оборудование Cisco, тогда для осуществления блокирования нам необходимо воспользоваться командами Cisco IOS.
Приведем необходимый набор команд для доступа к консоли и изменения списка доступа. Приводить здесь команды для настройки NAT и связи с ACL я не буду, так как это не является темой статьи, и предполагаю, что у вас уже всё настроено и нормально функционирует. Приведу лишь те команды, которые необходимо будет передавать с помощью Perl-сценария. В моем примере использовался скромный PIX 501, однако для других моделей существенных различий не будет.
login as: user
user@1.1.1.1"s password:
Secure Access
Type help or "?" for a list of available commands.
|
ipfw add deny ip from 172.29.1.19 to 172.29.1.1
pixfirewall# configure terminal
Так как если в список доступа просто добавить запись, то она будет поставлена в конец, а в нашем случае это запрещающая запись, то соответственно ее добавление будет бессмысленно и не приведет к желаемому результату. Поэтому нам необходимо сначала убрать старый список доступа, а потом уже прописать новый. Естественно, что кроме наших записей, запрещающих трафик с определенных узлов там должны быть еще другие записи, обеспечивающие нормальный доступ к ресурсам сети.
В нашем случае необходимо лишь заблокировать трафик с конкретного хоста (2.2.2.2). Запись, которая осуществляет данные действия, мы разместим в начале нашего нового ACL. Реализовать все эти действия можно следующим образом:
pixfirewall(config)# no access-list 110
pixfirewall(config)# access-list 110 deny ip host 2.2.2.2 host 1.1.1.254
pixfirewall(config)# access-list 110 permit ip 172.16.0.0 0.0.255.255 1.1.1.0 0.0.0.255
…
pixfirewall(config)# end
Не забудьте, что по умолчанию действует неявный запрет, то есть все то, что не разрешено, будет запрещено.
Изменения в конфигурацию внесены, остается только сохранить их и отключиться.
pixfirewall# write mem
pixfirewall# logout
Такие действия нужны для того, чтобы закрыть атакующему доступ к ресурсам нашей сети.
Теперь можно приступать к написанию сценария, реализующего эти действия на практике. Так как исходный текст сценария подвергается существенным изменениям, приведем его полностью.
use Net::Telnet ();
use Mail::Sendmail;
#!/usr/bin/perl -w
use Mail::Sendmail;
# максимально разрешенное число попыток
$max=10;
# файл журнала, с которым мы и работаем
$watchfile= '/var/log/messages';
# открываем лог на чтение
open(LFILE, "<$watchfile");
# количество попыток для IP
%tries=();
# уже заблокированных IP
%blocked=();
# получаем доступ к консоли
$t = new Net::Telnet ;
$hostname="1.1.1.254";
$t->open($hostname);
$t->waitfor('/login:.*$/')
or die "bad login: ", $t->lastline;
$t->print("user");
$t->waitfor('/Password:.*$/')
or die "bad password: ", $t->lastline;
$t->print("password");
$t->waitfor('/pixfirewall>:.*$/')
or die "No user mode: ", $t->lastline;
$t->print("enable");
$t->waitfor('/login:.*$/')
or die "bad login: ", $t->lastline;
$t->print("user");
$t->waitfor('/Password:.*$/')
or die "bad password: ", $t->lastline;
$t->print("password");
$t->waitfor('/pixfirewall#:.*$/')
or die "No router privilege mode: ", $t->lastline;
$t->print("configure terminal");
$t->waitfor('/# pixfirewall(config):.*$/')
or die "No router configure mode: ", $t->lastline;
$t->print("no access list 110");
# просматриваем /var/log/messages
while (1) {
for ($curpos = tell(LFILE); $_ = <LFILE>; $curpos = tell(LFILE)) {
# искомая строка
if (/sshd\[\d+\]: Failed password for .+ from \D+(\d+\.\d+\.\d+\.\d+)/) {
$ip=$1;
next if defined($blocked{$ip});
$tries{$ip}+=1; # увеличиваем счетчик
if ($tries{$ip} eq $max){
# если превышено максимальное значение,
# пакеты с данного адреса должны быть заблокированы
$blocked{$ip}=1;
# добавляем в список доступа запись о запрете
# доступа с IP
$t->waitfor('/# pixfirewall(config):.*$/')
or die "No router configure mode: ", $t->lastline;
$t->print("access list 110 deny ip host $ip host 1.1.1.254");
# отправляем письмо
%mail = ( To => 'admin@test.local',
From => 'firewall@test.local',
Message => "IP $ip has been blocked !",
SMTP => 'smtp.mail.ru'
);
sendmail(%mail) or die $Mail::Sendmail::error;
}
}
}
sleep 1;
seek(LFILE, $curpos, 0);
}
$t->waitfor('/# pixfirewall(config):.*$/')
or die "No router configure mode: ", $t->lastline;
$t->print("access list 110 permit ….");
…
# здесь добавляем другие записи в список доступа
$t->waitfor('/# pixfirewall(config):.*$/')
or die "No router configure mode: ", $t->lastline;
$t->print("end");
$t->waitfor('/# pixfirewall#:.*$/')
or die "No router configure mode: ", $t->lastline;
$t->print("write mem");
$t->waitfor('/# pixfirewall#:.*$/')
or die "No router configure mode: ", $t->lastline;
$t->print("logout");
$result=$t->getline;
Перед тем как начать поиск попыток проникновения в сеть, подключаемся к консоли брандмауэра с помощью протокола Telnet. Как и в предыдущих сценариях, мы ищем вхождения искомых строк, и в случае, если количество попыток проникновения превосходит заданное значение, добавляем запись в список доступа. Затем, после того как все «новые» записи добавлены, мы добавляем основные, разрешающие записи, сохраняем измененную конфигурацию и отключаемся от консоли. Конечно, кому-то такой подход может показаться не слишком удобным, так как необходимо вносить изменения в конфигурацию работающего устройства, однако такой вариант позволяет отсекать вредоносный трафик еще до того, как он проникает в вашу локальную сеть. Естественно необходимо позаботиться о безопасности доступа по Telnet, разрешив подключения только определенным узлам из внутренней сети [3].
Автоматизируем запуск
После написания сценария нам необходимо автоматизировать его работу. Сделаем это с помощью cron. По поводу работы с данной утилитой и ее синтаксиса написано уже достаточно, поэтому приведу лишь несколько примеров. Прежде всего рекомендовую не запускать сценарий слишком часто, по крайней мере не каждые пять минут. Это связано с тем, что на активно работающем сервере, как правило, файл messages имеет весьма внушительный размер, и поиск текстовых вхождений может существенно нагрузить систему. К тому же слишком частые обращения к консоли сетевого оборудования тоже не очень желательны. Я бы рекомендовал запуск сценария каждый час. Задать выполнение сценария по такому расписанию можно следующим образом:
@hourly /tmp/pop3watch.pl
Итак, мы рассмотрели различные сценарии, позволяющие обнаружить и адекватно реагировать на попытки несанкционированного доступа к ресурсам сети. Конечно, приведенные способы поиска можно расширить, например, искать информацию не только о попытках проникновения, но и о неисправностях и сбоях, возникающих в системе, например, в случае аварийного завершения работы какого-либо демона или сценария. Поэтому данный сценарий может оказаться полезен и при решении других задач автоматизации системного администрирования.
Использованные источники:
- http://cpan.org/authors/id/D/DR/DRAGOS/sshwatch-0.01.pl – исходный сценарий.
- Родерик Смит. Полный справочник по FreeBSD.
- Основы организации сетей Cisco. Справочное руководство.