it-swarm.com.de

Wie funktionieren ASLR und DEP?

Wie funktionieren die Randomisierung des Adressraumlayouts (ASLR) und die Verhinderung der Datenausführung (DEP), um zu verhindern, dass Schwachstellen ausgenutzt werden? Können sie umgangen werden?

115
Polynomial

Die Adressraum-Layout-Randomisierung (ASLR) ist eine Technologie, mit der verhindert wird, dass Shellcode erfolgreich ist. Dies geschieht durch zufälliges Versetzen der Position von Modulen und bestimmten speicherinternen Strukturen. Data Execution Prevention (DEP) verhindert bestimmte Speichersektoren, z. der Stapel, von der Ausführung. In Kombination wird es äußerst schwierig, Schwachstellen in Anwendungen mithilfe von Shellcode- oder ROP-Techniken (Return-Oriented Programming) auszunutzen.

Schauen wir uns zunächst an, wie eine normale Sicherheitsanfälligkeit ausgenutzt werden kann. Wir werden alle Details überspringen, aber sagen wir einfach, wir verwenden eine Sicherheitsanfälligkeit bezüglich Stapelpufferüberlauf. Wir haben einen großen Blob von 0x41414141 - Werten in unsere Nutzdaten geladen, und eip wurde auf 0x41414141 Gesetzt, sodass wir wissen, dass es ausnutzbar ist. Wir haben dann ein geeignetes Tool (z. B. Metasploits pattern_create.rb) Verwendet, um den Versatz des in eip geladenen Werts zu ermitteln. Dies ist der Startoffset unseres Exploit-Codes. Zur Überprüfung laden wir 0x41 Vor diesem Offset, 0x42424242 Am Offset und 0x43 Nach dem Offset.

In einem Nicht-ASLR- und Nicht-DEP-Prozess ist die Stapeladresse jedes Mal dieselbe, wenn wir den Prozess ausführen. Wir wissen genau, wo es in Erinnerung ist. Schauen wir uns also an, wie der Stapel mit den oben beschriebenen Testdaten aussieht:

stack addr | value
-----------+----------
 000ff6a0  | 41414141
 000ff6a4  | 41414141
 000ff6a8  | 41414141
 000ff6aa  | 41414141
>000ff6b0  | 42424242   > esp points here
 000ff6b4  | 43434343
 000ff6b8  | 43434343

Wie wir sehen können, zeigt esp auf 000ff6b0, Das auf 0x42424242 Gesetzt wurde. Die Werte davor sind 0x41 Und die Werte danach sind 0x43, Wie wir gesagt haben. Wir wissen jetzt, dass die unter 000ff6b0 Gespeicherte Adresse gesprungen wird. Also setzen wir es auf die Adresse eines Speichers, den wir steuern können:

stack addr | value
-----------+----------
 000ff6a0  | 41414141
 000ff6a4  | 41414141
 000ff6a8  | 41414141
 000ff6aa  | 41414141
>000ff6b0  | 000ff6b4
 000ff6b4  | cccccccc
 000ff6b8  | 43434343

Wir haben den Wert auf 000ff6b0 So gesetzt, dass eip auf 000ff6b4 Gesetzt wird - den nächsten Offset im Stapel. Dadurch wird 0xcc Ausgeführt, was eine Anweisung int3 Ist. Da int3 Ein Software-Interrupt-Haltepunkt ist, wird eine Ausnahme ausgelöst und der Debugger angehalten. Auf diese Weise können wir überprüfen, ob der Exploit erfolgreich war.

> Break instruction exception - code 80000003 (first chance)
[snip]
eip=000ff6b4

Jetzt können wir den Speicher bei 000ff6b4 Durch Shellcode ersetzen, indem wir unsere Nutzdaten ändern. Damit ist unser Exploit abgeschlossen.

Um zu verhindern, dass diese Exploits erfolgreich sind, wurde Data Execution Prevention entwickelt. DEP erzwingt, dass bestimmte Strukturen, einschließlich des Stapels, als nicht ausführbar markiert werden. Dies wird durch die CPU-Unterstützung mit dem No-Execute (NX) -Bit, auch als XD-Bit, EVP-Bit oder XN-Bit bezeichnet, verstärkt, mit dem die CPU Ausführungsrechte auf Hardwareebene erzwingen kann. DEP wurde 2004 unter Linux (Kernel 2.6.8) und Microsoft 2004 als Teil von WinXP SP2 eingeführt. Apple hat DEP-Unterstützung hinzugefügt, als sie 2006 auf die x86-Architektur umgestiegen sind. Wenn DEP aktiviert ist, funktioniert unser vorheriger Exploit nicht:

