Auf heißer Spur (Linux-Magazin, Februar 2009)

Mit einer Datenbank, die IP-Adressen lokalisiert und dem Google Charts Service kann man sehen, aus welchen geographischen Regionen die Linkspammer einer Webseite herkommen.

Manchmal wäre es schon sehr befriedigend, wie in dem amerikanischen Werbespot der Firma Snickers [2] das Büro eines Spammers oder Telemarketers aufzuspüren, tatsächlich dort hinzufahren und seiner Wut freien Lauf zu lassen. Leider ist dies aus legalen und logistischen Gründen oft nicht möglich. Außerdem erledigen die Drecksarbeit nicht die Gauner selbst, sondern infizierte Botnetze, aber es wäre durchaus interessant, einmal graphisch darzustellen, aus welchen geographischen Regionen die meisten Spamaktivitäten so kommen.

Fährte aufnehmen

Das Internet ist die ideale Plattform für anonyme Gaunereien, aber eine Spur hinterlassen die Verbrecher dennoch: Jeder eingehende Request führt die IP-Addresse des Senders mit sich (Abbildung 1). Diese lässt sich zwar auch spoofen, aber das ist nicht so einfach und den meisten Spammern zu umständlich.

Abbildung 1: Link-Spammer hinterlassen im Access-Log des Webservers ihre IP-Adresse.

Abbildung 2: Ein Reverse-DNS-Lookup bringt oft die einer IP-Addresse zugeordnete Domain zum Vorschein.

Listing 1: revlookup

    01 #!/usr/local/bin/perl -w
    02 use strict;
    03 use Socket;
    04 
    05 my $host = $ARGV[0] or 
    06   die "usage: $0 ipaddr";
    07 
    08 print reverse_lookup($host) || "unknown", 
    09       "\n";
    10 
    11 ###########################################
    12 sub reverse_lookup {
    13 ###########################################
    14   my ($ip) = inet_aton $_[0];
    15 
    16   return (gethostbyaddr($ip, AF_INET))[0];
    17 }

Das DNS-System, das Hostnamen IP-Adressen zuweist, bringt oft auch die umgekehrte Zuordnung zustande. Ein sogenannter DNS-Reverse-Lookup nimmt eine IP-Adresse entgegen, und falls der Service Provider des Gauners alles ordnungsgemäss aufgesetzt hat, gibt das Skript in Listing revlookup wie in Abbildung 2 gezeigt einen Hostnamen aus, aus dem sich oft der Provider ermitteln lässt. Abbildung 2 zeigt, dass die beim Spammen erwischte IP 69.162.110.146 dem ISP lstn.net gehört und eine freundliche Email an deren Webmaster mit Angabe der IP, der Uhrzeit (wichtig, da diese IPs unter Umständen dynamisch vergeben werden), bringt mit etwas Glück den Spammer zum Schweigen.

Die Funktion inet_aton() aus dem Modul Socket nimmt eine IP-Adresse in String-Darstellung (``x.x.x.x'') entgegen und gibt eine Datenstruktur für einen folgenden Aufruf der Perl-Funktion gethostbyaddr() zurück. Letztere führt den DNS-Reverse-Lookup durch (AF_INET gibt an, dass es sich um eine IPv4-Adresse handelt) und liefert im Erfolgsfall einen String mit dem Hostnamen, im Fehlerfall undef. Der Vorgang kann allerdings bis zu einigen Sekunden dauern, je nachdem, wie beschäftigt der genutzte DNS-Server gerade ist und wieviele seiner Kollegen er zur Beantwortung der Frage konsultieren muss.

Wer bist du?

Die Kommandozeilen-Utility whois funktioniert nicht nur mit Domains, sondern verkraftet ebenfalls IP-Adressen als Argumente. Abbildung 3 zeigt, dass der Provider ``Limestone Networks'' alles sehr ordentlich registriert hat und auch gleich eine Email-Adresse für Beschwerden angibt, an die sich der bespammte Webmaster wenden kann. Das Ganze geht auch programmatisch in Perl, beispielsweise mit dem CPAN-Modul Net::Whois::Raw, doch erfolgt der Lookup über die Server der Firma Network Solutions, die nach etwa 100 Lookups in kurzer Folge den Zugang sperrt. Eine ganze Access-Log-Datei damit zu durchforsten ist damit also, auch wenn man bereits erfolgte Abfragen in einem Cache speichert, nicht möglich.

