Hai als Haustier (Linux-Magazin, September 2015)

Zum Stöbern in vorbeirauschenden Paketen im lokalen Netzwerk leistet Platzhirsch Wireshark gute Dienste. Wer lieber eigene Tools baut, greift auf die Kommandozeilenversion tshark zurück.

Wer lüftet schon jemals den Vorhang, der verdeckt, was eigentlich hinter den Kulissen abgeht, während ein argloser User im Browser die Titelseite eines etablierten Nachrichtenanbieters ansteuert? Ein flugs installiertes und relativ einfach zu bedienendes Analysetool wie Wireshark enthüllt, dass nicht selten durch eine einzige Anfrage 3000 Netzwerkpakete hin- und herschwirren, die auf ein paar Dutzend verschiedener Internetseiten die Fingerabdrücke des Benutzers hinterlassen.

Browser als Posaune

Schließlich muss jede Station im Internet, an der der User auf seinem Surftrip vorbeizieht, um seinen Nachrichtendurst zu stillen, ihm seine kürzlich auf Amazon begutachteten Güter wieder unter die Nase reiben und zu jedem Unfug Facebook-Like-Buttons präsentieren. Die in der Steinzeit des Internets von Netscape eingeführte "Same Origin Policy" für Cookies zur Wahrung der Privatsphäre ist dank Grenzdurchbrechern wie Doubleclick zur Makulator verkommen. Jeder User kann mittlerweile davon ausgehen, dass nicht nur die angesteuerte Seite weiß, dass er wieder da ist, sondern auch noch ein Dutzend zahlender Neugieriger. Falls der User nicht eigenhändig drastische Schritte einleitet, posaunt der Browser seine Identität mitsamt Surfverhalten permanent in alle Welt hinaus.

Abbildung 1: Die Wireshark-GUI zeigt abgefangene Pakete an.

Anders als das mächtige GUI-Tool Wireshark in Abbildung 1 lässt sich das heute vorgestellte Perlskript einfach wie die Utility top wie in Abbildung 2 gezeigt in einem Terminalfenster starten. Es zeigt eine nach Häufigkeit sortierte und permanent aktualisierte Liste der nach DNS-Auflösung ermittelten Ziele aller aus der Leitung gesogenen Netzwerkpakete.

Abbildung 2: Das Skript topaddr gibt die am häufigsten gefundenen Paketadressen aus.

Nicht als Root

Der Linux-Kernel bietet einem Prozess nur dann die rohen Netzwerkpakete an, wenn dieser unter Root läuft oder über entsprechende Unix-Capabilities verfügt. Wireshark oder vergleichbare Analyseprodukte als Root laufen zu lassen erscheint deren Machern aber zu riskant, also bieten sie im Paket wireshark-common ein Verfahren an, das einen neuen Unix-User namens wireshark mit gleichnamiger Unix-Gruppe anlegt. Ruft der Admin das Kommando

    sudo dpkg-reconfigure wireshark-common

auf, peppt das Postinst-Skript das von Wireshark genutzte Capture-Skript /usr/bin/dumpcap entweder mit den Linux-Capabilities cap_net_raw und cap_net_admin (wo vorhanden) oder einem set-user-ID Bit dergestalt auf, dass jedes Mitglied der Unix-Gruppe wireshark Zugriff auf rohe Netzwerkdaten erhält. Der an den Rohpaketen interessierte User muss nun nur noch mittels

    sudo usermod -a -G wireshark <user-name>

der wireshark-Gruppe beitreten, und nach erfolgtem Aus- und wieder Einloggen (!) funktioniert dann das Paketsammeln unter dem nichtpriviligierten Account.

Kommando statt GUI

Wiresharks kleiner Bruder tshark, den Ubuntu mit

    sudo apt-get install tshark