> Access violation - code c0000005 (!!! second chance !!!)
[snip]
eip=000ff6b4

Dies schlägt fehl, da der Stapel als nicht ausführbar markiert ist und wir versucht haben, ihn auszuführen. Um dies zu umgehen, wurde eine Technik namens Return-Oriented Programming (ROP) entwickelt. Dies beinhaltet die Suche nach kleinen Codeausschnitten, sogenannten ROP-Gadgets, in legitimen Modulen innerhalb des Prozesses. Diese Gadgets bestehen aus einer oder mehreren Anweisungen, gefolgt von einer Rückgabe. Wenn Sie diese mit den entsprechenden Werten im Stapel verketten, kann Code ausgeführt werden.

Schauen wir uns zunächst an, wie unser Stapel jetzt aussieht:

stack addr | value
-----------+----------
 000ff6a0  | 41414141
 000ff6a4  | 41414141
 000ff6a8  | 41414141
 000ff6aa  | 41414141
>000ff6b0  | 000ff6b4
 000ff6b4  | cccccccc
 000ff6b8  | 43434343

Wir wissen, dass wir den Code bei 000ff6b4 Nicht ausführen können, daher müssen wir einen legitimen Code finden, den wir stattdessen verwenden können. Stellen Sie sich vor, unsere erste Aufgabe besteht darin, einen Wert in das Register eax zu bringen. Wir suchen irgendwo in einem Modul des Prozesses nach einer pop eax; ret - Kombination. Sobald wir eine gefunden haben, sagen wir bei 00401f60, Legen wir ihre Adresse in den Stapel:

stack addr | value
-----------+----------
 000ff6a0  | 41414141
 000ff6a4  | 41414141
 000ff6a8  | 41414141
 000ff6aa  | 41414141
>000ff6b0  | 00401f60
 000ff6b4  | cccccccc
 000ff6b8  | 43434343

Wenn dieser Shellcode ausgeführt wird, wird erneut eine Zugriffsverletzung angezeigt:

> Access violation - code c0000005 (!!! second chance !!!)
eax=cccccccc ebx=01020304 ecx=7abcdef0 edx=00000000 esi=7777f000 edi=0000f0f1
eip=43434343 esp=000ff6ba ebp=000ff6ff

Die CPU hat nun Folgendes getan:

  • Sprang zur Anweisung pop eax Unter 00401f60.
  • cccccccc vom Stapel in eax verschoben.
  • ret wurde ausgeführt und 43434343 In eip eingefügt.
  • Es wurde eine Zugriffsverletzung ausgelöst, da 43434343 Keine gültige Speicheradresse ist.

Stellen Sie sich nun vor, dass anstelle von 43434343 Der Wert bei 000ff6b8 Auf die Adresse eines anderen ROP-Gadgets gesetzt wurde. Dies würde bedeuten, dass pop eax Ausgeführt wird, dann unser nächstes Gadget. So können wir Gadgets verketten. Unser oberstes Ziel ist normalerweise, die Adresse einer Speicherschutz-API wie VirtualProtect zu ermitteln und den Stapel als ausführbar zu markieren. Wir würden dann ein endgültiges ROP-Gadget einfügen, um eine jmp esp Äquivalente Anweisung auszuführen und Shellcode auszuführen. Wir haben DEP erfolgreich umgangen!

Um diese Tricks zu bekämpfen, wurde ASLR entwickelt. Bei ASLR werden Speicherstrukturen und Modulbasisadressen zufällig versetzt, um das Erraten des Speicherorts von ROP-Gadgets und APIs sehr schwierig zu machen.

