Botnet-Angriffe mit rsyslog und iptables recent module abwehren

Abstract

fail2ban gilt gemeinhin als das Werkzeug der Wahl, wenn Botnet-Dronen davon abgehalten werden sollen, alle eingehenden SMTP-Verbindungen eines SMTP-Servers (DoS-Attacke) zu sättigen. Ich musste erleben, dass fail2ban zu langsam ist, und fand eine einfache, elegante und vor allem sehr performante Lösung.

fail2ban gilt gemeinhin als das Werkzeug der Wahl, wenn Botnet-Dronen davon abgehalten werden sollen, alle eingehenden SMTP-Verbindungen eines SMTP-Servers (DoS-Attacke) zu sättigen. Das Tool verfolgt Fehlermeldungen im LOG des Mailservers und blockt ab einer bestimmten Anzahl von Fehlern die IP des Botnet-Clients.

Auf normal belasteten Servern funktioniert das gut, es scheitert aber gründlich, wenn fail2ban mit größeren Mengen von Botnet-Clients zu tun hat. Dann ist das beliebte Programm schlichtweg zu langsam. Genau das ist mir auf einem Backup-MX passiert, das zusätzlich einen SMTP Boundary Filter für eine bei Bots sehr beliebte Domain darstellt.

Der Server ist nicht besonders groß dimensioniert, ein Quad Core Intel Xeon (2.66GHz 4 GB RAM), und die betroffene Domain hat nur 15 Empfänger – gerade passend. Als Betriebssystem kommt openSUSE 11.2 (i586) zum Einsatz. Außer Postfix und Mailgraph betreibe ich dort keine weiteren nennenswerten Dienste.

Um die Botnet-Last vom Postfix-SMTP-Server (smtpd) fernzuhalten, hatte ich folgende Methoden bereits ausgeschöpft:

  • RBLs
  • Postscreen
  • Greylisting
  • Anvil Connection Limiting

Caution!

Hier ging es nicht wirklich darum zu verhindern, dass Spam eingeliefert wird. Der Server funktionierte mit "normalen" Postfix-Settings und ClamAV-Milter sehr gut.

Aber die Anzahl der Botnet-Connects war so hoch, dass dies einem Angriff gleichkam. Logs waren – weil riesengroß – zur Analyse kaum mehr brauchbar und der DNS-Traffic wegen ständiger RBL-Abfragen erheblich. Es kam sporadisch zur Ablehnung von SMTP-Verbindungen, weil alle SMTP-Server-Prozesse von Botnet-Clients in Anspruch genommen wurden. Ich musste die Anzahl verbundener Botnet-Clients reduzieren.

Das Ablehnen von Verbindungen, weil der Client auf einer RBL-Liste vermerkt ist, hatte ich bereits ausgeschöpft. Trotzdem hatte ich zu viele Verbindungen mit unerwünschten Clients. Mein Plan war, diese per Firewall auszusperren. Dazu wollte ich sie anhand fehlerhaften Verhaltens im Log erkennen und anschließend Verbindungen von deren IP blockieren lassen.

fail2ban

fail2ban war mein erster Ansatz. Genau dafür ist es bekannt. Es zeigte sich aber schnell, dass die Anzahl der Botnet-Verbindungsversuche die Fähigkeiten von fail2ban überfordert, Logs zu parsen – fail2ban kam den Ereignissen nicht hinterher. Dadurch war der Zeitversatz zur Erstellung einer DROP/REJECT-Regel in iptables zu groß, und der Botnet-Client konnte bis zum DROP zu lange versuchen, Spam abzusetzen.

Um dieses Problem zu lösen, wollte ich fail2ban mehr Zeit zum Log-Parsen verschaffen. Dazu wollte ich die Anzahl von STMP-Verbindungen pro IP und Zeiteinheit limitieren.

iptables recent Modul

Um es kurz zu machen: Der Effekt reichte nicht aus, denn zu viele Bots bauten zu schnell Verbindungen auf.

Note

Für einen Moment erwog ich, ganze Länder anhand geographischer Zuordnung der IP-Adresse auszuperren, aber ich verwarf diese Idee schnell wieder. Sie hätte zu viele False-Positives durch unberechtigtes Blocking generiert.

Die nächste Idee brachte mich auf den richtigen Weg: Ich wollte den Zeitraum zwischen Fehler und Beginn des Firewalling verkürzen, indem ich fail2ban gegen etwas anderes, schnelleres ersetzte und mir die Trigger für den IP-Block direkt aus dem Mail-Log holen würde. Das Kommando für den Block hatte ich schon vor Augen:

