Als Multiusersystem ist Unix aus Tradition der Sicherheit verpflichtet. Zutrittsschutz mit Benutzername und Passwort, die Unterscheidung zwischen Superuser und übrigen Benutzern sowie Dateizugriffsrechte, die lesenden, schreibenden und ausführenden Zugriff für den Besitzer, eine Gruppe und den Rest getrennt festlegen, waren Unix schon in die Wiege gelegt.
Heute reichen diese Schutzmechanismen nicht mehr aus, um aktuelle Methoden von Angreifern abzuwehren. Häufiger Angriffsvektor sind Buffer Overflows. Bei diesem Angriff nutzen die Exploits der Black Hats zumeist Programmierfehler aus, die es ihnen ermöglichen, Speicherbereiche zu überschreiben.
Tun sie dies auf dem Stack und modifizieren die dort befindliche Rücksprungadresse einer Unterfunktion, bringen sie die CPU dazu, beim »return« der Funktion eigenen Code anzuspringen. Dieser liegt meist auf dem Stack. Noch bequemer ist es, eine Bibliotheksfunktion - zum Beispiel »system()« der Libc - anzuspringen. So gewinnt der Angreifer Kontrolle über die Maschine (siehe Abbildung 1). Zwei Voraussetzungen spielen den Angreifern in die Hände: Die starre Aufteilung des Adressraums und die Möglichkeit, auf dem Stack befindlichen Code auszuführen.
Abbildung 1: Gelingt es einem Angreifer, mehr Daten in einem Buffer auf dem Stack abzulegen, als der Programmierer dafür vorgesehen hat, kann er beim Buffer Overflow die gültige Rücksprungadresse überschreiben. Beim Rücksprung führt die CPU dann mitunter bösartigen Code aus.
Angriffsmuster
Genau an diesen Stellen setzt der Linux-Kernel an, um Black Hats die Arbeit zu erschweren. Richtig konfiguriert verhindert es der Kernel mit Hardwarehilfe des Prozessors, Code auf dem Stack auszuführen. Außerdem kann er jeder Applikation ein eigenes, durch den Zufall bestimmtes Speichermapping verpassen. Diese Eigenschaften muss der Anwender jedoch auch nutzen.
Speichermapping bezeichnet die Aufteilung des virtuellen Adressraums in Code, Daten, Stack, Heap und Bibliotheken eines Programms. Jeder Applikation stehen auf einem 32-Bit-Linux typischerweise 3 GByte Hauptspeicher zur Verfügung, unabhängig davon, ob nur 64 MByte oder 4 GByte RAM im Rechner stecken. Ohnehin nutzen nur wenige Programme den virtuellen Adressraum vollkommen aus. Somit verteilt Linux die einzelnen Teile des Kernels sinnvoll über den Adressraum von 3 GByte, wie Abbildung 2 illustriert.
Abbildung 2: Im Normalfall ist die Einteilung des Adressraums auf einem 32-Bit-System der I-386-Architektur festgelegt. Heap und Stack wachsen von zwei Seiten aufeinander zu, da ihre Größe dynamisch wächst.
Ziemlich am Anfang befinden sich der Code des Programms, die vorinitialisierten (Data) und die nicht vorinitialisierten Daten (BSS), der Stack und der Heap. Der Heap ist der Speicherbereich, den etwa »malloc()« als Basis für dynamisch angeforderten Speicher verwendet. Die gemeinsam genutzten Bibliotheksfunktionen (Dynamic Shared Objects, DSO) blendet der Kernel ebenfalls in den Adressraum ein, etwa Speicherseiten, die eine Applikation per »mmap()« von anderen Prozessen angefordert hat.
Da beim Start eines Prozesses nicht bekannt ist, wie viel Arbeitsspeicher er dynamisch anfordern wird oder wie viele Speicherseiten er per »mmap()« verwenden will, wachsen die Heap- und die Mmap-Region aufeinander zu. Für den Stack, dessen Verbrauch beim Start ebenfalls nicht bekannt ist, reserviert der Kernel innerhalb des Adressraums einfach 128 MByte, falls nichts anderes konfiguriert ist.
Hackerfreundlich
Bei einer derart starren Aufteilung des virtuellen Adressraums ist es für Angreifer leicht, Rücksprungadressen auf dem Stack mit eigenen Adressen zu überschreiben. Daher bringt der Linux-Kernel - sofern gewünscht - mehr Variationen bei der Lage der Adressbereiche ein und verschiebt die Position des Stacks und der Mmap-Region, in der sich auch die gemeinsam genutzten Bibliotheken (DSO) befinden. Bei so verwürfeltem Speichermapping kommen Angreifer nur noch mit Ausprobieren weiter. Raten sie dabei falsch, stürzt die angegriffene Applikation mit hoher Wahrscheinlichkeit ab. Aufmerksamen Anwendern und Admins fällt das auf.
Abbildung 3 zeigt, wie der Kernel eine Speicherlücke vor das obere Ende des Stacks einfügt. Ein Blick in die Kernelquellen offenbart, dass Linux beim Systemaufruf »execve()« 11 Bits (»0x7ff«) der oberen Startadresse des neuen Stacksegments auf einem 32-Bit-System variiert (Listing 1). Da das Stacksegment immer auf einer Seitengrenze liegt, ist die Startadresse bei 4 KByte großen Speicherseiten (12 Bit) grundsätzlich innerhalb eines 8 MByte (223 Byte) großen Bereichs zu suchen. Mehr noch: Zusätzlich initialisiert Linux den Stackpointer per Zufall auf eine Adresse innerhalb eines 8 KByte großen Bereichs (Listing 2). Da das Betriebssystem den Stackpointer allerdings auf eine 16-Byte-Adresse ausrichtet, ergibt sich eine weitere Variation um ld(8192) - 4 = 9 Bit.
Abbildung 3: Aktiviertes ASLR erschwert es Angreifern, die Adresslage des Stacks und der Mmap-Region vorherzusagen. Vor dem oberen Ende des Stacks fügt der Kernel zufällige Leerräume ein.
01 #ifndef STACK_RND_MASK /* 8MB of VA */
02 #define STACK_RND_MASK (0x7ff >> (PAGE_SHIFT - 12))
03 #endif
04
05 static unsigned long
06 randomize_stack_top(unsigned long stack_top)
07 {
08 unsigned int random_variable = 0;
09
10 if ((current->flags & PF_RANDOMIZE) &&
11 !(current->personality & ADDR_NO_RANDOMIZE)) {
12 random_variable = get_random_int() &
13 STACK_RND_MASK;
14 random_variable <<= PAGE_SHIFT;
15 }
|
01 unsigned long arch_align_stack(unsigned long sp)
02 {
03 if (!(current->personality & ADDR_NO_RANDOMIZE)
04 && randomize_va_space)
05 sp -= get_random_int() % 8192;
06 return sp & ~0xf;
07 }
|
Summa summarum sprechen Security-Fachleute davon, dass die Entropie der Stackadresse 11 Bit + 9 Bit = 20 Bit beträgt und ein Angreifer im für ihn ungünstigsten Fall also 220 (ungefähr eine Million) Versuche braucht, um eine Schwachstelle auszunutzen.