Abbildung 3: Der Whois-Eintrag für die ertappte IP-Adresse zeigt die Daten des Internet-Providers des Spammers.

Die harte Tour

Viele Spammer arbeiten jedoch mit IP-Adressen, die keinen Reverse-Eintrag im DNS-System aufweisen. Doch auch dann ist eine gewisse Lokalisierung möglich, denn IP-Adressen werden in Blocks an Service-Provider vergeben und es existieren Datenbanken, die man sich herunterladen kann und die dann zu einer gegebenen IP-Adresse blitzschnell deren geographische Lage ermittelt.

Die Firma MaxMind bietet unter [3] eine Datenbankdatei an, die man für nichtkommerzielle Zwecke kostenlos nutzen kann. Die genauen Lizensbedingungen liegen im gleichen Ordner wie die Datenbank selbst. Das CPAN-Modul IP::Country::MaxMind stellt eine passende API zur Verfügung, damit man nicht mit den Binärdaten direkt herumfutzeln muss. Die gespeicherten IP-Zuweisungen ändern sich nur sehr langsam, sodass Updates nur alle paar Monate notwendig sind.

Nach der Installation des Moduls, das auch ein weiteres CPAN-Mudul namens Geo::IP::PurePerl verlangt, liest der Konstruktor open() die angegebene lokale Datenbank ein und die Methode inet_atocc() liefert zu einer IP-Adresse den Ländercode (zum Beispiel DE für Deutschland) zurück.

Für eine graphische Darstellung dieser Codes auf einer Weltkarte bietet sich die Google-Charts-API [4] an. Spielt man dem Google-Server die Wertepaare per URL zu, antwortet dieser mit einer Bilddatei im PNG-Format. Das Datenformat der Wertepaare ist etwas gewöhnungsbedürftig, denn auch mittelgroße Datenmengen müssen in das stark eingeschränkte Platzangebot einer URL mit Query-Parametern passen.

Betont einfach

Das einfachste Datenformat, das sogenannte ``Simple Encoding'' lässt über das API nur Werte von 0 bis 61 zu, kodiert mit 'A-Z' (0-25), 'a-z' (26-51) und '0-9' (52-61).

Weist man beispielsweise Deutschland den Wert 23 zu, den USA den Wert 3 und Japan den Wert 60, kodiert man die Ländercodes im URL-Parameter chld mit ``DEUSJP'' ('DE', 'US', 'JP' ohne Leerzeichen aneinandergereiht) und die Werte in chd mit ``s:XD8'' ('s'=> simple encoding, 'X'=>23, 'D'=>3, '8'=>60).

Das Skript in Listing spam2geo fasst alles bisher gezeigte zusammen. Es analysiert die Datei access.log eines von Linkspam bedrohten Apache-Servers. Das CPAN-Modul ApacheLog::Parser stellt die Funktion parse_line_to_hash bereit, die das Format von access.log versteht und die Einzelfelder in einem Hash zurückliefert. Unter dem Eintrag client steht jeweils die IP-Adresse des Spammers, ein Aufruf der Methode inet_atocc in Zeile 27 gibt den zweibuchstabigen Ländercode zurück, falls die Datenbank ihn findet.

