Angriffe über Buffer Overflows haben es schwerer, wenn Address-Space Layout Randomization (ASLR) oder die Hardware-Unterstützung durch NX- und XD-Bits aktiv ist. Die Implementation der 32-Bit-Architektur bietet Linux-Hackern viele Möglichkeiten zur Absicherung.
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.
|
Listing 1: |
|---|
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 }
|
|
Listing 2: |
|---|
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.
Besser als nichts
Die Entropie der Lage der Mmap-Region ist leider nicht ganz so groß. Sie ist jedoch wichtig, um den “Return to Libc” genannten Angriff zu verhindern. Der Start dieser Region variiert nur innerhalb eines 1 MByte großen Bereichs (Listing 3). Der Kernel richtet die Region an einer Pagegrenze (typischerweise 4 KByte) aus, also ergeben sich 256 unterschiedliche Startadressen (20 – 12=8, 28=256). Theoretisch ließe sich die Entropie sowohl des Stacks als auch der Mmap-Region erhöhen, das ginge aber zu Lasten des zur Verfügung stehenden virtuellen Adressraums.
|
Listing 3: |
|---|
01 static inline unsigned long
02 mmap_base(struct mm_struct *mm)
03 {
04 unsigned long gap =
05 current->signal->rlim[RLIMIT_STACK].rlim_cur;
06 unsigned long random_factor = 0;
07
08 if (current->flags & PF_RANDOMIZE)
09 random_factor = get_random_int() %
10 (1024*1024);
11
12 if (gap < MIN_GAP)
13 gap = MIN_GAP;
14 else if (gap > MAX_GAP)
15 gap = MAX_GAP;
16
17 returnPAGE_ALIGN(TASK_SIZE-gap-random_factor);
18 }
|
Eine besondere Rolle nimmt die VDSO-Page (Virtual Dynamic Shared Object) ein. Diese Seite ist für den schnellen Aufruf von Systemcalls zuständig [1]. Der Kernel blendete sie früher ans Ende des virtuellen Adressraums bei »0xffffe000« ein (siehe Abbildung 4). Zwischenzeitlich wurde diese Seite aber in die Mmap-Region verschoben, wodurch sie jetzt typischerweise eine zufällige Adresse erhält. Applikationen kommen mit einer zufälligen VDSO-Page nur zurecht, wenn eine aktuelle Glibc (Version 2.3.3 und neuer) installiert ist.

Abbildung 4: Im Kompatibilitätsmodus ist die VDSO-Page auf einem 32-Bit-Linux fest an die Adresse »0xffffe000« gebunden. Wird er deaktiviert, bekommt sie einen zufällen Platz in der Mmap-Region.
Daher hat sich Ubuntu für eine konservative Konfiguration entschieden. Mit
if grep vdso /proc/self/maps; then
echo "feste vDSO-Page"
fi
findet der Admin durch eine Abfrage im »/proc«-Filesystem heraus, ob sich die VDSO-Page an der kompatiblen, festen Adresse oder in der Mmap-Region befindet. Mit der ersten Variante kommen auch ältere Programme zurecht.
Auf aktuellen Kerneln kann der Administrator beim Booten den Parameter »vdso=1« übergeben und damit die VDSO-Page in die variable Mmap-Region verschieben. Alternativ kann er das auch per Kernelkonfiguration erreichen, wenn er die Option »COMPAT_VDSO« deaktiviert (Abbildung 6).
Heap bleibt Heap
Gänzlich außen vor bleibt bei einem Standard-Linux der Heap-Bereich. Wer auch dessen Adresslage per Zufall variieren möchte, kommt um das Patchen des Kernels nicht herum. Das Pax-Team bietet dazu Vorschläge an [3]. Address-Space Layout Randomization (ASLR) – so der offizielle Name der vorgestellten Technik – lässt sich beim Starten des Kernels oder auch während des Betriebs ein- oder ausschalten. Beim Booten übergibt der Anwender dazu den Parameter »norandmaps« an den Kernel. Während des Betriebs steuert der Eintrag »/proc/sys/kernel/randomize_va_space« ASLR (siehe Abbildung 5).
Der Effekt von ASLR lässt sich anschaulich visualisieren. Das im Proc-Filesystem abgelegte Speicherlayout offenbart sich (als Hoffnungsschimmer für Angreifer) mit dem Befehl »cat /proc/self/maps«. Bei aktiviertem ASLR ändert sich die Adresslage, wie das untere Beispiel von Abbildung 5 zeigt.

Abbildung 5: Bei aktiviertem ASLR liegt die VDSO-Page bei jedem Prozess an anderer Stelle im Hauptspeicher.

