Aus Linux-Magazin 09/2015

Ein Perl-Skript als Sniffer mit eingebauter Statistik

© sirikorn thamniyom, 123RF

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.

Online PLUS

Im Screencast demonstriert Michael Schilli das Beispiel: [https://www.linux-magazin.de/Ausgaben/2015/09/plus].

Ein schnell installiertes und einfach zu bedienendes Analysetool wie Wireshark enthüllt, dass durch den Aufruf einer einzigen Nachrichtenseite im Internet 3000 Netzwerkpakete und mehr hin und her schwirren können, die auf ein paar Dutzend verschiedenen Internetseiten die Fingerabdrücke des jeweiligen Benutzers hinterlassen.

Browser als Posaune

Jede Station im Internet, an der der User auf seinem Surftrip vorbeikommt, um seinen Nachrichtendurst zu stillen, muss ihm seine kürzlich bei 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 Makulatur 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 samt Surfverhalten in alle Welt hinaus.

Anders als das mächtige GUI-Tool Wireshark (Abbildung 1) lässt sich das im Folgenden vorgestellte Perl-Skript einfach wie das Utility Top in einem Terminalfenster starten (Abbildung 2). Es zeigt eine nach Häufigkeit sortierte und permanent aktualisierte Liste der ermittelten Ziele aller aus der Leitung gesogenen Netzwerkpakete, wobei die Adressen via DNS in Namen aufgelöst sind.

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

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

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

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

Eine Klippe gilt es vorab noch zu umschiffen: Der Linux-Kernel bietet einem Prozess nur dann die rohen Netzwerkpakete an, wenn dieser unter Rootrechten läuft oder über entsprechende Unix-Capabilities verfügt. Wireshark mit Rootrechten laufen zu lassen, erschien seinen 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.

Nicht als Root

Ruft der Admin auf einem Ubuntu- oder Debian-System 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« (wenn vorhanden) oder mit einem Set-User-ID-Bit dergestalt auf, dass jedes Mitglied der Unix-Gruppe Wireshark Zugriff auf rohe Netzwerkdaten erhält. Andere Distributionen richten das bei der Paketinstallation von vornherein so ein. Der an den Rohpaketen interessierte Nutzer muss dann nur noch mittels

sudo usermod -a -G wireshark Username

der Wireshark-Gruppe beitreten, und nach erfolgtem Aus- und Einloggen kann das Paketsammeln unter dem nicht privilegierten Account beginnen.

Kommando statt GUI

Wiresharks kleiner Bruder Tshark, den Ubuntu mit dem Kommando

sudo apt-get install tshark

installiert, verfügt über ähnliche Fähigkeiten zum Abfangen und Filtern von Netzwerkpaketen und eignet sich daher gut zum Skript-gestützten Datensammeln. Da das Ubuntu-Paket »tshark« zur Unterstützung ebenfalls das Paket »wireshark-common« 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 Ethernet-Karte findet sich hier zum Beispiel als »eth0« , eine Wifi-Schnittstelle als »en0« und das Loopback-Interface als »lo« . Wer sich nicht entscheiden kann und auf allen Kanälen gleichzeitig lauschen möchte, kann später auch »any« angeben.

Den eintrudelnden Datenstrom speichert Tshark nun entweder 1:1 im binären PCAP-Format oder filtert ihn platzsparend vor. Der Aufruf

tshark -i any -T fields -e ip.dst

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, diese und weitere Daten aus der Ausgabe von Tshark entgegenzunehmen, zu kumulieren und das Ergebnis als aufgefrischte Liste mit Zählern in einem unter Curses laufenden Terminal anzuzeigen.

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;

Wer bin ich?

Listing 1 implementiert das Top-ähnliche Skript in Abbildung 2 und nutzt dazu das flexible Event-Framework »AnyEvent« und die beiden Module »TopCapture« und »TopGUI« . Sie fangen die Netzwerkpakete ein und stellen sie in einem dynamischen Curses-UI dar. Zeile 8 startet den Capture-Vorgang mit »tshark« in »TopCapture« mit dessen Methode »start()« .

Damit das GUI später nicht die IP des aktuellen Hosts anzeigt, die ja in jedem Paket entweder als Ziel- oder Senderadresse enthalten ist, versucht Zeile 15 diese herauszufinden, indem sie einen Socket mit Googles Webserver als Ziel aufmacht (aber keine Verbindung aufbaut) und dann mit »sockhost()« die IP-Adresse ausliest, die der Netzwerkstack dort als Sender eingebaut hat.

Dieses Verfahren funktioniert tadellos, auch 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, wird ihre Darstellung unterdrückt.

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 oft die gesammelten DNS-Namen jeweils in Paketen vorkamen.

GUI im Kuckucksnest

Am Ende von Listing 1 startet das GUI, das mit dem CPAN-Modul Curses::UI::POE implementiert ist, ein zu »AnyEvent« konkurrierendes, aber mit ihm einseitig kompatibles Eventmodell. Die in der Methode »start()« des Moduls »TopGUI« aufgerufene Eventschleife ab Zeile 44 arbeitet – ohne es zu wissen – auch die von »AnyEvent« eingebauten Einträge wie den Timer und die Paketsammel-Callbacks ab.

Listing 2 nutzt die Module AnyEvent::DNS und AnyEvent::Run vom CPAN zur asynchronen Namensauflösung von IP-Adressen und zum Abfeuern eines externen Tshark-Prozesses. Um Platz zu sparen, zieht Zeile 6 Moo herein, weswegen 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 ###########################################i
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
      !exists $self->{ dns_cache };
24
25    my @tshark_cmd = qw( tshark -q
   -  i any -T fields
   -  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 Eventschleife stetig weitertickt, darf die Methode »start()« ab Zeile 18 nicht einfach »tshark« aufrufen und warten, bis es Ergebnisse ausspuckt, sondern nutzt AnyEvent::Run, das mit »fork()« ein Prozesskind hochfährt und jedes Mal den Callback »on_read« ansteuert, wenn auf der Standardausgabe ein Fitzelchen Text erscheint. Aber nicht einzelne Bits, sondern nur ganze Zeilen sind für die Applikation interessant, also schiebt »push_read()« in Zeile 35 die Einzelbits zurück in den Puffer und fordert »AnyEvent« auf, unter dem mit »line« definierten Callback 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 oder etwa den einleitenden Kommentar, den »tshark« auch zum Besten gibt.

Einen besseren Eindruck als die Liste der blanken IP-Adressen geben deren per DNS aufgelöste Hostnamen. Die Daten fallen aber oft zügiger an, als externe DNS-Resolver sie abarbeiten können. Daher legt Listing 2 im Hash »dns_cache« zunächst die IP-Adresse ab, ruft zugleich per 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 IP-Adressen – gerade solche aus dem lokalen Netzwerk oder vom eigenen ISP – verfügen oft über gar keinen Namenseintrag. In diesem Fall lässt der »dns_cache« einfach die IP-Adresse stehen.

Wer einen Chrome-Browsers nutzt, erfährt so, 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 und oft mit dem Mutterschiff.

Die Methode »stats()« ab Zeile 74 gibt die Anzahl der bislang analysierten Pakete sowie eine Referenz auf einen Hash zurück, der Paketzählern aufgelöste Hostnamen zuweist. Listing 3 implementiert das Top-ähnliche GUI mit Curses und kommuniziert mit dem Hauptprogramm über die von Object::Event vererbten Methoden »event()« (Event senden) und »reg_cb()« (Events empfangen und Callback anspringen). Die Methode »start()« baut das 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. 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. Der DNS-Cache bleibt hingegen erhalten.

[C] bleicht GUI

Drückt der User also [C], sendet der Callback in Zeile 46 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. Das ruft daraufhin in Zeile 12 die Methode »reset()« im »TopCapture« -Modul auf, worauf dieses seine Zähler auf null setzt. Der nächste Timer-Aufruf mit »stats()« findet leere Einträge vor, die der Aufruf der Methode »update()« in Zeile 40 von Listing 1 dem »TopGUI« -Modul mitteilt, das seinerseits das GUI für einen Neustart der Zähler bei null bereinigt.

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 hofft, dass Tshark auch Pakete im gleichen Subnetz abgreift, die gar nicht an den aktuellen Host adressiert sind, täuscht sich. Moderne Switches schicken (anders als die vor 20 Jahren gängigen Hubs) nicht alle Pakete an alle angeschlossenen Interfaces. Pakete, die nicht an den Host, auf dem »tshark« läuft, gerichtet sind, kommen also physisch gar nicht erst dort an.

In einem Wireless LAN dagegen gilt, dass Pakete zwar auf allen Wifi-Karten im Empfangsbereich ankommen, diese jedoch selten in einen Modus schaltbar sind, der auch Pakete an andere Adressaten einkassiert. Ist Tshark deshalb nicht auf einem Router installiert, zeigt es nur die vom lokalen Host abgehenden und dort eintrudelnden Pakete an.

Der Autor

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 mailto:mschilli@perlmeister.com beantwortet er gerne Fragen.

DIESEN ARTIKEL ALS PDF KAUFEN
EXPRESS-KAUF ALS PDFUmfang: 4 HeftseitenPreis €0,99
(inkl. 19% MwSt.)
LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Inline Feedbacks
Alle Kommentare anzeigen
Nach oben