% echo +ip.add.res.se > /proc/net/xt_recent/SMTP

Nur was sollte ich als Trigger zum blockieren verwenden ?

Postfix Postscreen

Postscreen ist ein Connection Filter Daemon des Postfix-SMTP-Servers. Ich nutze Postscreen mit zen.spamhaus.org, und deshalb loggt Postfix brav, welche IPs Postscreen abweist:

postscreen_dnsbl_sites = zen.spamhaus.org, list.dnswl.org*-5
postscreen_dnsbl_threshold = 1
postscreen_dnsbl_action = enforce
postscreen_access_list =
    permit_mynetworks,
    cidr:/etc/postfix/postscreen_access.cidr
postscreen_blacklist_action = drop
postscreen_greet_action = enforce
postscreen_hangup_action = drop
smtp_tls_block_early_mail_reply = yes
postscreen_bare_newline_action = drop
postscreen_bare_newline_enable = yes
postscreen_non_smtp_command_enable = yes
postscreen_pipelining_enable = yes
reject_rbl_client zen.spamhaus.org

Eine typische Postscreen-log-Zeile sieht in etwa so aus:

Nov 16 16:43:31 mxback postfix/postscreen[30717]: \
      NOQUEUE: reject: RCPT from [x.x.x.x]:65156: 550 5.7.1 Service unavailable; \
      client [x.x.x.x] blocked using zen.spamhaus.org; from=<...>, to=<...>, \
      proto=ESMTP, helo=<x.lan>

Mein Plan war deshalb, die geblockten IP-Adressen über eine vorgefilterte Syslog-Pipe zu erfassen und diese anschließend sofort mit iptables zu blocken. Der folgende Filter in /etc/rsyslog.conf schreibt die IP-Adressen nach /tmp/testpipe:

Note

Die Pipe muss evtl. mit z.B. mkfifo zunächst erzeugt werden.

mail.*        -/var/log/mail;RSYSLOG_TraditionalFileFormat
mail.info     -/var/log/mail.info;RSYSLOG_TraditionalFileFormat
    $template MyTemplate,"%msg:R,ERE,1,BLANK:.*\[([0-9.]+)].*--end%\n"
    if $msg contains 'blocked using zen.spamhaus.org' then | /tmp/testpipe;MyTemplate
mail.warning  -/var/log/mail.warn;RSYSLOG_TraditionalFileFormat
mail.err      /var/log/mail.err;RSYSLOG_TraditionalFileFormat

Mit dem nachfolgenden bash-Skript zen-spam-blocker.sh konnte ich dann die IP-Adressen aus der Syslog-Pipe in die recent-Datei schreiben:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#!/bin/bash

chmod 600 /sys/module/xt_recent/parameters/ip_list_tot
echo 10000000 > /sys/module/xt_recent/parameters/ip_list_tot
chmod 400 /sys/module/xt_recent/parameters/ip_list_tot

#RESET Firewall Suse special
SuSEfirewall2 stop
SuSEfirewall2 start

#WAIT  POSTFIX
/etc/init.d/postfix stop
sleep 10
/etc/init.d/postfix start

#create rule block for 24 hours

NAME="SPAM"

iptables -I INPUT -p tcp --dport 25 -m state --state NEW \
    -m recent --name $NAME --rcheck --seconds 86400 -j DROP

while true
do
if read line </tmp/testpipe; then
        echo +$line > /proc/net/xt_recent/$NAME
fi
done

Das Ergebnis überzeugt! Hier die Anzahl der Rejects pro Tag vor Einführung:

% grep reject mail-20110911 | wc -l
1016323

Das entspricht ca. 4 Rejects pro Sekunde.

Heute sieht das schon viel entspannter aus:

% zgrep reject mail-20121116.bz2 | wc -l
8977

Das wären dann nur noch 0,1 Rejects pro Sekunde.

Das Skript läuft seit einem Jahr mit gelegentlichen Restarts, um die Tabelle zu leeren, und es gab bisher keine False-Positives.

Caution!

Die beschriebene Lösung ist sozusagen maßgeschneidert auf diesen Anwendungsfall, sie sollte nicht einfach auf andere Systeme übertragen werden. Die Lösung soll Administratoren mit ähnlichen Problemen nur als Ideengeber dienen.

Robert Schetterer, 28. December 2012