installiert, verfügt über ähnliche Fähigkeiten zum Abfangen und Filtern von Netzwerkpaketen und eignet sich daher hervorragend zur skriptgestützten Datensammelei. Da das Ubuntu-Paket tshark zur Unterstützung ebenfalls das Paket wireshark-common mit heranzieht, erlaubt der oben beschriebene Aufruf von dkpg-reconfigure tshark ebenfalls den Zugriff auf die rohen Netzdaten. Eine Liste ansprechbarer Netzwerkschnittstellen gibt der Aufruf tshark -D aus, eine Ethernetkarte steht hier zum Beispiel als eth0, eine Wifi-Schnittstelle unter en0 und das Loopback-Interface unter lo. Wer sich nicht entscheiden kann und auf allen Kanälen gleichzeitig lauschen möchte, kann später auch any angeben.

Listing 1: topaddr

    01 #!/usr/local/bin/perl -w
    02 use strict;
    03 use TopGUI;
    04 use AnyEvent;
    05 use TopCapture;
    06 
    07 my $tsc = TopCapture->new;
    08 $tsc->start;
    09 
    10 my $tsg = TopGUI->new();
    11 $tsg->reg_cb( "clear", sub {
    12   $tsc->reset;
    13 } );
    14 
    15 my $my_ip = IO::Socket::INET->new(
    16     PeerAddr => 'google.com',
    17     PeerPort => 80,
    18   )->sockhost;
    19 
    20 my $timer = AnyEvent->timer(
    21   after    => 0,
    22   interval => 1,
    23   cb => sub {
    24     my( $packets, $dnsmap ) = $tsc->stats;
    25 
    26     my $max = $tsg->{ lbox }->height();
    27     my @lines = ();
    28 
    29     for my $name ( sort { $dnsmap->{ $b } 
    30       <=> $dnsmap->{ $a } } 
    31         sort keys %$dnsmap ) {
    32       
    33       next if $name eq $my_ip;   
    34 
    35       push @lines, 
    36         sprintf "%4d %s", 
    37           $dnsmap->{ $name }, $name;
    38         last if --$max == 0;
    39     }
    40     $tsg->update( $packets, \@lines );
    41   },
    42 );
    43 
    44 $tsg->start;

Den eintrudelnden Datenstrom kann tshark nun entweder 1:1 im binären PCAP-Format abspeichern oder platzsparend vorfiltern. Der Aufruf

    $ tshark -i any -T fields -e ip.dst
    Capturing on 'any'
    216.58.192.4
    198.252.206.16
    ...

gibt zum Beispiel nur die Zieladresse der vorbeiflitzenden Pakete aus, und führt auf einem aktiven System trotzdem zu einer beachtlichen Datenflut. Ziel des Skripts in Listing 1 ist es nun, diese und weitere Daten aus der Ausgabe von tshark entgegenzunehmen, aufzukumulieren und das Ergebnis als aktiv aufgefrischte Liste mit Zählern in einem unter Curses laufenden Terminal anzuzeigen.

Wer bin ich?

Listing 1 implementiert das Top-ähnliche Skript in Abbildung 2 und nutzt hierzu das flexible Event-Framework AnyEvent und die beiden später erläuterten Module TopCapture und TopGUI zum Einfangen der Netzwerkpakete und deren Darstellung in einer dynamischen Curses-UI. Zeile 8 startet den Capture-Vorgang mit tshark in TopCapture mit dessen Methode start(). Damit die GUI später nicht die IP des aktuellen Hosts anzeigt, die ja in jedem Paket entweder als Ziel- oder Senderadresse steht, versucht Zeile 15, diese dadurch herauszufinden, dass sie einen Socket mit Googles Webserver als Ziel aufmacht (aber keine Verbindung aufbaut) und dann mit sockhost() die IP-Adresse ausliest, die der Network-Stack dort als Sender eingebaut hat. Dieses Verfahren funktioniert auch tadellos, wenn mehrere Netzwerkschnittstellen im Host verfügbar sind, da ein neu aufgebauter Socket automatisch die richtige nimmt. Findet Zeile 33 diese Adresse in einem Paket, kommt sie nicht zur Darstellung.

Der ab Zeile 20 definierte Timer ruft im Sekundentakt den mit der Option cb definierten Callback auf, holt dort mit stats() in TopCapture den aktuellen Paketzählerwert und einen Hash ab, der anzeigt, wie häufig die gesammelten DNS-Namen jeweils in Paketen vorkamen.

GUI im Kuckucksnest

