• Keine Ergebnisse gefunden

Der Weg zum erfolgreichen Kernel-Hacking

3.3 Der Ausführungsschritt

3.3.3 Den Kernelzustand wiederherstellen

Es ist spannend, sich auf einem Computer volle Rechte zu verschaffen, aber sie aufgrund einer Kernelpanik nur eine Sekunde später wieder zu verlieren, macht wirklich keinen Spaß. Mit der Wiederherstellungsphase versuchen Sie, den Computer am Laufen zu erhal-ten, sodass Sie die neu hinzugewonnenen Rechte nutzen können. In dieser Phase müssen Sie sich um die beiden folgenden Probleme kümmern:

• Der Exploit kann sensible Kernelstrukturen gestört haben. Im Allgemeinen hat er Kernelarbeitsspeicher beschädigt, auf den andere Kernelpfade zugreifen müssen.

• Der übernommene Kernelpfad kann Sperren erworben haben, die freigegeben werden müssen.

Das erste Problem tritt hauptsächlich bei Bugs mit Speicherbeschädigungen auf, bei deren Ausnutzung Sie leider nicht sehr gezielt vorgehen können. Alles zwischen dem überlau-fenden Puffer und Ihrem Ziel wird überschrieben, und in vielen Fällen können Sie nicht einmal den Umfang des Überlaufs genau steuern, um ihn unmittelbar hinter Ihrem Ziel aufhören zu lassen. In einem solchen Fall müssen Sie zwei verschiedene Arten von Struk-turen wiederherstellen, nämlich Stackframes und Heap-SteuerstrukStruk-turen.

95 3.3 Der Ausführungsschritt

Hinweis

In den meisten Architekturen und ABIs sind an der Prozedurverkettung und an Softwaretraps in starkem Maße Stackframes beteiligt. Wir versuchen zwar, die folgende Erörterung so allgemein wie möglich zu halten, doch um Einzelheiten zur Stackwiederherstellung geben zu können, müssen wir uns auf eine bestimmte Architektur festlegen. Die praktischen Hinweise in diesem Teilabschnitt gelten daher für die x86-64-Architektur.

Bei einem Stacküberlauf ist es zwar möglich, aber nicht unbedingt sicher, dass sie wie-der zu einem stabilen Zustand zurückkehren. Beispielsweise können Sie den Shellcode so anpassen, dass er zu einem verschachtelten Aufrufer im angegriffenen Pfad zurückkehrt und die Ausführung von dort an fortsetzt. Es kann jedoch auch sein, dass Sie den Stack zu stark beschädigt haben, sodass Sie die Funktionskette abbrechen und ins Userland zurückspringen müssen. Wie Sie bereits wissen, erreichen Userlandprozesse den Kernel über Softwaretraps bzw. Interrupts. Sobald der Kernel die Ausführung des angeforderten Dienstes abgeschlossen hat, muss er die Steuerung an den Prozess zurückgeben und sei-nen Zustand wiederherstellen, sodass er mit der nächsten Anweisung hinter der Trap wei-termachen kann. Die übliche Vorgehensweise, um von einem Interrupt zurückzukehren, besteht in der Verwendung der Anweisung IRETQ (bzw. IRET auf x86). Damit können Sie von vielen verschiedenen Situationen aus zurückkehren, aber wir sind hier an dem interessiert, was in den Intel-Handbüchern als rechteüberschreitender Rücksprung (»inter-privilege return«) bezeichnet wird, da wir vom Kernelland (der höchsten Rechteebene) zum Benutzerland (der niedrigsten) wechseln.

Die erste Operation, die die Anweisung IRETQ ausführt, besteht darin, eine Reihe von Werten vom Stack zu nehmen. Im Pseudocode der Intel-Handbücher sieht das wie folgt aus:

tempRIP ← Pop();

tempCS ← Pop();

tempEFLAGS ← Pop();

tempRSP ← Pop();