Abbildung 6: Mit diesem Programm lässt sich überprüfen, ob Code auf dem Stack ausführbar ist oder nicht. Erzeugt das Programm einen Segmentation-Fault, ist das ein gutes Zeichen.
NX- und XD-Bits
Ein Standard-Linux hat zum Schutz vor Buffer Overflows noch mehr zu bieten, allerdings nur, wenn es auf einem Prozessor läuft, der nicht älter als drei oder vier Jahre ist. Neuere CPUs verfügen nämlich über ein zusätzliches Bit im Seitendeskriptor. AMD spricht vom No Execute (NX), Intel vom Execute Disable (XD) Bit. Mit ihnen markiert Linux Speicherseiten (Pages), die nur Daten und keinen Code enthalten.
Jeder Versuch, so markierten Code auszuführen, führt zu einer Exception (Abbildung 6). Diese Technik verhindert es, auf dem Stack abgelegten (bösartigen) Code auszuführen (Abbildung 7). Leider stehen das NX- oder das XD-Bit nur bei aktivierter PAE (Physical Address Extension) bereit. Wie dies festzustellen ist, beschreibt der Kasten “Stack-Execution per Hardware verbieten”.
|
Stack-Execution per Hardware |
|---|
|
Neuere CPUs unterstützen das NX- (Intel) oder das XD-Bit (AMD). Ist diese Prozessorerweiterung vorhanden, kann der Standardkernel verhindern, auf dem Stack liegende Daten als Code zu interpretieren. Da es für die Bits im ursprünglichen Seitendeskriptor der x86-Architektur für 32-Bit-Systeme keinen Platz mehr gab, haben die Hersteller es an Position 63 des erweiterten Pagedeskriptors positioniert. Der Zugriff auf diesen Deskriptor ist im 32-Bit-Modus allerdings nur dann möglich, wenn die Physical Address Extension (PAE), mit der auch ein 32-Bit-System 64 GByte physikalischen Speicher adressieren kann, aktiv ist. Ob PAE unterstützt wird, zeigt ein Eintrag in »/proc/cpuinfo«: if grep pae /proc/cpuinfo; then
echo "PAE aktiviert"
fi
Ist PAE aktiv, nutzt der Kernel die Möglichkeiten der NX- oder XD-Bits aus. Overflow-Exploits haben auf diese Weise weniger Erfolg. |
Um PAE und damit das NX-Bit zu aktivieren, muss der Admin bei der Kernelkonfiguration unter »Processor Type and Features« den »High Memory Support« entweder auf »off« oder auf »64 GByte« stellen. Ist das System mit maximal 1 GByte Hauptspeicher bestückt, sollte er zunächst »off« und dann die Option »PAE« aktivieren (Abbildung 8). Ist mehr Speicher vorhanden, empfiehlt sich die Auswahl »64 GByte«. Diese Wahl impliziert automatisch PAE und damit die Unterstützung des NX-Bits.
Bei der außerdem noch möglichen Auswahl »4 GByte« – gedacht für Systeme, die zwischen 1 und 4 GByte Hauptspeicher vorzuweisen haben – unterstützt der Kernel übrigens kein PAE! Natürlich muss der Admin den so konfigurierten Kernel anschließend noch übersetzen, installieren und neu booten.

Abbildung 7: Diese Meldung bedeutet, dass Angreifer Code auf dem Stack ausführen und so etwas leichter die Kontrolle über das System gewinnen können.

Abbildung 8: Wenn »High Memory Support« auf »off« steht, muss der Admin PAE explizit anwählen, um die Stack Protection zu aktivieren.
Heile Welt
Eigentlich könnte der Kernel den Stack auf einer x86-Architektur mit 32 Bits auch ohne NX- oder XD-Bit schützen. Schließlich beinhalten die Segmentdeskriptoren seit Urzeiten vergleichbare Mechanismen. Es gibt unter dem Stichwort »SEGMEXEC« ein Patch des Pax-Teams, das diese Mechanismen nutzt [3]. Da es aber den virtuellen Adressraum reduziert, ist es bis dato nicht Teil des Standardkernels geworden.
Es bleibt die Frage, was die vorgestellten Ansätze bringen und was sie kosten. Die von Linus Torvalds gewählte Lösung zum Schutz vor Buffer Overflows bleibt Applikationen und Anwendern weitgehend verborgen. Sie zwackt vergleichsweise wenig vom virtuellen Adressraum für die Zufallslücken ab. Performance-Einbußen sind gering, da der Kernel jeden Prozess beim Ausführen der »exec«-Funktionsfamilie randomisiert. Andererseits wird es zwar für den Angreifer schwieriger, einen Programmierfehler für seine Machenschaften auszunutzen, nicht aber unmöglich.
Shacham hat zusammen mit seinen Mitstreitern in einem Paper gezeigt, dass für einen effektiven Schutz eine Entropie von 8 Bit oder auch 20 Bit nicht ausreichend ist [2]. Anders sieht es übrigens in der 64-Bit-Welt aus: Hier genügt eine Entropie von 28 Bit für die Mmap-Region und ebenfalls 28 Bit für den Stack. Daran – so die Autoren – beißen sich Angreifer lange die Zähne aus, wenn sie durch Raten den Weg ins System finden wollen. Dass unter der 64-Bit-Version von Linux der Stack grundsätzlich mit dem NX-Bit geschützt ist, sollte Grund genug für jeden Systembetreiber der entsprechenden Architektur sein, den Linux-Kernel mit 64 Bit auch zu nutzen.
Schutzschilde aktivieren
Neben dem eingeschränkten Support durch die Prozessoren ist auch der Performance-Verlust bei aktiviertem PAE ein Grund für Distributoren, das NX-Bit zunächst ungenutzt zu lassen. Letztlich entscheidet jedoch der Benutzer, wie viel Sicherheit er braucht. (mg)
|
Infos |
|---|
|
[1] Eva-Katharina Kunst, Jürgen Quade, “Kern-Technik, Folge 13, Systemaufrufe”: Linux-Magazin 08/04 [2] Shacham et. al., “On the Effectiveness of Address-Space Randomization”: Proceedings of the 11th ACM conference on Computer and communications security, 2004,[http://www.stanford.edu/~blp/papers/asrandom.pdf] [3] Patch des Pax-Teams: [http://pax.grsecurity.net/] |
|
Die Autoren |
|---|
|
Eva-Katharina Kunst, Journalistin, und Jürgen Quade, Professor an der Hochschule Niederrhein, sind seit den Anfängen von Linux Fans von Open Source. Unter dem Titel “Linux-Treiber entwickeln” haben sie zusammen ein Buch zum Kernel 2.6 veröffentlicht. |






