Skriv din egen Postfix-policy

Tirsdag, den 15. februar 2011

I sidste indlæg skrev vi lidt om spambekæmpelse med de standardkomponenter, der er til rådighed for Postfix. Desværre er der kommet en ny metode til at omgå disse kontroller. Vi har opdaget, at der er opstået et trick, der går ud på at opfylde alle disse ting. Hvis et domæne har alle disse ting: Korrekt HELO-navn, der ikke peger forkert, korrekt SPF-record, der viser at vi har den korrekte afsenderserver. Forward-DNS, der endda peger korrekt : Skulle sådan en domæneejer finde på at udsende spam, ville det jo øjeblikkeligt føre til klager og ultimativt ville sådan et domæne blive lukket. Korrekt. Men. Hvis en spammer nu køber et .info-domæne for 1$, udsender en masse spam, og derefter blot glemmer domænet når klagerne kommer.. tja, så har spammeren jo fået nogle dage eller uger til at sende en masse spam for den relativt lille pris af 1$. Hmm. Det må vi have sat en stopper for, men vi kan ikke gøre det med de standardværktøjer, der findes i Postfix. Vi kan begynde med at skrive vores egen Policy.

Hvordan egen policy
For at få skovlen under de mere udspekulerede spammere skal vi altså skrive vores egen policy, men hvordan? Postfix har et relativt nemt interface til at skrive policy-service i lige det sprog, der passer os. Formatet er meget enkelt. Kort fortalt skal vi altså blot skrive et program, der kan læse en masse linier fra stdin i formatet key=value. På baggrund af disse informationer kan man så skrive “DUNNO”, “REJECT”, “DEFER_IF_PERMIT” eller andet relevant ud. Dette reagerer Postfix så på. Så skal vi blot til at kode.

Kode kode kode
I de officielle eksempler er Postfix-policies skrevet i Perl. Man kunne fint skrive dem i andre ting, som eksempelvis PHP, C eller Shell, men for eksemplets skyld fortsætter vi i Perl.

#!/usr/bin/perl

use Fcntl;
use POSIX;
use Date::Manip;
use Sys::Syslog qw(:DEFAULT setlogsock);

$syslog_socktype = 'unix'; # inet, unix, stream, console
$syslog_facility="mail";
$syslog_options="pid";
$syslog_priority="info";
$domain_append_file="/var/db/postfix-policyd-dotinfo/spammers";

#
# Demo SMTPD access policy routine. The result is an action just like
# it would be specified on the right-hand side of a Postfix access
# table.  Request attributes are available via the %attr hash.
#
sub smtpd_access_policy {
    @domain_parts = reverse(split(/\./, $attr{"helo_name"})); 

    if ($domain_parts[0] != 'info') {
        syslog $syslog_priority, "info: non-.info sender ".$attr{'helo_name'}." was allowed.";
        return "dunno";
    } else {
        $domain = $domain_parts[1] . "." . $domain_parts[0];

        ## Make whois-query, see if it is a recent domain
        open (WHOIS, "/usr/bin/whois $domain |") or return "DUNNO";

        while () {
            if (/([^:]+):(.*)\n/) {
                $whois_attrs{substr($1, 0, 512)} = substr($2, 0, 512);
            } else {
                chop;
            }
        }

        if (!$whois_attrs{"Created On"}) {
            syslog $syslog_priority, "warn: Could not resolve domain age of .info sender ".$attr{'helo_name'}.".";
            return "DUNNO";
        }

        $domain_date =& ParseDate($whois_attrs{"Created On"});
        $domain_secs =& UnixDate($domain_date,"%s");
        $now_secs = time();

        %whois_attrs = ();
        if ($now_secs - $domain_secs < 5000000) {
            syslog $syslog_priority, "info: .info sender ".$attr{'helo_name'}." (sending to ".$attr{'recipient'}.") was rejected.";
            $ret = `echo "$domain\tREJECT Recipient address rejected: User unknown in local recipient table" >> $domain_append_file`;
            `/usr/local/sbin/postmap $domain_append_file`;
            return "REJECT Go away... please. We're tired of spam."
        } else {
            syslog $syslog_priority, "warn: .info sender ".$attr{'helo_name'}." was an old domain.";
            return "DUNNO";
        }
    }
}