tempSS ← Pop();

Hier werden RIP (der 64-Bit-Programmzeiger), CS (der Codesegmentselektor), EFLAGS (das Register mit verschiedenen Statusinformationen) RSP (der 64-Bit-Stackzeiger) und SS (der Stacksegmentselektor) vom Stack in temporäre Werte kopiert. Die im Segment-selektor CS enthaltene Rechteebene wird mit der aktuellen Rechteebene verglichen, um zu entscheiden, welche Überprüfungen an den verschiedenen temporären Werten erforder-lich sind und wie EFLAGS wiederhergestellt werden muss. Kenntnisse der Überprüfungen

sind wichtig um zu verstehen, welche Werte die Architektur auf dem Stack erwartet. In unserem Fall enthält CS eine niedrigere Rechteebene (Rücksprung zum Userland), wes-halb die Register auf dem Stack folgende Inhalte haben müssen:

CS, SS Das im Userland verwendete Code- bzw. Stacksegment. Jeder Kernel definiert diese Werte statisch.

RIP Ein Zeiger auf einen gültigen ausführbaren Bereich im Kernelland. Am besten ist es, ihn auf eine Funktion innerhalb unseres Userland-Exploits zu setzen.

EFLAGS Dies kann ein beliebiger Userlandwert sein. Wir können dazu einfach den Wert verwenden, den das Register hatte, als wir mit der Ausführung des Exploits begonnen haben.

RSP Ein Zeiger auf einen gültigen Stack. Dabei kann es sich um jeden Speicherbe-reich handeln, der groß genug ist, um die Routine, auf die RIP zeigt, gefahrlos bis zur Ausführung der lokalen Shell mit hohen Rechten auszuführen.

Wenn wir die Werte für diese Register korrekt vorbereiten, in der von IRETQ erwarteten Reihenfolge in den Speicher kopieren und den Kernelstackzeiger auf den zuvor genannten Speicherbereich verweisen lassen, können wir IRETQ einfach ausführen und kommen damit gefahrlos aus dem Kernelland heraus. Da die Inhalte des Stacks beim Eintritt ins Kernelland verworfen werden (der Stackzeiger wird praktisch auf einen festen Offset vom Anfang der für den Stack zugewiesenen Seite zurückgesetzt, und alle Inhalte werden als tot betrachtet), reicht das aus, um das System in einem stabilen Zustand zu belassen. Falls Kernel- und Userland den Selektor GS nutzen (wie es heutzutage der Fall ist), muss vor IRETQ die Anweisung SWAPGS ausgeführt werden, die lediglich den Inhalt des Registers GS und einen Wert in einem der maschinenspezifischen Register (MSRs) austauscht. Der Kernel erledigt das beim Eintritt, und wir müssen das auf dem Weg nach draußen tun.

Insgesamt sieht die Stackwiederherstellungsphase unseres Shellcodes damit wie folgt aus:

push $SS_USER_VALUE push $USERLAND_STACK push $USERLAND_EFLAGS push $CS_USER_VALUE

push $USERLAND_FUNCTION_ADDRESS swapgs

iretq

Da die Wiederherstellung von Heapstrukturen von der Implementierung des Betriebssys-tems und nicht von der zugrunde liegenden Architektur abhängt, werden wir uns in den Kapiteln 4 bis 6 darum kümmern. Hier wollen wir nur festhalten, dass das Überschreiben von zugewiesenen Heapobjekten keine umfangreiche Wiederherstellung erfordert, sofern nicht irgendeine Form von Heapdebugging vorhanden ist. (Gewöhnlich müssen einfach nur genügend gültige Kernelwerte emuliert werden, damit der Kernelpfad, der sie nutzt, den Punkt erreicht, an dem das Objekt freigegeben wird.) Das Überschreiben von freien

97 3.3 Der Ausführungsschritt

