Rootkits ermöglichen es Angreifern, einen Rechner komplett zu kontrollieren. Dieser Beitrag beschreibt die Tricks, mit denen sich Eindringlinge Zugang zum Linux-Kern verschaffen. Daneben gibt er eine Anleitung, wie man den Kernel gegen solche Angriffe härtet.
Auf eine kleineparlamentarische Anfrage antwortete die deutsche Bundesregierung im Mai 2012 sinngemäß, sie sei in der Lage, PGP-Nachrichten oder SSH-Verbindungen zumindest teilweise zu entschlüsseln [1]. Ausgerechnet ein deutscher Geheimdienst hat die Lösung für ein Problem, an dem sich Sicherheitsexperten aller Länder seit Jahren die Zähne ausbeißen?
Seriöse Fachleute verdrehen bei dieser Nachricht nur die Augen. Keiner glaubt ernsthaft, die Verschlüsselung von PGP & Co. sei geknackt. Die Substanz der Meldung liegt vermutlich darin, dass sich die Daten beim Senden oder nach dem Empfangen abfangen lassen, also vor der Ver- oder nach der Entschlüsselung. Unter Umständen kann der Geheimdienst auch in den Besitz des privaten Schlüsselteils und des möglicherweise dazu gehörenden Passworts gelangen.
Per Staatstrojaner beispielsweise wäre das alles kein Problem. Dieser klinkt sich hierfür als Rootkit in den Kernel ein. Der Begriff Rootkit bezeichnet klassischerweise Software, die einem Angreifer getarnt Zugang und damit Kontrolle über einen Rechner gibt. Userland-Rootkits modifizieren dazu vor allem Applikationen. Die viel mächtigeren Kernel-Rootkits dagegen verändern die Datenstrukturen und den Code des Betriebssystemkerns, beispielsweise in Form des so genannten Systemcall-Hijacking – der feindlichen Übernahme von Systemaufrufen.
Userland-Rootkits
In den Ubuntu-Repositories finden sich mit »rkhunter« und »chkrootkit« zwei Pakete, die Rootkits aufspüren. Sie sind allerdings für Userland-Rootkits gedacht und können nur sehr eingeschränkt Kernel-Rootkits entdecken. Userland-Rootkits verbergen Trojaner in normalen Applikationen, beispielsweise einer Shell oder auch einem Officeprogramm, um von dort aus Rechner beziehungsweise Anwender auszuspähen.
Typischerweise sind sie dem Opfer leichter unterzuschieben, haben im Gegenzug aber weniger Kontrolle über den Rechner. Bekannte Rootkits sind ähnlich wie Viren über Prüfsummenvergleiche zu identifizieren.
Gekapert
Das Hijacken eines Systemcalls ist in der Theorie einfach. Ein Systemcall, der einen Auftrag für den Betriebssystemkern darstellt, ist durch eine Nummer gekennzeichnet, die als Index in einer Tabelle mit Funktionsadressen dient. Diese Adressen verweisen auf die Funktionen, die den jeweiligen Systemaufruf implementieren. Im Linux-Kernel heißt diese Tabelle »sys_call_table« .
Um den Systemcall zu hijacken, tauscht der Angreifer die jeweilige Funktionsadresse gegen die Adresse seiner typischerweise bösartigen Funktion aus (Abbildung 1). Im Rahmen dieser Funktion kann er Aufrufparameter ändern, den Code des originalen Systemcalls aufrufen und die Ergebnisse kopieren oder abändern. Um etwa alle Tastatureingaben und damit auch eingegebene Passwörter mitzuprotokollieren, tauscht der Angreifer den Systemcall »sys_read()« gegen seine bösartige Variante aus, im Beispiel »evil_sys_read()« genannt.
Diese Funktion ruft zunächst den originalen Systemcall »sys_read()« auf, der die angeforderten Daten über die Hardware einliest und in den Speicherbereich der Applikation (Userland) kopiert. Bevor »evil_sys_read()« der Applikation die Kontrolle zurückgibt, greift sie auf die Daten im Userland zu und leitet sie beispielsweise über das Internet an einen Server des Angreifers weiter.
Linux wird sicherer
Kernel 2.4 machte dem Angreifer die geschilderte Übernahme vergleichsweise einfach. Mit Hilfe eines zur Laufzeit ladbaren Kernelmoduls (Loadable Kernel Module, LKM) konnte er die Anfangsadresse der »sys_call_table« auslesen und deren Einträge austauschen. Die Kernelversionen 2.6 und 3.0 haben diesen Angriff erschwert. Die Adresse der »sys_call_table« ist nicht mehr global definiert, sie ist einem Kernelmodul demnach unbekannt. Zusätzlich liegt die »sys_call_table« in einem Segment, das nicht verändert werden darf.
Es gibt jedoch mehrere Angriffsvektoren, um die Adresse der »sys_call_table« zu bestimmen und das nicht schreibbare Segment zu beschreiben. Mit den Programmzeilen in Listing 1 können Angreifer auch schreibgeschützte Code- und Datenbereiche im Kernel verändern [2]. Das Aushebeln des Schreibschutzes ist vergleichsweise einfach, denn der Angreifer besitzt ja Rootrechte und führt bereits Code im Kernel aus.
Listing 1
Schreibgeschützte Bereiche verändern
01 void disable_write_protection_cr0( void )
02 {
03 unsigned long value;
04
05 asm volatile("mov %%cr0,%0":"=r"
06 (value));
07 if( value & 0x00010000 ) {
08 value &= ~0x00010000;
09 asm volatile("mov %0,%%cr0"::"r"
10 (value));
11 }
12 }
Listing 1 zeigt die zugehörige Sequenz für eine x86-Architektur, die das 16. Bit des Control-Registers »CR0« löscht. Über dieses Bit lässt sich der Schreibschutz generell aktivieren beziehungsweise deaktivieren. Die schlechte Nachricht: Dieser Angriff lässt sich nicht unterbinden.
Adressenliste
Um die Adresse der »sys_call_table« zu erhalten, muss ein Eindringling dagegen schon mehr Geschick aufbringen. Mit etwas Glück findet er, zum Beispiel unter Ubuntu, eine Liste aller Kerneladressen auf dem Rechner. Die Distribution legt diese Liste beim Kompilieren des Kernels an und speichert sie unter »/boot/System.map-Kernelversion« .
Doch selbst wenn diese Liste fehlt, muss der Angreifer noch nicht aufgeben. Yao Wang beispielsweise beschreibt einen Trick für die x86-Plattform, um die Adresse der »sys_call_table« im Kernel selbst zu finden [3]. Hierzu muss man nur dem Codepfad folgen, den Linux beim Aufruf eines Systemcalls abarbeitet, bekanntermaßen dient ein Software-Interrupt zum Aufrufen des Systemcalls.
Die Adresse der aufgerufenen Funktion »system_call()« befindet sich in der Interrupt-Deskriptor-Tabelle (IDT). Die Adresse der IDT wiederum ist im IDT-Register der CPU gespeichert und lässt sich daher einfach auslesen. Die Funktion »system_call()« greift auf die »sys_call_table« zu und das – wie leicht im Quellcode in der Datei »arch/x86/kernel/entry_32.S« nachzulesen ist – über den Maschinenbefehl »call« . Dieser Assembler-Befehl ergibt einen leicht zu findenden Fingerprint aus 3 Bytes, dem die gesuchte, aus 4 Bytes bestehende Adresse der »sys_call_table« folgt.
Abbildung 2 fasst das Vorgehen zusammen: Der Angreifer liest zuerst das IDTR aus (1) und hat damit die Adresse der IDT. Der Eintrag an Index »0x80« (2) innerhalb der Tabelle zeigt auf die Funktion »system_call()« . Ab der Startadresse dieser Funktion (3) muss er nur wenige Bytes im Speicher nach dem Fingerprint (4) mit dem Aufruf des ausgewählten Systemcalls suchen, um die Adresse der »sys_call_table« (5) zu finden.
Im Datensegment
Daneben gibt es weitere Möglichkeiten, um an die Adresse der »sys_call_table« zu gelangen. Das zeigen diverse Kernel-Rootkits, die im Internet zu finden sind und meist im Rahmen von Machbarkeitsstudien entstanden. Das Rootkit Intoxonia beispielsweise sucht im Datensegment nach der Adresse des Systemcalls »sys_close« [4]. Diese Adresse wird vom Kernel exportiert und ist damit einem ladbaren Modul bekannt. Wer die Adresse im Datensegment findet, kann davon ausgehend den Beginn der »sys_call_table« berechnen.
Auf einer x86-Plattform droht zudem das Einnisten des Rootkits mit Hilfe der Debugregister. Die CPU stellt insgesamt vier Debugregister bereit, in die sich je eine Adresse ablegen lässt. Sobald eine Codesequenz auf eine der abgelegten Adressen zugreift, wird ein Interrupt ausgelöst und der Linux-Kernel springt über die IDT in die Funktion »do_debug()« .
Belegt der Angreifer ein Debugregister mit der Adresse der Funktion »system_call« und tauscht gleichzeitig die Adresse von »do_debug()« gegen eine Adresse des Rootkits, etwa »evil_do_debug()« aus, aktiviert jeder Aufruf eines Systemcalls das Rootkit. Der Angriffsvektor über die Debugregister funktioniert natürlich nur auf x86. Die zuvor besprochenen Angriffe sind jedoch in abgeänderter Form auch auf anderen Linux-Plattformen, beispielsweise auf Android, anwendbar [5].
Darüber hinaus haben Securityforscher weitere Methoden zusammen mit einem Proof-of-Concept-Rootkit veröffentlicht, die das Einnisten ermöglichen. Diese Rootkits sind weniger dazu entstanden, Geheimdienstlern mit funktionierendem Code die Arbeit zu erleichtern, als vielmehr prophylaktisch Abwehrmaßnahmen zu entwickeln.
Die gute Nachricht
Für diese Abwehr von Rootkits gibt es grundsätzlich zwei Ansätze. Der erste verhindert die Installation eines Kernel-Rootkits, der zweite Ansatz ergreift Maßnahmen, die das Einnisten im Kernel, beispielsweise das Hijacken von Systemcalls, unterbinden oder zumindest erkennen (siehe Abbildung 3). Um die Installation eines Kernel-Rootkits zu verhindern, muss der Admin einen eigenen Linux-Kernel konfigurieren und übersetzen. Prominente Einfallstore für Rootkits sind »/dev/kmem« und »/dev/mem« , deren Treiber er aus der Kernelkonfiguration entfernt. Gerne nutzen böse Buben auch ladbare Kernelmodule (LKM), sodass der Admin den Support hierfür ebenfalls deraktivieren muss. Damit ist es notwendig, benötigte Treiber fest in den Kernel einzukompilieren.
Des Weiteren sollte er die Mechanismen »ksplice« und »kexec« deaktivieren. »ksplice« ermöglicht ein Kernelupdate ohne Reboot und »kexec« gestattet einen Reboot unter Auslassung des Bios. Solange der Angreifer keinen eigenen Kernel installieren kann und kein schwerwiegender Kernelbug vorhanden ist, bleibt er nach diesen Maßnahmen selbst mit Rootrechten draußen.
Für den zweiten Ansatz, der die zentralen Aktivitäten des Rootkits unterbindet, haben die Forscher unterschiedliche Werkzeuge in ihren Laboratorien entwickelt. Diese arbeiten wie ein Intrusion Detection System (IDS) und speichern direkt nach der Installation Hashsummen wesentlicher Systemtabellen, beispielsweise der »IDT« oder der »sys_call_table« ab. In regelmäßigen Abständen bildet ein Tool diese Prüfsummen neu. Ergeben sich Änderungen zu den anfangs erfassten Vergleichswerten, hat ein Rootkit zugeschlagen.
Eine weitere Methode überwacht Schreibzugriffe auf die »sys_call_table« , etwa mit Hilfe der erwähnten Debugregister. Sobald ein Schreibversuch stattfindet, schlägt der Alarm an. In vielen Fällen setzen die Wissenschaftler auch Hypervisor-, also Virtualisierungstechniken ein und können damit unabhängig vom Betriebssystem bösartige Software ausfindig machen.
Allerdings haben all diese Werkzeuge einen kleinen Nachteil: Sie sind im akademischen Umfeld entstanden. Das heißt, sie haben für eine Veröffentlichung hergehalten und sind dann wieder in den Schubladen verschwunden – kein Quellcode, keine Pflege, kein Paket. So scheint für die Erkennung und Abwehr von Kernel-Rootkits zurzeit nur das in [6] vorgestellte IDS »samhain« einsatzbereit zu sein. Für Userland-Rootkits gibt es immerhin etwas mehr Auswahl (siehe Kasten “Userland-Rootkits”). (mhu)
Infos
- Ronald Pofalla, “Antwort auf die Kleine Anfrage der Fraktion Die Linke”: http://www.andrej-hunko.de/start/download/doc_download/225-strategische-fernmeldeauf-klaerung-durch-geheimdienste-des-bundes
- Wono Kaerun, “Advanced Techniques Ring0/Kernel Levels Rootkit Development on Linux 2.6”: http://www.slideshare.net/idsecconf/linux-kernelrootkitdev-wonokaerun
- Wang Yao, “Rootkit on Linux x86 v2.6”: http://www.slideshare.net/fisher.w.y/rootkit-on-linux-x86-v26
- Sebastian Vogl, “A Bottom-up Approach to VMI-based Kernel-level Rootkit Detection”: http://www.sec.in.tum.de/assets/studentwork/finished/Vogl2010.pdf
- Dong-hoon You, “Android Platform Based Linux Kernel Rootkit”, Phrack Issue 68: http://www.phrack.org/issues.html?issue=68&id=6
- Tim Schürmann, “Intrusion Detection mit Samhain”: Admin-Magazin 01/2010, S. 88 http://www.admin-magazin.de/Das-Heft/2010/01/Intrusion-Detection-mit-Samhain