Listing 2: spam2geo

    01 #!/usr/local/bin/perl -w
    02 use strict;
    03 use LWP::UserAgent;
    04 use URI::URL;
    05 use List::Util qw(max min);
    06 
    07 use IP::Country::MaxMind;
    08 use ApacheLog::Parser 
    09                     qw(parse_line_to_hash);
    10 
    11 my $gi = 
    12    IP::Country::MaxMind->open("GeoIP.dat");
    13 
    14 my %by_country;
    15 
    16 open LOG, "<access.log" or 
    17   die "Can't open access.log ($!)"; 
    18 
    19 while(<LOG>) {
    20   chomp;
    21   my %fields = parse_line_to_hash $_;
    22 
    23     # only proceed if forum post
    24   next if $fields{file} !~ /posting/;
    25 
    26   my $country = 
    27         $gi->inet_atocc( $fields{client} );
    28 
    29   if(defined $country) {
    30     $by_country{ $country }++;
    31   }
    32 }
    33 
    34 close LOG;
    35 
    36   # Convert values to Google format
    37 my @SYMBOLS = ("A" .. "Z", 
    38                "a" .. "z", 0 .. 9);
    39 
    40 my $max = max values %by_country;
    41 my $min = min values %by_country;
    42 
    43 for my $country (keys %by_country) {
    44 
    45     my $val = $by_country{ $country };
    46     my $norm = ($val - $min) / 
    47                $max * $#SYMBOLS;
    48 
    49     $by_country{ $country } = $norm;
    50 }
    51 
    52 my $chld  = join "", keys %by_country;
    53 my $data  = join "", 
    54               map { $SYMBOLS[ $_ ] } 
    55               values %by_country;
    56 
    57   # Fetch chart
    58 my $ua = LWP::UserAgent->new();
    59 
    60 my $uri = URI::URL->new(
    61      "http://chart.apis.google.com/chart");
    62 
    63 $uri->query_form(
    64   cht  => "t",
    65   chs  => "440x220",
    66   chtm => "world",
    67   chd  => "s:$data",
    68     # white, yellow, red
    69   chco => "ffffff,f4ed28,f11414",
    70   chld => $chld,
    71     # light blue
    72   chf  => "bg,s,EAF7FE"
    73 );
    74 
    75 my $resp = $ua->get($uri);
    76           
    77   # Print image on success
    78 if($resp->is_success()) {
    79 
    80   open FILE, ">file.png" or die;
    81   print FILE $resp->content();
    82   close FILE;
    83   system ("eog file.png");
    84 } else {
    85 
    86   die $resp->request->url() . " failed\n";
    87 }

Im Erfolgsfall zählt Zeile 30 den Hash-Eintrag dieses Landes um Eins hoch und weiter geht es mit der nächsten Logzeile. Da nicht alle URLs interessieren, sondern nur die von Spammern generierten, filtert Zeile 24 alle Einträge aus, deren Pfadangabe (Hash-Key ``file'') nicht auf den regulären Ausdruck posting passt. Dies ist an die lokalen Verhältnisse anzupassen und sollte nur URLs herausfiltern, die Spammer zum Posten auf dem zu überwachenden Diskussionsforum nutzen.

Normiert kodiert

Ab Zeile 37 beginnt dann die Normalisierung der Daten und die Umwandlung ins Google-Format. Da die Zahlenwerte für jedes Land im Hash %by_country nicht nur im Bereich 0-61 liegen, sondern beliebige Werte annehmen können, muss spam2geo die Grenzen des gesamten Wertebereichs mit min() und max() aus List::Utils ermitteln. Anschließend quetscht sie die darzustellenden Zahlenwerte mittels Subtraktion von $min und Teilung durch $max in den Bereich zwischen 0 und 1 und multipliziert letzteren Wert mit der Anzahl der verfügbaren Codierungszeichen minus 1. So steht in $norm jeweils eine Fließkommazahl, deren Integerwert sich als Index in den Array @SYMBOLS nutzen lässt und so den gesamten Wertebereich auf ein Element dieses Arrays abbildet.

Die Zeilen 52 und 53 setzen dann die ermittelten Werte in Strings ohne trennende Leerzeichen zusammen und bereiten so deren Übergabe in die URL-Parameter chld (Ländercodes) und chd (Werte) vor. Die Reihenfolge, in der die Funktionen keys und values die Hashkeys bzw. Werte zurückliefert, ist aus Programmierersicht zufällig, aber innerhalb eines Perlscripts konstant und dem Google-Service egal.

Die Kommunikation mit dem Google-Server erledigt der LWP::UserAgent über das HTTP-Protokoll. Die URL-Parameter setzt die Methode query_form(), die gleich automatisch eventuell notwendige URL-Kodierungen vornimmt.

Spammer auf Weltkarte

Der Parameter cht gibt den von Googles ``Charts'' service genutzten Charts-Typ an und wird für eine Weltkarte auf "t" (wohl für 'topological') gesetzt. Es besteht die Möglichkeit, die Ansicht auf einzelne Kontinente zu beschränken, doch für eine gesamte Weltkarte steht der Parameter chtm auf ``world''.