Objekten dagegen kann etwas mehr Behandlung erfordern, da manche Kernelheapalloka-toren Verwaltungsdaten darin speichern (z. B. das nächste freie Objekt). Es ist eine große Hilfe, wenn Sie in der Lage sind, den Heap in einen vorhersagbaren Zustand zu versetzen.

Die Theorie dahinter sehen wir uns im folgenden Abschnitt über den Auslöseschritt an.

Bis jetzt haben wir uns auf die Wiederherstellung nach den Problemen konzentriert, die wir durch das Auslösen der Schwachstelle hervorgerufen haben. Allerdings haben wir uns nicht darum gekümmert, was der Kernelpfad vor Erreichen der Schwachstelle getan hat und was er daraufhin getan hätte, wenn wir den Ausführungsfluss nicht gekapert hätten.

Insbesondere müssen wir sorgfältig alle Ressourcensperren freigeben, die der Pfad erwor-ben hat. Bei Schwachstellen, die Ausführungsblöcke hinzufügen, ist das kein Problem. So-bald wir den Shellcode ausgeführt haben, kehren wir genau zu dem Punkt zurück, an dem wir den Kernelpfad übernommen haben, sodass er einfach seine Ausführung vollendet und dabei alle Ressourcen freigibt, die er gesperrt haben mag.

Zerstörerische Maßnahmen wie Stacküberläufe mit der zuvor beschriebenen IRETQ-Technik dagegen kehren niemals zum ursprünglichen Kernelpfad zurück, weshalb wir uns im Shellcode während der Wiederstellung um die Sperren kümmern müssen.

Betriebs systeme implementieren eine Vielzahl verschiedener Sperrmechanismen: Spin-locks, Semaphoren, bedingte Variablen und Mutexe mit verschiedenen Spielarten von mehreren oder einzelnen Lese- und Schreibfunktionen, um nur einige zu nennen. Diese Vielfalt ist nicht überraschend, denn Sperren sind entscheidend für die Leistung, insbe-sondere wenn viele Prozesse oder Teilsysteme um eine Ressource konkurrieren. Wir kön-nen Sperren grundlegend in zwei Kategorien aufteilen: Wartesperren und blockierende Sperren. Bei Wartesperren umkreist der Kernelpfad die Sperre, arbeitet CPU-Zyklen ab und führt eine enge Schleife aus, bis die Sperre freigegeben ist. Trifft ein Kernelpfad da-gegen auf eine blockierende Sperre, legt er sich schlafen und erzwingt eine Neuplanung der CPU. Er konkurriert dann nicht mehr um die Ressource, bis der Kernel feststellt, dass sie wieder verfügbar ist, und den Task aufweckt.

Wenn Sie einen Exploit schreiben, der den Ausführungsfluss stört, müssen Sie als Erstes herausfinden, wie viele kritische Sperren der Kernelpfad erwirbt, und sie alle ordnungsge-mäß freigeben. Kritische Sperren sind diejenigen, auf die sich das System verlässt (in allen Betriebssystemen gibt es nur jeweils eine Handvoll davon, und dabei handelt es sich im Allgemeinen um Spinlocks), sowie diejenigen, die zu einem Deadlock für eine nach dem Exploit benötigte Ressource führen würden. Manche Kernelpfade überprüfen auch eini-ge Sperren, weshalb Sie sehr vorsichtig sein müssen, dass dabei keine Trap oder gar eine Panikbedingung auftritt. Alle kritischen Sperren müssen sofort wiederhergestellt werden.

Nicht kritische Sperren dagegen können auch in einem späteren Stadium repariert wer-den (z. B. das Lawer-den eines externen Moduls). Sie können sie auch einfach vergessen, wenn die einzige Auswirkung darin besteht, dass ein Userlandprozess abgebrochen wird (es ist genauso einfach, die Berechtigungsnachweise des Elternprozesses anzuheben wie die des aktuellen Prozesses) oder dass eine nicht kritische Ressource nicht mehr benutzt werden kann.