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.
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. |
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.
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.
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.
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.
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.
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;
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.
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.
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.
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.
Listings zu diesem Artikel: ftp://www.linux-magazin.de/pub/listings/magazin/2015/09/Perl