• Keine Ergebnisse gefunden

Klassifizierung von Kernel schwachstellen

2.3 Schwachstellen durch beschädigten Arbeitsspeicher

Kernel versucht, von dem beschädigten Zeiger zu lesen oder einen Wert an der Speicher-adresse abzulegen, auf die der Zeiger verweist (wie im vorstehenden Beispiel), tritt ein willkürlicher Lesevorgang bzw. willkürlicher Schreibvorgang auf. Hat der Angreifer die volle oder wenigstens teilweise Kontrolle über die Adresse, auf die der Zeiger verweist, haben wir es mit einem kontrollierten oder teilweise kontrollierten Lese- bzw. Schreibvorgäng zu tun, anderenfalls mit einem unkontrollierten. Beachten Sie, dass ein Angreifer jedoch auch bei einem unkontrollierten Lese- oder Schreibvorgang die Quelle bzw. das Ziel bis zu ei-nem gewissen Grad vorhersehen und die Situation dadurch zuverlässig ausnutzen kann.

2.3 Schwachstellen durch beschädigten Arbeitsspeicher

Die nächste große Klasse von Bugs, die wir untersuchen, deckt all die Fälle ab, in denen Code Inhalte des Kernels überschreibt und somit den Kernelspeicher beschädigt. Es gibt zwei grundlegende Arten von Kernelspeicher, nämlich den Kernelstack für die Threads oder Prozesse, die gerade auf Kernelebene ausgeführt werden, und den Kernelheap, der immer dann verwendet wird, wenn ein Kernelpfad ein kleines Objekt oder temporären Arbeitsspeicher zuweisen muss.

Bei den Zeigerproblemen (und wie im weiteren Verlauf dieses Kapitels) stellen wir die Einzelheiten zum Ausnutzen dieser Schwachstellen bis Kapitel 3 (für allgemeine Vorge-hensweisen) und Teil II dieses Buchs zurück.

2.3.1 Schwachstellen des Kernelstacks

Die erste Form von Arbeitsspeicher, die wir uns ansehen, ist der Kernelstack. Jeder User-land-Prozess, der auf einem System läuft, verfügt über mindestens zwei Stacks, nämlich einen Userland- und einen Kernelstack. Der Kernelstack kommt immer dann ins Spiel, wenn ein Prozess einen Dienst vom Kernel anfordert (Trapping), etwa als Folge eines Systemaufrufs.

In seiner allgemeinen Funktionsweise unterscheidet sich der Kernel- nicht von einem typischen Userlandstack. Der Kernelstack setzt auch dieselben Architekturregeln durch, also die Richtung des Wachstums (entweder abwärts, also von höheren zu niedrigeren Adressen, oder umgekehrt), das Register, das die oberste Adresse festhält (gewöhnlich als Stackzeiger bezeichnet) und die Wechselwirkung von Prozeduren mit dem Stack (also wie lokale Variablen gespeichert und Parameter übergeben werden, wie verschachtelte Aufrufe miteinander verknüpft sind usw.).

Kernel- und Userlandstacks funktionieren zwar auf die gleiche Weise, doch gibt es kleine Unterschiede zwischen ihnen, die Sie kennen sollten. Beispielsweise ist die Größe des Ker-nelstacks gewöhnlich begrenzt (4 KB und 8 KB sind übliche Größen bei x86-Architekturen), weshalb für die Kernelprogrammierung der Grundsatz gilt, so wenige lokale Variablen wie möglich zu verwenden. Des Weiteren gehören die Kernelstacks sämtlicher Prozesse alle zum

selben virtuellen Adressraum (dem Kerneladressraum), wobei sie jeweils bei verschiedenen virtuellen Adressen beginnen und verschiedene Adressen überspannen.

Hinweis

Manche Betriebssysteme, darunter Linux, verwenden sogenannte Interruptstacks.

Jeder dieser Stacks ist jeweils für eine CPU zuständig und wird verwendet, wenn der Kernel mit einem Interrupt umgehen muss (im Fall des Linux-Kernels handelt es sich dabei um externe, von der Hardware generierte Interrupts). Interruptstacks dienen dazu, die Belastung des Kernelstacks zu verringern, da Letztere nur eine geringe Größe aufweisen (4 KB bei Linux).

Wie Sie anhand dieser Einführung erkennen können, unterscheiden sich Kernelstack-schwachstellen nicht so stark von ihren Userlandgegenstücken. Gewöhnlich sind sie die Folge von Schreibvorgängen jenseits der Grenzen eines auf dem Stack zugewiesenen Puf-fers. Eine solche Situation kann durch folgende Vorgänge hervorgerufen werden:

• Verwendung von unsicheren C-Funktionen wie strcpy() oder sprintf(), die in ihren Zielpuffer schreiben, bis sie das Abschlusszeichen \0 im Quellstring finden, und dabei die Größe des Puffers nicht beachten.

• Fehlerhafte Beendigungsbedingung in einer Schleife, die ein Array füllt. Betrachten Sie dazu das folgende Beispiel:

#define ARRAY_SIZE 10 void func() {

int array[ARRAY_SIZE];