Am Ende von Listing 1 startet GUI, die lustigerweise mit dem CPAN-Modul Curses::UI::POE implementiert ist, ein zu AnyEvent konkurrierendes aber mit AnyEvent (einseitig) kompatibles Event-Modell. Die in der Methode start() des Moduls TopGUI aufgerufene Eventschleife ab Zeile 44 arbeitet unwissenderweise auch die von AnyEvent eingebauten Einträge wie den Timer und die Paketsammelcallbacks ab.

Listing 2 nutzt die Module AnyEvent::DNS und AnyEvent::Run vom CPAN zur asynchronen Namensauflösung von IP-Adressen und dem Abfeuern eines externen tshark-Prozesses. Aus Platzspargründen im Heft zieht Zeile 6 Moo herein, wegen dem der Konstruktor new() wegfällt. Die drei Instanzvariablen ip_counts, packet_count und dns_cache speichern die Häufigkeit aller IP-Adressen, die Gesamtzahl aller analysierten Pakete und einen Cache, der IP-Adressen Hostnamen zuordnet, die beim Reverse-Lookup vom DNS-Server zurückkamen. ip_counts und packet_count lassen sich mit der Methode reset() zurücksetzen.

Listing 2: TopCapture.pm

    01 package TopCapture;
    02 use strict;
    03 use warnings;
    04 use AnyEvent::DNS;
    05 use AnyEvent::Run;
    06 use Moo;
    07 
    08 ###########################################
    09 sub reset {
    10 ###########################################
    11   my( $self ) = @_;
    12 
    13   $self->{ ip_counts }    = {};
    14   $self->{ packet_count } = 0;
    15 }
    16 
    17 ###########################################
    18 sub start {
    19 ###########################################
    20   my( $self ) = @_;
    21 
    22   $self->reset;
    23   $self->{ dns_cache } = {} if 
    24     !exists $self->{ dns_cache };
    25 
    26   my @tshark_cmd = qw( tshark -q
    27     -i any -T fields 
    28     -e ip.src -e ip.dst );
    29 
    30   $self->{ runner } = AnyEvent::Run->new(
    31     cmd      => \@tshark_cmd,
    32     on_read  => sub {
    33       my( $handle ) = @_;
    34 
    35       $handle->push_read( line => sub {
    36         my( $handle, $line ) = @_;
    37 
    38         $self->line_process( $line );
    39       } ) },
    40     on_error => sub { die "CapError: $!" }
    41   );
    42 }
    43 
    44 ###########################################
    45 sub line_process {
    46 ###########################################
    47   my( $self, $line ) = @_;
    48 
    49   my @ips = split ' ', $line;
    50   return 1 if scalar @ips != 2;
    51 
    52   $self->{ packet_count }++;
    53 
    54   for my $ip ( @ips ) {
    55     $self->{ ip_counts }->{ $ip }++;
    56 
    57     my $dns = $self->{ dns_cache };
    58     next if exists $dns->{ $ip };
    59     $dns->{ $ip } = $ip;
    60 
    61     AnyEvent::DNS::reverse_lookup( $ip, 
    62       sub {
    63         my( $hostname ) = @_;
    64 
    65         return if !defined $hostname or
    66           $hostname eq "unknown";
    67 
    68         $dns->{ $ip } = "$hostname";
    69       } );
    70   }
    71 }
    72 
    73 ###########################################
    74 sub stats {
    75 ###########################################
    76   my( $self ) = @_;
    77 
    78   return ( $self->{ packet_count }, {
    79     map { $self->{ dns_cache }->{ $_ } 
    80           => $self->{ ip_counts }->{ $_ } }
    81     sort keys %{ $self->{ ip_counts } } } );
    82 }
    83 
    84 1;

Nur ganze Zeilen