#
# Log an error and abort.
#
sub fatal_exit {
    my($first) = shift(@_);
    syslog "err", "fatal: $first", @_;
    exit 1;
}

#
# This process runs as a daemon, so it can't log to a terminal. Use
# syslog so that people can actually see our messages.
#
setlogsock $syslog_socktype;
openlog 'postfix-policyd-dotinfo', $syslog_options, $syslog_facility;

#
# We don't need getopt() for now.
#
while ($option = shift(@ARGV)) {
    if ($option eq "-v") {
        $verbose = 1;
    } else {
        syslog $syslog_priority, "Invalid option: %s. Usage: %s [-v]",
            $option, $0;
        exit 1;
    }
}

#
# Unbuffer standard output.
#
select((select(STDOUT), $| = 1)[0]);

#
# Receive a bunch of attributes, evaluate the policy, send the result.
#
while () {
    if (/([^=]+)=(.*)\n/) {
        $attr{substr($1, 0, 512)} = substr($2, 0, 512);
    } elsif ($_ eq "\n") {
        if ($verbose) {
            for (keys %attr) {
                syslog $syslog_priority, "Attribute: %s=%s", $_, $attr{$_};
            }
        }
        fatal_exit "unrecognized request type: '%s'", $attr{request}
	    unless $attr{"request"} eq "smtpd_access_policy";

        $action = smtpd_access_policy();
        syslog $syslog_priority, "Action: %s", $action if $verbose;
        print STDOUT "action=$action\n\n";
        %attr = ();
    } else {
        chop;
        syslog $syslog_priority, "warning: ignoring garbage: %.100s", $_;
    }
}

Hvordan virker det? Alt det interessante og individuelle sker i “sub smtpd_access_policy”. Skulle man skrive en ny policy kunne man undlade at ændre andre steder. I det aktuelle tilfælde har vi et problem med at spritnye .info-domæner misbruges til at opfylde alle kravene til SPF, forward DNS mv. Vi ved også, at sådanne domæner hurtigt bliver (eller burde blive) lukkede. Dette udnytter vi. Når vi har fundet ud af, at det drejer sig et .info-domæne foretager vi et whois-opslag på domænet. Dette indeholder en “Created On”-attribut. Nu ser vi om denne “Created On” ligger under ca. 58 dage tilbage i tiden, med andre ord, om det er et “nyt” domæne. Hvis det er tilfældet, afviser vi mailen med det samme med besked om at vi er trætte af spam. For nemhedens skyld tilføjer vi domænet til en sortliste, så vi slipper for at lave kontrollen én gang til, inden vi skal tage denne kontrol en gang mere.

Integration med Postfix
Vi har skrevet en policy, og nu skal den integreres i Postfix. Dette sker to steder

Først i bunden af /etc/postfix/master.cf:


policydotinfo unix - n n - - spawn
user=nobody argv=/usr/local/sbin/postfix-policyd-dotinfo

Dernæst i /etc/postfix/main.cf:


smtpd_recipient_restrictions = permit_mynetworks,
permit_sasl_authenticated,
reject_unauth_destination,
reject_unknown_sender_domain,
reject_rbl_client zen.spamhaus.org,
check_policy_service unix:private/policydotinfo

Bid mærke i teksten “policydoinfo”.

Problemer
Nej, hverken koden eller konceptet er perfekt. Eksempelvis kunne vi komme i en situation, hvor det er et lovligt nyt .info-domæne. Domæner skifter ikke så ofte, så jeg tvivler på at dette skulle være et problem af noget særligt omfang. Vores whois-opslag kunne også fejle; i dette tilfælde er vores policy kodet til blot at returnere “DUNNO”.

Fremtidige projekter
Spammerne ligger aldrig stille. Da spamkontrollerne lige nu er tæt på 100% præcise, ved vi, at der må komme nye tiltag. Dette kunne være at stresse kontrollerne. Det er altid vigtigt at tage de billigste kontroller først i processen, så vi får sorteret så meget fra så tidligt som muligt. Lige så snart spammeren kommer forbi simple tabelopslag eller regulære udtryk, og kræver databaseaktivitet eller netværkskommunikation (Eks. RBL eller Whois), så er der mulighed for at stresse kontrollen.

Det er ikke muligt at skrive kommentarer.