for (j = 0; j <= ARRAY_SIZE; j++) { array[j] = some_value;

[...]

} }

Die Arrayelemente laufen von 0 bis ARRAY_SIZE. Wenn wir some_value mit j == 10 in array[j] schreiben, gehen wir über die Grenzen des Puffers hinaus und überschreiben möglicherweise sensiblen Arbeitsspeicher (z. B. eine Zeigervariable, die unmittelbar hinter dem Array gespeichert ist).

• Verwendung sicherer C-Funktionen wie strncpy(), memcpy() oder snprintf() mit fehlerhafter Berechnung der Zielpuffergröße. Das ist gewöhnlich die Folge von Bugs, die Integeroperationen beeinträchtigen, was allgemein als Integerüberläufe bezeichnet wird. Diese Klasse von Fehlern beschreiben wir ausführlicher in dem Abschnitt »Inte-gerprobleme« weiter hinten in diesem Kapitel.

Da der Stack eine entscheidende Rolle für die binäre Schnittstelle einer Anwendung mit einer bestimmten Architektur spielt, kann die Ausnutzung von Kernelschwachstellen sehr stark architekturabhängig sein, wie wir in Kapitel 3 noch sehen werden.

51 2.3 Schwachstellen durch beschädigten Arbeitsspeicher

2.3.2 Schwachstellen des Kernelheaps

In Kapitel 1 haben wir gesehen, dass der Kernel die Abstraktion des virtuellen Arbeitsspei-chers implementiert und dadurch die Illusion eines riesigen und unabhängigen virtuellen Adressraums für alle Userlandprozese (und für sich selbst) erzeugt. Die Grundeinheiten des Arbeitsspeichers, die der Kernel verwaltet, sind die physischen Seitenframes, die unter-schiedliche Größen aufweisen können, aber nie kleiner als 4 KB sind. Gleichzeitig muss der Kernel aber auch kontinuierlich Arbeitsspeicher für eine breite Palette an kleinen Objekten und temporären Puffern zuweisen. Es wäre nicht nur äußerst ineffizient, für diese Zwecke physische Seitenframes zuzuweisen, sondern würde auch zu einer starken Fragmentierung und einer großen Platzverschwendung führen. Außerdem haben diese Objekte gewöhnlich nur eine kurze Lebensdauer, was den Seitenframeallokator (und die Einrichtung für die Auslagerung bei Bedarf) erheblich belasten würde. All dies würde die Gesamtleistung des Systems stark beeinträchtigen.

Um dieses Problem zu lösen, verwenden die meisten modernen Betriebssysteme einen eigenen Speicherallokator auf Kernelebene, der mit dem Allokator für physische Seiten kommuniziert und für die schnelle und kontinuierliche Zuweisung und Freigabe klei-ner Objekte optimiert ist. Die verschiedenen Betriebssysteme verfügen jeweils über ihre eigene Variante für diese Art von Allokator. Diese unterschiedlichen Implementierungen sehen wir uns in Teil II dieses Buchs genauer an. Vorläufig ist es nur wichtig, die allgemei-nen Prinzipien zu kenallgemei-nen, die hinter dieser Art von Objektallokator stehen, sodass wir verstehen können, welchen Arten von Schwachstellen er unterliegen kann.

Wie wir bereits festgestellt haben, ist dieser Allokator ein Verbraucher des Allokators für physische Seiten. Er fragt nach Seiten und gibt sie letzten Endes zurück. Jede Seite wird dann in eine Reihe von Abschnitten fester Größe aufgeteilt (die nach dem von Jeff Bonwick für Sun OS 5.4 entworfenen Slab Allocator5 gewöhnlich als Slabs bezeichnet werden). Seiten, die Objekte derselben Größe enthalten, werden gruppiert. Eine solche Gruppe von Seiten wird Cache genannt.

Objekte können zwar praktisch jede Größe aufweisen, doch im Allgemeinen werden aus Gründen der Effizienz Größen in Zweierpotenzen bevorzugt. Wenn ein Kernelteilsystem nach einem Objekt fragt, gibt der Allokator einen Zeiger auf einen der Seitenabschnitte zurück. Außerdem muss sich der Allokator darüber auf dem Laufenden halten, welche Objekte frei sind (um nachfolgende Zuweisungen/Freigaben korrekt auszuführen). Diese Informationen kann er als Metadaten auf der Seite selbst festhalten, aber auch in einer externen Datenstruktur (z. B. einer verknüpften Liste). Auch hier wird der Objektspei-cher aus Leistungsgründen gewöhnlich nicht zur Freigabe- oder Zuweisungszeit gelöscht.

Stattdessen werden bestimmte Funktionen bereitgestellt, die den Objektspeicher zu die-sen Zeiten leeren. Ebenso wie es toten Arbeitsspeicher gibt, können wir auch von totem Heap sprechen.

Die Größe kann das einzige Entscheidungskriterium für die Erstellung mehrerer Caches sein, es ist jedoch auch möglich, objektspezifische Caches anzulegen. In letzterem Fall

5 Bonwick J, 1994. »The slab allocator: an object-caching kernel memory allocator«, www.usenix.org/