Damit die laufende die Eventschleife stetig weiter tickt, darf die Methode start() ab Zeile 18 in Listing 2 nicht einfach tshark aufrufen und warten, bis letzteres Ergebnisse ausspuckt, sondern nutzt AnyEvent::Run, das mittels fork() ein Prozesskind hochfährt und jedesmal den Callback on_read anfährt, wenn auf der Standartausgabe ein Fitzelchen Text erscheint. Da die Applikation aber nicht an einzelnen Bits sondern nur ganzen Zeilen interessiert ist, schiebt push_read() in Zeile 35 die Einzelbits wieder zurück in den Puffer und fordert AnyEvent auf, unter dem mit line definierten Callback doch bitte erst zurückzurufen, wenn eine Zeile vollständig vorliegt. Sobald dies passiert, verzweigt TopCapture zur Methode line_process() ab Zeile 45.

Dort prüft Zeile 50 zunächst, ob die Ausgabe tatsächlich zwei IP-Adressen (ip.src und ip.dst wie von tshark in Zeile 28 gefordert) enthält und nicht etwa den einleitenden Kommentar, den tshark ebenfalls zum Besten gibt. Einen besseren Eindruck als die Liste der blanken IP-Adressen geben deren per DNS aufgelöste Hostnamen. Die Daten fallen allerdings oft zügiger an, als externe DNS-Resolver sie abarbeiten können. Deshalb legt Listing 2 im Hash dns_cache zunächst die IP-Adresse ab, ruft gleichzeitig mittels AnyEvent::DNS asynchron den DNS-Server an und fährt mit der Verarbeitung weiterer Pakete fort. Antwortet der DNS-Server geraume Zeit später, springt Listing 2 in den Callback ab Zeile 62 und flickt das Ergebnis in den Eintrag in dns_cache ein, der bei später ankommenden IP-Adressen den zeitraubenden Lookup einsparen hilft.

Diese Reverse-Lookups, die ausgehend von der IP-Adresse den Hostnamen beim DNS-Resolver erfragen, funktionieren nicht immer, denn manche IPs, gerade wenn es sich um solche aus dem lokalen Netzwerk oder vom eigenen ISP handelt, verfügen oft über gar keinen Namenseintrag. In diesem Fall lässt der dns_cache einfach die IP-Adresse stehen.

Chrome an Mutterschiff

Bei der Verwendung eines Chrome-Browsers zeigt sich, dass dieser oft mit IPs kommuniziert, die der DNS-Server in die merkwürdig aussehende Domain *.1e100.net auflöst. Ein kurzer Blick ins Internet zeigt, dass es sich bei 1e100.net um niemand anderen als Google handelt, wobei 1e100 eine Anspielung auf die Zahl mit 100 Nullen ist, die in Fachkreisen auch als ein "Googol" bekannt ist. Chrome telefoniert also gerne mal mit dem Mutterschiff während der User im Netz herumsurft.

Die Methode stats() ab Zeile 74 gibt interessierten Abonenten die Anzahl der bislang analysierten Pakete sowie eine Referenz auf einen Hash zurück, der aufgelöste Hostnamen Paketzählern zuweist. Die top-ähnliche GUI implementiert Listing 3 mit Curses und kommuniziert mit dem Hauptprogramm über die von Object::Event per Vererbung eingeschleusten Methoden event() (Event senden) und reg_cb() (Events empfangen und Callback anspringen). Die Methode start() baut die GUI aus einer Kopfzeile top, einer Listbox lbox und einer Fußzeile bottom zusammen.

