Администраторы больших сетей регулярно получают жалобы на исходящий из них спам. Получив жалобу,
можно найти виновного и устранить проблему. Но нехорошо узнавать о таких делах только от чужих людей.
Спамеров вполне можно вычислить самому, и принять меры раньше, чем кто-то пострадает.Чисто интуитивно достаточно запустить tcpdump на исходящем канале, и посмотреть, какие IP-адреса больше в
сего посылают SYN-пакетов на 25 порт.
tcpdump -ni bridge0 'tcp[tcpflags] & tcp-syn != 0 and dst port 25'
В идеале это был бы ваш SMTP-сервер. В действительности вы увидите в основном IP-адреса простых пользователей,
"одержимых бесами". Дело в том, что обычный спамбот создает SMTP-соединений во много раз больше
вашего почтового сервера. Причем с разными IP-адресами.
В наши дни все нормальные почтовые сервера имеют PTR-запись DNS. Простые клиенты, напротив,
такой записи обычно не имеют. Если почтовый сервер может устанавливать SMTP-соединения
со множеством IP-адресов, то для простого клиента эта величина обычно не более 1-2 (например,
пользование удаленными SMTP-серверами с лаптопа).
Исходя из этого, можно автоматически определять спамботов в нашей сети, и блокировать их (например,
средствами IPFW). В качестве критерия оценки мы установили максимальное количество IP-адресов,
с которыми каждый хост может общаться по SMTP в течение 5 минут. Для хостов с реверсной записью DNS это 200,
для хостов без реверсной записи - 20. Если хост превышает этот лимит, он попадает в черный список на сутки.
Нами написан Perl-скрипт, запускающий tcpdump на 5 минут, и анализирующий результаты.
Хосты-нарушители заносятся в таблицу PostgreSQL для удобства персонала, и затем помещаются
в таблицу IPFW. Правила файрвола блокируют все соединения на 25 порт с хостов, находящихся в данной таблице.
Текст скрипта:
#!/usr/local/bin/perl
# dimss@stalin.lv 2009-08-20
my $colltime = 300; # How long to collect data
my $rev_limit = 200; # Remote IP limit for hosts with PTR record
my $norev_limit = 20; # Remote IP limit for hosts without PTR record
my $table_no = 1; # Number of IPFW table
use strict;
use warnings;
use POSIX ':signal_h';
use Socket;
use DBI;
#
# Collect data
# Run tcpdump for some time
#
my $conns = {};
my $mask = POSIX::SigSet->new( SIGALRM );
my $action = POSIX::SigAction->new(
sub {
system("killall tcpdump");
},
$mask
);
my $oldaction = POSIX::SigAction->new();
sigaction(14, $action, $oldaction);
alarm($colltime); # Run tcpdump for this amount of time
open(T, "/usr/sbin/tcpdump -ni bridge0 'tcp[tcpflags] & tcp-syn != 0 " .
"and dst port 25' " .
"2>/dev/null |") or die;
while(<T>){
/\ ((\d+\.){3}\d+).+?((\d+\.){3}\d+)/;
my $locip = $1;
my $remip = $3;
#print "$locip $remip\n";
$conns->{$locip} ||= {};
$conns->{$locip}->{$remip} ||= 1;
}
alarm(0);
sleep(1);
#
# Analyze connections
#
$mask = POSIX::SigSet->new( SIGALRM );
$action = POSIX::SigAction->new(
sub {
die("Reverse lookup took too long");
},
$mask
);
$oldaction = POSIX::SigAction->new();
sigaction(14, $action, $oldaction);
alarm(60); # Reverse DNS lookups must complete within this period
my %concount;
my %bots;
for my $locip (keys(%$conns)){
#print("Loc IP: $locip\n");
my $cnt = 0;
for my $remip (keys(%{$conns->{$locip}})){
#print(" Rem IP: $remip\n");
$cnt++;
}
$concount{$locip} = $cnt;
}
for my $locip (sort {$concount{$b} <=> $concount{$a}} keys(%concount)){
if($concount{$locip} > $norev_limit){
my $ip = inet_aton($locip);
my $name = gethostbyaddr($ip, AF_INET);
if($concount{$locip} > ($name ? $rev_limit : $norev_limit)){
#print("$locip: $concount{$locip} (" . ($name || '') . ")\n");
$bots{$locip} = $name;
}
}
}
alarm(0);
#
# Update database
#
$mask = POSIX::SigSet->new( SIGALRM );
$action = POSIX::SigAction->new(
sub {
die("Database timeout");
},
$mask
);
$oldaction = POSIX::SigAction->new();
sigaction(14, $action, $oldaction);
alarm(15);
my $dbh = DBI->connect("dbi:Pg:host=db.host.tld;dbname=spambot",
"spambot", "passwd");
if(!$dbh) {
die("Cannot connect to DB");
}
my $query;
my $sth;
my $rv;
for my $botip (keys(%bots)){
#print "$botip\n";
my $qname = $dbh->quote($bots{$botip});
$query = "
update spambot_hosts set
active_till = now() + '1 days',
hostname = $qname
where ip = '$botip'
";
$sth = $dbh->prepare($query);
$rv = $sth->execute();
if($rv != 1){
$query = "
insert into spambot_hosts
(ip, hostname, active_till)
values
('$botip', $qname, now() + '1 days')
";
$sth = $dbh->prepare($query);
$sth->execute();
}
}
$query = "
delete from spambot_hosts
where active_till < now()
";
$sth = $dbh->prepare($query);
$sth->execute();
#
# Get full list of banned hosts from DB
#
$query = "
select ip from spambot_hosts
";
$sth = $dbh->prepare($query);
$sth->execute();
my %dlist;
while(my $row = $sth->fetchrow_hashref){
$dlist{$row->{ip}} = 1;
}
undef $dbh;
alarm(0);
#
# Read list of banned hosts from kernel (ipfw) table
#
my %klist;
if(open(KTABLE, "/sbin/ipfw table $table_no list|")){
while(<KTABLE>){
chomp();
s/\/32//;
my ($prefix, $queue) = split();
$klist{$prefix} = 1;
}
close(KTABLE);
}
#
# Update kernel table
#
for my $host (keys(%dlist)){
unless($klist{$host}){
#print "Add $host\n";
system("/sbin/ipfw table $table_no add $host");
}
}
for my $host (keys(%klist)){
unless($dlist{$host}){
#print "Remove $host\n";
system("/sbin/ipfw table $table_no delete $host");
}
}
exit;
Из /etc/ipfw.rules:
#....
ipfw -q add deny tcp from "table(1)" to any 25
#....
У нас скрипт работает на шейпере, обслуживающем только заграничный канал. Местный трафик им не анализируется
и не блокируется. При этом большая часть "хорошей" почты ходит именно по местным каналам, но спамеры
спамят в основном по заграничному каналу. Так что лимиты вам наверняка придется подстроить под себя.
Возможно, вам придется также реализовать "белый список", если у вас есть очень активные SMTP-сервера.
На момент написания статьи в "расстрельном списке" несколько десятков хостов. Жалоб за истекшие сутки
не поступало, хотя раньше их было множество.
URL:
Обсуждается: http://www.opennet.dev/tips/info/2146.shtml