Unter Windows Vista und 7 randomisiert ASLR den Speicherort von ausführbaren Dateien und DLLs im Speicher sowie den Stapel und die Heaps nach dem Zufallsprinzip. Wenn eine ausführbare Datei in den Speicher geladen wird, erhält Windows den Zeitstempelzähler (TSC) des Prozessors, verschiebt ihn um vier Stellen, führt den Divisionsmod 254 durch und addiert dann 1. Diese Zahl wird dann mit 64 KB multipliziert, und das ausführbare Image wird mit diesem Versatz geladen . Dies bedeutet, dass es 256 mögliche Speicherorte für die ausführbare Datei gibt. Da DLLs prozessübergreifend im Speicher gemeinsam genutzt werden, werden ihre Offsets durch einen systemweiten Bias-Wert bestimmt, der beim Booten berechnet wird. Der Wert wird als TSC der CPU berechnet, wenn die Funktion MiInitializeRelocations zum ersten Mal aufgerufen, verschoben und in einen 8-Bit-Wert maskiert wird. Dieser Wert wird nur einmal pro Start berechnet.

Wenn DLLs geladen werden, gehen sie in einen gemeinsam genutzten Speicherbereich zwischen 0x50000000 Und 0x78000000. Das erste zu ladende DLL] ist immer ntdll.dll, das bei 0x78000000 - bias * 0x100000 Geladen wird, wobei bias der systemweite Bias-Wert ist, der beim Booten berechnet wird. Da es trivial wäre, den Offset eines Moduls zu berechnen, wenn Sie die Basisadresse von ntdll.dll kennen, wird auch die Reihenfolge, in der Module geladen werden, zufällig ausgewählt.

Wenn Threads erstellt werden, wird ihre Stapelbasisposition zufällig ausgewählt. Dies erfolgt durch Auffinden von 32 geeigneten Stellen im Speicher und anschließende Auswahl einer Stelle basierend auf der aktuellen TSC, die maskiert in einen 5-Bit-Wert verschoben wurde. Sobald die Basisadresse berechnet wurde, wird ein weiterer 9-Bit-Wert von der TSC abgeleitet, um die endgültige Stapelbasisadresse zu berechnen. Dies liefert einen hohen theoretischen Grad an Zufälligkeit.

Schließlich werden die Position der Heaps und die Heapzuordnungen zufällig ausgewählt. Dies wird als 5-Bit-TSC-abgeleiteter Wert multipliziert mit 64 KB berechnet, was einen möglichen Heap-Bereich von 00000000 Bis 001f0000 Ergibt.

Wenn alle diese Mechanismen mit DEP kombiniert werden, können wir keinen Shellcode ausführen. Dies liegt daran, dass wir den Stack nicht ausführen können, aber wir wissen auch nicht, wo sich eine unserer ROP-Anweisungen im Speicher befinden wird. Bestimmte Tricks können mit nop Schlitten ausgeführt werden, um einen probabilistischen Exploit zu erstellen, aber sie sind nicht ganz erfolgreich und können nicht immer erstellt werden.

Die einzige Möglichkeit, DEP und ASLR zuverlässig zu umgehen, besteht in einem Zeigerleck. Dies ist eine Situation, in der ein Wert auf dem Stapel an einem zuverlässigen Ort verwendet werden kann, um einen verwendbaren Funktionszeiger oder ein ROP-Gadget zu lokalisieren. Sobald dies erledigt ist, ist es manchmal möglich, eine Nutzlast zu erstellen, die beide Schutzmechanismen zuverlässig umgeht.

Quellen:

Weiterführende Literatur:

153
Polynomial

Zur Ergänzung der Selbstantwort von @ Polynomial: DEP kann tatsächlich auf älteren x86-Computern (die vor dem NX-Bit liegen) erzwungen werden, jedoch zu einem Preis.

Die einfache, aber eingeschränkte Möglichkeit, DEP auf alter x86-Hardware auszuführen, ist die Verwendung von Segmentregistern. Bei aktuellen Betriebssystemen auf solchen Systemen sind Adressen 32-Bit-Werte in einem flachen 4-GB-Adressraum, aber intern verwendet jeder Speicherzugriff implizit eine 32-Bit-Adresse und ein spezielles 16-Bit-Register , genannt "Segmentregister".