Listing 3: TopGUI.pm

    01 package TopGUI;
    02 use strict;
    03 use warnings;
    04 use Curses::UI::POE;
    05 use base qw( Object::Event );
    06 use Moo;
    07 
    08 ###########################################
    09 sub start {
    10 ###########################################
    11   my( $self ) = @_;
    12 
    13   $self->{ cui } = Curses::UI::POE->new(
    14       -color_support => 1 );
    15 
    16   $self->{ win } = 
    17    $self->{ cui }->add("win_id", "Window");
    18 
    19   my @loptions = qw( -width -1 
    20     -paddingspaces 1 -fg white -bg blue );
    21 
    22   $self->{ top } = $self->{ win }->add(
    23     qw( top Label -y 0 ), @loptions, 
    24     -text => "" );
    25 
    26   $self->{ lbox } = $self->{ win }->add(
    27     qw( lb Listbox
    28         -padtop 1 -padbottom 1 -border 1 ),
    29   );
    30 
    31   my $footer = "Type [c] to clear " .
    32     "counters [q] to quit";
    33 
    34   $self->{ bottom } = $self->{ win }->add(
    35     qw( bottom Label -y -1), @loptions,
    36     -text => $footer );
    37 
    38   $self->reg_cb( "update", sub {
    39     $self->update( @_ );
    40   } );
    41 
    42   $self->{ cui }->set_binding(
    43     sub { exit 0; },   "q");
    44 
    45   $self->{ cui }->set_binding(
    46     sub { $self->event( "clear" ) }, "c");
    47 
    48   $self->{ cui }->mainloop;
    49 }
    50 
    51 ###########################################
    52 sub update {
    53 ###########################################
    54   my( $self, $packets, $lines ) = @_;
    55 
    56   $self->{ top }->text( 
    57     "Packets captured: $packets" );
    58   $self->{ top }->draw();
    59 
    60   $self->{ lbox }->{-values} = $lines;
    61   $self->{ lbox }->{-labels} = { 
    62       map { $_ => $_ } @$lines };
    63 
    64   $self->{ lbox }->draw(1);
    65   $self->{ bottom }->draw();
    66 }
    67 
    68 1;

In der Kopfzeile erhöht Listing 3 mit eintrudelnden Werten stetig die Anzahl aller bislang analysierten Pakete. Mit "q" beendet der User das Programm, und wie alle Tastatur-Events fängt Curses sie mittels set_binding() in Zeile 42 ab. Mit der Taste "c" setzt der User alle Zähler auf Null zurück, die Liste der Top-Hosts verschwindet und erhält beim nächsten Durchlauf des Timers neue Werte nach den aktuell gesammelten Daten. Der DNS-Cache bleibt hingegen erhalten, damit das Skript den DNS-Server nicht von neuem belästigen muss.

"c" bleicht GUI

Drückt der User also "c", sendet der Callback in Zeile 64 mit event() einen Event an interessierte Parteien, die sich vorher mit reg_cb() beim Objekt der Klasse TopGUI angemeldet haben, im vorliegenden Fall das Hauptprogramm in Listing 1. Dieses ruft daraufhin im Callback in Zeile 12 von Listing 1 die Methode reset() im TopCapture-Modul auf, worauf dieses seine Zähler auf Null zurücksetzt, und der nächste Timer-Aufruf mit stats() leere Einträge vorfindet, die der Aufruf der Methode update() in Zeile 40 von Listing 1 dem TopGUI-Modul mitteilt, welches wiederum die GUI für einen Neustart der Zähler bei Null reinwäscht.

Kommen neue Paketdaten an, bleibt Listing 1 nur, die Stats-Daten nach Zählerstand zu sortieren, mit sprintf() zu formatieren und wiederum update() im Modul TopGUI aufzurufen.

Wer übrigens hofft, dass tshark auch Pakete im gleichen Subnetz abgreift, die gar nicht an den aktuellen Host adressiert sind, schaut in die Röhre. Moderne Switches schicken (anders als die vor 20 Jahren gängigen "Hubs") nicht mehr alle Pakete an alle eingestöpselten Netzwerkkarten. Nicht an den Host auf dem tshark läuft gerichtete Pakete kommen also physikalisch gar nicht erst dort an, können also weder vom Kernel noch von tshark interpretiert werden. In einem Wireless-LAN gilt hingegen, dass Pakete physikalisch zwar auf allen Wifi-Karten im Empfangsbereich ankommen, letztere jedoch selten in den sogenannten "Monitor Mode" schaltbar sind, der auch Pakete an andere Adressateni einkassiert. Ist tshark deshalb nicht gerade auf etwas wie einem als Router konfigurierten System installiert, zeigt es nur vom Host abgehende und dort eintrudelnde Pakete an.

Infos

[1]

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

Michael Schilli

arbeitet als Software-Engineer in der San Francisco Bay Area in Kalifornien. In seiner seit 1997 laufenden Kolumne forscht er jeden Monat nach praktischen Anwendungen der Skriptsprache Perl. Unter mschilli@perlmeister.com beantwortet er gerne Ihre Fragen.