Die Ausmaße des produzierten Bildes gibt der Parameter chs mit 440x220 Pixeln vor. Die in chco als Hex-RGB-Werte angegebenen Farben Weiß, Gelb und Rot legen die durchzuführende Länder-Einfärbung für minimale, mittlere und maximale Werte fest. In der vorgegebenen Einstellung bleiben Länder mit Werten um 0 weiß, Werte um 30 färben ein Land Gelb, und Werte um 60 produzieren Rot. Der String ``bg,s,EAF7FE'' für den Parameter chf steht für ``background'' (Hintergrundfarbe), ``solid'' (ganzflächig), und dem Hex-Wert für ein ganz helles Blau für die Ozeane der Welt.

Alles zusammen ergibt dann einen URL wie zum Beispiel

    http://chart.apis.google.com/chart?cht=t&chs=440x220&chtm=world&;
    chd=s%3ABFAABAHGQAAA8BAAAAAAAaBAA&chco=ffffff%2Cf4ed28%2Cf11414&
    chld=GBNLHKEELVKRRUSAPAMDCASECNDEPKITPLINMEBRCZUSUAESFR&
    chf=bg%2Cs%2CEAF7FE

wofür Google innerhalb von Sekundenbruchteilen einen Graphen wie den in Abbildung 4 liefert. Kommentiert man Zeile 24 in spam2geo hingegen aus, bildet der Graph die Verteilung aller eingehenden URLs ab, wie in Abbildung 5 gezeigt. Während also die meisten Spam-Requests aus China und den USA stammen, ist die Hauptkundschaft der Website im deutschen Raum zu suchen. Der System-Befehl ``eog file.png'' zeigt die von Google produzierte und per Webrequest eingeholte Datei sofort mit der Utility ``eye of gnome'' an.

Abbildung 4: Spammer kommen hauptsächlich aus China und Nordamerika.

Abbildung 5: User der Website, hauptsächlich aus Deutschland.

Installation

Nach dem Herunterladen der MaxMind-Datenbank GeoIP.dat.gz von [3] sollte die dekomprimierte Datei GeoIP.dat mitsamt dem Skript spam2geo im aktuellen Verzeichnis landen. Die CPAN-Module IP::Country::MaxMind, Geo::IP::PurePerl, List::Utils, ApacheLog::Parser, und alle ihre Abhängigkeiten installieren sich am einfachsten mit einer CPAN-Shell. Zur Nutzung der Google-API ist keinerlei Registrierung erforderlich. Zeile 24 in spam2geo muss noch auf die lokalen Verhältnisse angepasst werden, in dem der Pattern-Match /posting/ so verändert wird, dass er nur auf URLs passt, die Spammer nutzen, um Diskussionsforen mit ihren parasitären Eintragen zuzupflastern.

Für tiefergehende Analysen wie zum Beispiel die Anzahl der Forum-Requests im Vergleich zu anderen Aktivitäten oder die bevorzugten (unter Umständen simulierten) Browsertypen der Spammer, sei auf die reiche Auswahl der Google-Charts-API auf [4] hingewiesen, die derlei Information in ähnlich eleganten Charts grafisch aufbereitet.

Infos

[1]

Listings zu diesem Artikel: ftp://www.linux-magazin.de/pub/listings/magazin/2009/01/Perl

[2]

Snickers-Werbung, ein genervter Mann im Schlafanzug stöbert einen Telemarketer auf: http://www.youtube.com/watch?v=R6QATC2C0h8

[3]

Die freie MaxMind-GeoIP-Datenbank zum Herunterladen: http://www.maxmind.com/download/geoip/database/

[4]

``Maps''-Charts des Google Charts Webservice: http://code.google.com/apis/chart/types.html#maps

Michael Schilli

arbeitet als Software-Engineer bei Yahoo! in Sunnyvale, Kalifornien. Er hat "Goto Perl 5" (deutsch) und "Perl Power" (englisch) für Addison-Wesley geschrieben und ist unter mschilli@perlmeister.com zu erreichen. Seine Homepage: http://perlmeister.com.