Im sogenannten geschützten Modus zeigen Segmentregister auf eine interne Tabelle (die "Deskriptortabelle" - tatsächlich gibt es zwei solche Tabellen, aber das ist eine technische Tatsache), und jeder Eintrag in der Tabelle gibt die Eigenschaften des Segments an. Insbesondere die Arten der zulässigen Zugriffe und die Größe des Segments. Darüber hinaus verwendet die Codeausführung implizit das CS-Segmentregister, während der Datenzugriff meistens DS (und Stapelzugriff, z. B. mit den Opcodes Push und pop) verwendet SS). Dadurch kann das Betriebssystem den Adressraum in zwei Teile aufteilen, wobei die unteren Adressen sowohl für CS als auch für DS im Bereich liegen, während die oberen Adressen für CS außerhalb des Bereichs liegen. Beispielsweise wird das von CS beschriebene Segment erstellt Dies bedeutet, dass auf jede Adresse jenseits von 0x20000000 als Daten zugegriffen werden kann (gelesen oder geschrieben, um DS als Basisregister) zu verwenden. Bei Ausführungsversuchen wird jedoch CS verwendet Die CPU löst eine Ausnahme aus (die der Kernel in ein geeignetes Signal wie SIGILL oder SIGSEGV umwandelt, was normalerweise den Tod des fehlerhaften Prozesses impliziert).

(Beachten Sie, dass Segmente auf den Adressraum angewendet werden. Das MMU ist auf einer unteren Ebene noch aktiv, sodass der oben erläuterte Trick pro Prozess ausgeführt wird.)

Dies ist billig zu tun: Die x86-Hardware tut Segmente systematisch erzwingen (und der erste 80386 tat dies bereits; tatsächlich hatte der 80286 bereits solche Segmente mit Grenzen, aber nur 16-Bit-Offsets ). Wir können sie normalerweise vergessen, weil vernünftige Betriebssysteme die Segmente so einstellen, dass sie bei Offset Null beginnen und 4 GB lang sind. Wenn Sie sie jedoch anderweitig einstellen, bedeutet dies keinen Overhead, den wir noch nicht hatten. Als DEP-Mechanismus ist es jedoch unflexibel: Wenn ein Datenblock vom Kernel angefordert wird, muss der Kernel entscheiden, ob dies für Code gilt oder nicht, da die Grenze fest ist. Wir können uns nicht entscheiden, eine bestimmte Seite dynamisch zwischen Codemodus und Datenmodus zu konvertieren.

Die unterhaltsame, aber etwas teurere Art, DEP auszuführen, verwendet etwas namens PaX . Um zu verstehen, was es tut, muss man auf einige Details eingehen.

Das MMU auf x86-Hardware verwendet speicherinterne Tabellen, die den Status jeder 4-kB-Seite im Adressraum beschreiben. Der Adressraum beträgt 4 GB, es gibt also 1048576 Seiten. Jede Seite wird durch einen 32-Bit-Eintrag in einer Untertabelle beschrieben. Es gibt 1024 Untertabellen mit jeweils 1024 Einträgen und eine Haupttabelle mit 1024 Einträgen, die auf die 1024 Untertabellen verweisen. Jeder Eintrag gibt an, wo sich das Objekt, auf das verwiesen wird (eine Untertabelle oder eine Seite), im RAM befindet oder ob es überhaupt vorhanden ist und welche Zugriffsrechte es hat. Die Ursache des Problems liegt darin, dass es bei Zugriffsrechten um Berechtigungsstufen (Kernelcode vs. Benutzerland) und nur ein Bit für den Zugriffstyp geht, sodass "Lese-/Schreibzugriff" oder "Nur-Lesezugriff" möglich ist. "Ausführung" wird als eine Art Lesezugriff angesehen. Daher hat die MMU keine Vorstellung davon, dass "Ausführung" sich vom Datenzugriff unterscheidet. Was lesbar ist, ist ausführbar.

(Seit dem Pentium Pro im vorigen Jahrhundert kennen x86-Prozessoren ein anderes Format für die Tabellen, PAE . Es verdoppelt die Größe der Einträge, wodurch lässt Raum für die Adressierung von mehr physischem RAM und das Hinzufügen eines NX-Bits - aber dieses spezifische Bit wurde erst um 2004 von der Hardware implementiert.)