publications/library/proceedings/bos94/full_papers/bonwick.a.

erhalten häufig genutzte Objekte einen besonderen Cache, während größengestützte All-zweckcaches für alle anderen Zuweisungen zur Verfügung stehen (z. B. für temporäre Puffer). Häufig genutzte Objekte sind beispielsweise Strukturen mit Informationen über einzelne Verzeichniseinträge im Dateisystem oder die eingerichteten Socketverbindun-gen. Bei der Suche nach einer Datei im Dateisystem werden sehr schnell viele Verzeich-niseintragsobjekte verbraucht, und bei einer umfangreichen Website sind oft Tausende von Verbindungen geöffnet.

Wenn solche Objekte einen eigenen Cache erhalten, spiegelt die Größe der Slabs ge-wöhnlich die Größe des Objekts wider, weshalb zur optimalen Platzausnutzung auch Größen verwendet werden, die keine Zweierpotenzen sind. In diesem Fall (sowie bei im Cache gespeicherten Metadaten) kann es vorkommen, dass der für die Slabs verfügbare freie Speicherplatz nicht durch die Slabgröße teilbar ist. Der »übrige« Platz wird in man-chen Implementierungen dazu genutzt, den Cache zu färben, sodass die Objekte auf den einzelnen Seiten an verschiedenen Offsets beginnen und daher in verschiedenen Hard-warecachezeilen enden (was wiederum die Gesamtleistung verbessert).

Schwachstellen, die den Kernelheap beeinträchtigen, sind gewöhnlich eine Folge von Puf-ferüberläufen mit den gleichen Auslösern, wie wir sie schon im Abschnitt »Schwachstellen des Kernelstacks« kennengelernt haben (Verwendung unsicherer Funktionen, fehlerhaft beendete Schleifen, falsche Verwendung sicherer Funktionen usw.). Bei einem solchen Überlauf werden gewöhnlich die Inhalte des Slabs überschrieben, der auf den übergelau-fenen Abschnitt folgt, oder die Metadaten zu dem betreffenden Cache (falls vorhanden) oder zufällige andere Inhalte des Kernelspeichers (wenn der Überlauf umfangreich genug ist, um über die Seitengrenze des Slabs hinauszugehen, oder wenn sich der Slab am Ende der Cacheseite befindet).

Tipp

Fast alle Objektallokatoren in den von uns untersuchten Betriebssystemen bieten eine Möglichkeit, diese Art von Überlauf zu erkennen. Dazu wird die Technik des sogenannten Redzonings verwendet, bei der am Ende jedes Slabs ein willkürlicher Wert platziert und bei der Freigabe des Objekts überprüft wird, ob dieser Wert überschrieben wurde. Mit ähnlichen Techniken können auch Zugriffe auf nicht initisalisierten oder freigegebenen Arbeitsspeicher erkannt werden. All diese De-buggingmöglichkeiten beeinträchtigen jedoch die Leistung des Betriebssystems und sind daher standardmäßig ausgeschaltet. Eingeschaltet werden sie gewöhn-lich entweder zur Laufzeit (mithilfe eines Boot-Flags oder durch Ändern eines Werts über den Kerneldebugger) oder zur Kompilierungszeit (über die Kompilie-rungsoptionen). Wir können diese Möglichkeiten nutzen, um zu sehen, wie sich unser Heap-Exploit verhält (überschreibt er den Slab?), wir können sie aber auch zusammen mit Fuzzing einsetzen, um ein besseres Verständnis über die Bugs zu gewinnen, die wir gefunden haben.

53 2.4 Integerprobleme

2.4 Integerprobleme

Integerprobleme beeinträchtigen die Verarbeitung und Verwendung von Integerzahlen.

Die beiden am häufigsten auftretenden Arten solcher Bugs sind (arithmetische) Integer-überläufe und Vorzeichenprobleme.

Bei unserer Besprechung der Datenmodelle haben wir schon erwähnt, dass Integer eben-so wie andere Variablen eine bestimmte Größe haben, die den Bereich der durch sie aus-gedrückten und in ihnen gespeicherten Werte festlegt. Integer können ein Vorzeichen haben, also sowohl positive als auch negative Werte annehmen, aber auch vorzeichenlos sein, sodass sie nur positive Zahlen darstellen.

Wenn n die Größe eines Integers in Bit angibt, kann er bis zu 2n Werte darstellen. Ein Integer ohne Vorzeichen kann alle Werte von 0 bis 2n -1 speichern, während ein Integer mit Vorzei-chen nach dem Prinzip des Zweierkomplements den Bereich von -(2n -1) bis (2n -1) abdeckt.

Bevor wir uns die verschiedenen Probleme, die bei Integern auftreten können, genauer an-sehen, müssen wir jedoch noch eines betonen: Diese Arten von Schwachstellen lassen sich gewöhnlich nicht an sich ausnutzen, doch können sie zu anderen Schwachstellen führen, meistens zu Speicherüberläufen. In praktisch allen modernen Kerneln sind Integerproble-me entdeckt worden, was sie zu einer sehr interessanten (und lohnenden) Klasse von Bugs macht.