Es gibt jedoch einen Trick. RAM ist langsam. Um einen Speicherzugriff durchzuführen, muss der Prozessor zuerst die Haupttabelle lesen, um die zu konsultierende Untertabelle zu finden, und dann einen weiteren Lesevorgang für diese Untertabelle durchführen Zu diesem Zeitpunkt weiß der Prozessor, ob der Speicherzugriff zulässig sein soll oder nicht und wo in physischen RAM die Daten tatsächlich abgerufen werden). Dies sind Lesezugriffe mit voller Abhängigkeit (jeder Zugriff hängt von der ab Wert, der vom vorherigen gelesen wurde), so dass sich die volle Latenz auszahlt, die auf einer modernen CPU Hunderte von Taktzyklen darstellen kann. Daher enthält die CPU einen bestimmten Cache, der die zuletzt aufgerufene Tabelle MMU enthält) Einträge. Dieser Cache ist der Translation Lookaside Buffer .

Ab dem 80486 hat die x86-CPU nicht mehr eins TLB, sondern zwei. Das Caching arbeitet mit Heuristiken, und Heuristiken hängen von Zugriffsmustern ab, und Zugriffsmuster für Code unterscheiden sich tendenziell von Zugriffsmustern für Daten. Daher fanden es die intelligenten Mitarbeiter von Intel/AMD/other lohnenswert, einen TLB für den Codezugriff (Ausführung) und einen anderen für den Datenzugriff zu haben. Darüber hinaus verfügt der 80486 über einen Opcode (invlpg), mit dem ein bestimmter Eintrag aus dem TLB entfernt werden kann.

Die Idee ist also folgende: Stellen Sie sicher, dass die beiden TLBs unterschiedliche Ansichten desselben Eintrags haben. Alle Seiten sind in den Tabellen (im RAM) als "nicht vorhanden" markiert, wodurch beim Zugriff eine Ausnahme ausgelöst wird. Der Kernel fängt die Ausnahme ab, und die Ausnahme enthält einige Daten zur Art des Zugriffs, insbesondere, ob es sich um eine Codeausführung handelt oder nicht. Der Kernel macht dann den neu gelesenen TLB-Eintrag ungültig (derjenige, der "abwesend" sagt), füllt dann den Eintrag in RAM mit einigen Rechten, die den Zugriff ermöglichen, und erzwingt dann einen Zugriff des erforderlichen Typs () entweder Daten lesen oder Code ausführen), was den Eintrag in den entsprechenden TLB einspeist, und nur diesen. Der Kernel setzt dann sofort den Eintrag in RAM zurück auf abwesend und kehrt schließlich zum Prozess zurück (zurück zum erneuten Versuch des Opcodes, der die Ausnahme ausgelöst hat).

Der Nettoeffekt besteht darin, dass, wenn die Ausführung zum Prozesscode zurückkehrt, der TLB für Code oder der TLB für Daten den entsprechenden Eintrag enthält, der andere TLB jedoch nicht und will nicht da die Tabellen in RAM immer noch "abwesend" sagen. Zu diesem Zeitpunkt ist der Kernel in der Lage zu entscheiden, ob die Ausführung zugelassen werden soll oder nicht, unabhängig davon, ob dies der Fall ist Ermöglicht den Datenzugriff oder nicht. Dadurch kann eine NX-ähnliche Semantik erzwungen werden.

Der Teufel versteckt sich im Detail; In diesem Fall ist Platz für eine ganze Legion Dämonen. Ein solcher Tanz mit der Hardware ist nicht einfach richtig umzusetzen. Besonders bei Mehrkernsystemen.

Der Overhead ist folgender: Wenn ein Zugriff ausgeführt wird und der TLB nicht den relevanten Eintrag enthält, muss auf die Tabellen in RAM) zugegriffen werden, und dies allein bedeutet, dass einige hundert Zyklen verloren gehen Kosten, PaX fügt den Overhead der Ausnahme und den Verwaltungscode hinzu, der den richtigen TLB ausfüllt, wodurch die "ein paar hundert Zyklen" in "ein paar tausend Zyklen" umgewandelt werden. Glücklicherweise sind TLB-Fehler richtig. Die PaX-Leute behaupten zu haben gemessen eine Verlangsamung von nur 2,7% bei einem großen Kompilierungsjob (dies hängt jedoch vom CPU-Typ ab).

Das NX-Bit macht all dies überflüssig. Beachten Sie, dass das PaX-Patchset auch einige andere sicherheitsrelevante Funktionen enthält, wie z. B. ASLR, das mit einigen redundant ist Funktionalität neuerer offizieller Kernel.

40
Thomas Pornin