Frage:
Wie funktionieren ASLR und DEP?
Polynomial
2012-08-12 20:13:42 UTC
view on stackexchange narkive permalink

Wie funktionieren Adressraum-Layout-Randomisierung (ASLR) und Data Execution Prevention (DEP), um zu verhindern, dass Schwachstellen ausgenutzt werden? Können sie umgangen werden?

Zwei antworten:
Polynomial
2012-08-12 20:13:42 UTC
view on stackexchange narkive permalink

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 Schwachstelle ausgenutzt werden kann. Wir werden alle Details überspringen, aber sagen wir einfach, wir verwenden eine Sicherheitsanfälligkeit bezüglich eines Stapelpufferüberlaufs. Wir haben einen großen Blob mit 0x41414141 -Werten in unsere Nutzdaten geladen, und eip wurde auf 0x41414141 gesetzt, sodass wir wissen, dass er ausnutzbar ist. Wir haben dann ein geeignetes Tool verwendet (z. B. Metasploits pattern_create.rb ), um den Offset des Werts zu ermitteln, der in eip geladen wird. 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 Bei einem Nicht-ASLR- und Nicht-DEP-Prozess ist die Stapeladresse bei jedem Ausführen des Prozesses dieselbe. Wir wissen genau, wo es in Erinnerung ist. Schauen wir uns also an, wie der Stapel mit den oben beschriebenen Testdaten aussieht:

  stack addr | Wert ----------- + ---------- 000ff6a0 | 41414141 000ff6a4 | 41414141 000ff6a8 | 41414141 000ff6aa | 41414141>000ff6b0 | 42424242 > esp Punkte hier 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 sagten, dass sie sein sollten. 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 | Wert ----------- + ---------- 000ff6a0 | 41414141 000ff6a4 | 41414141 000ff6a8 | 41414141 000ff6aa | 41414141>000ff6b0 | 000ff6b4 000ff6b4 | cccccccc 000ff6b8 | 43434343  

Wir haben den Wert auf 000ff6b0 festgelegt, sodass eip auf 000ff6b4 gesetzt wird. der nächste Versatz im Stapel. Dies führt dazu, dass 0xcc ausgeführt wird, was eine int3 -Anweisung ist. Da int3 ein Software-Interrupt-Haltepunkt ist, wird eine Ausnahme ausgelöst und der Debugger wird angehalten. Auf diese Weise können wir überprüfen, ob der Exploit erfolgreich war.

  > Ausnahme der Unterbrechungsanweisung - Code 80000003 (erste Chance) [snip] eip = 000ff6b4  

Jetzt Wir können 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:

  > Zugriffsverletzung - Code c0000005 (!!! zweite 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 | Wert ----------- + ---------- 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 übertragen. Wir suchen nach einem pop eax; ret Kombination irgendwo in einem beliebigen Modul innerhalb des Prozesses. Sobald wir eine gefunden haben, sagen wir bei 00401f60 , legen wir ihre Adresse in den Stapel:

  stack addr | Wert ----------- + ---------- 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:

  > Zugriffsverletzung - Code c0000005 (!!! zweite Chance! !!) eax = cccccccc ebx = 01020304 ecx = 7abcdef0 edx = 00000000 esi = 7777f000 edi = 0000f0f1eip = 43434343 esp = 000ff6ba ebp = 000ff6ff  

Die CPU hat jetzt Folgendes ausgeführt:

  • Zur Anweisung pop eax unter 00401f60 gesprungen.
  • cccccccc vom Stapel gestrichen , in ax .
  • ret ausgeführt und 43434343 in eip eingefügt.
  • Eine Zugriffsverletzung wurde ausgelöst, weil 43434343 keine gültige Speicheradresse ist.

Stellen Sie sich nun vor, dass anstelle von 43434343 wurde der Wert bei 000ff6b8 auf die Adresse eines anderen ROP-Gadgets gesetzt. Dies würde bedeuten, dass pop eax ausgeführt wird, dann unser nächstes Gadget. So können wir Gadgets verketten. Unser oberstes Ziel besteht normalerweise darin, 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. ASLR beinhaltet das zufällige Versetzen von Speicherstrukturen und Modulbasisadressen, 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 der Stapel und Haufen. 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, werden sie in einen gemeinsam genutzten Speicherbereich zwischen 0x50000000 und 0x78000000 verschoben. Die erste zu ladende DLL ist immer ntdll.dll, die unter 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 bietet einen hohen theoretischen Grad an Zufälligkeit.

Schließlich werden die Position der Heaps und die Heap-Zuordnungen randomisiert. 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 werden mit DEP kombiniert. Wir können keinen Shellcode ausführen. Dies liegt daran, dass wir den Stapel 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. Sie sind jedoch nicht vollständig erfolgreich und können nicht immer erstellt werden.

Die einzige Möglichkeit, DEP zuverlässig zu umgehen und ASLR ist durch ein 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:

obligatorisch tl; dr
@TerryChia Du meinst ta; cr? - Zu großartig, konnte nicht lesen;)
Ich gebe Ihnen inoffiziell den Titel Sir Polynomial. Ordentliche Antwort. xD
Ja ... Es ist fast so, als ob die Frage für diese Antwort * gestellt * wurde. Beeindruckend.
Normalerweise gehe ich nicht auf diese Selbstantworten ein. Aber das ist so eine gute Antwort, ich mache dieses Zeug seit Jahren und habe hier ein paar Dinge gelehnt. Großartige Arbeit +1
@lynks Ich finde, dass selbst beantwortete Fragen entweder neu formuliert werden oder dem Wohl der Gemeinschaft dienen. Ich versichere Ihnen, dass ich es mit der letzteren Absicht gepostet habe. Ich habe sehr lange gebraucht, um DEP und ASLR richtig zu verstehen. Ich hoffe nur, dass diese Antwort jemand anderem hilft, sie zu verstehen.
"Der einzige zuverlässige Weg, um DEP und ASLR zu umgehen": Das ist der schwierige Punkt. Der Angreifer benötigt keinen zuverlässigen Weg; Oft ist ein Weg, der nur einmal in tausendmal funktioniert, gut genug, da er es nur tausendmal versuchen muss (was oft als Skript geschrieben werden kann). Dies zeigt die Grenzen der Wirksamkeit von ASLR und DEP als Sicherheitsmechanismen.
@ThomasPornin Ja, die Nop-Schlittentechnik kann gut sein. Ein paar tausend Mal ist der Nachteil, dass es normalerweise nach dem ersten Mal zu einem Absturz kommt.
Tolle Erklärung und perfekte Länge
Thomas Pornin
2012-08-15 07:56:36 UTC
view on stackexchange narkive permalink

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 verwendet Segmentregister. 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, das aufgerufen wird ein "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 jeden 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 hauptsächlich DS verwendet (und der Stapelzugriff, z. B. mit den Opcodes push und pop , SS verwendet). Dadurch kann das Betriebssystem den Adressraum in zwei Teile aufteilen. Die unteren Adressen liegen sowohl für CS als auch für DS im Bereich, während die oberen Adressen für CS außerhalb des Bereichs liegen. Beispielsweise hat das von CS beschriebene Segment eine Größe von 512 MB. Dies bedeutet, dass auf jede Adresse jenseits von 0x20000000 als Daten zugegriffen werden kann (gelesen oder geschrieben, um DS als Basisregister zu verwenden), Ausführungsversuche jedoch CS verwenden. Zu diesem Zeitpunkt löst die CPU eine Ausnahme aus (die der Kernel in ein geeignetes Signal wie konvertiert) SIGILL oder SIGSEGV, was normalerweise den Tod des fehlerhaften Prozesses impliziert.

(Beachten Sie, dass Segmente auf den Adressraum angewendet werden; die MMU ist also auf einer unteren Ebene noch aktiv Der oben erläuterte Trick ist pro Prozess.)

Dies ist billig: Die x86-Hardware erzwingt systematisch Segmente (und der erste 80386 hat dies bereits getan; 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 Methode, DEP durchzuführen, verwendet etwas namens PaX. Um zu verstehen, was es tut, muss man auf einige Details eingehen.

Die MMU auf x86-Hardware verwendet In-Memory-Tabellen, die den Status jeder 4-kB-Seite in der Adresse beschreiben Raum. 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 um ein Bit für den Zugriffstyp geht, sodass "Lese-Schreib" oder "Nur-Lese" möglich ist. "Ausführung" wird als eine Art Lesezugriff angesehen. Daher hat die MMU keine Vorstellung davon, dass sich "Ausführung" vom Datenzugriff unterscheidet. Was lesbar ist, ist ausführbar.

(Seit dem Pentium Pro im vergangenen Jahrhundert kennen x86-Prozessoren ein anderes Format für die Tabellen, PAE. Es verdoppelt die Größe der Einträge, wodurch Platz für die Adressierung von mehr physischem RAM bleibt und auch ein NX-Bit hinzufügen - 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 durchsuchende Untertabelle zu finden, dann diese Untertabelle erneut lesen, und erst an diesem Punkt weiß der Prozessor, ob der Speicherzugriff erfolgen soll erlaubt oder nicht, und wo im physischen RAM die Daten, auf die zugegriffen wird, wirklich sind. Dies sind Lesezugriffe mit vollständiger Abhängigkeit (jeder Zugriff hängt von dem vom vorherigen gelesenen Wert ab), sodass 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 aufgerufenen MMU-Tabelleneinträge enthält. Dieser Cache ist der Translation Lookaside Buffer.

Ab 80486 verfügt die x86-CPU nicht über einen TLB, sondern über 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. Die intelligenten Mitarbeiter von Intel / AMD / other fanden es daher sinnvoll, einen TLB für den Codezugriff (Ausführung) und einen TLB 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 lautet also wie folgt: 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 handelte oder nicht. Der Kernel macht dann den neu gelesenen TLB-Eintrag ungültig (derjenige, der "abwesend" sagt), füllt dann den Eintrag im RAM mit einigen Rechten, die den Zugriff ermöglichen, und erzwingt dann einen Zugriff des erforderlichen Typs (entweder Datenlesen oder Codeausführung), der füttert den Eintrag in den entsprechenden TLB und nur diesen. Der Kernel setzt dann den Eintrag im RAM sofort wieder 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 ist der, wenn die Ausführung zurückkommt Für den Prozesscode enthält der TLB für Code oder der TLB für Daten den entsprechenden Eintrag, der andere TLB jedoch nicht und nicht , da die Tabellen im RAM immer noch angeben "abwesend". Zu diesem Zeitpunkt kann der Kernel entscheiden, ob die Ausführung zulässig ist oder nicht, unabhängig davon, ob er den Datenzugriff zulässt oder nicht. Es kann somit eine NX-ähnliche Semantik erzwingen.

Der Teufel versteckt sich in den Details; In diesem Fall ist Platz für eine ganze Legion Dämonen. Ein solcher Tanz mit der Hardware ist nicht einfach richtig umzusetzen. Insbesondere auf Mehrkernsystemen.

Der Overhead ist folgender: Wenn ein Zugriff ausgeführt wird und der TLB nicht den relevanten Eintrag enthält, muss auf die Tabellen im RAM zugegriffen werden, und dies allein bedeutet, dass einige verloren gehen hundert Zyklen. Zu diesen Kosten addiert PaX den Overhead der Ausnahme und den Verwaltungscode, der den richtigen TLB ausfüllt, wodurch die "einige hundert Zyklen" in "einige tausend Zyklen" umgewandelt werden. Glücklicherweise sind TLB-Fehler richtig. Die PaX-Leute behaupten, eine Verlangsamung von nur 2,7% bei einem großen Kompilierungsjob gemessen zu haben (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, z. B. ASLR, das mit einigen Funktionen neuerer offizieller Kernel überflüssig ist.

+1. Eine hervorragende Ergänzung zu meiner Antwort, einschließlich einiger Fakten, die ich nicht kannte.
Ich habe meine Antwort als akzeptiert markiert, aber Ihre hat mir viel zu denken gegeben, deshalb habe ich eine Prämie von +100 für Sie hinzugefügt. Ich werde es in 24 Stunden vergeben, wenn es abläuft :)
PaX bietet tatsächlich eine andere und überlegene ASLR-Implementierung.Für Dinge wie SMEP / SMAP ist UDEREF von PaX immer noch überlegen (aufgrund der Schwierigkeit, UDEREF im Vergleich zu SMEP / SMAP zu deaktivieren, und SMAP-Leckseitentabellen, die UDEREF nicht hat).


Diese Fragen und Antworten wurden automatisch aus der englischen Sprache übersetzt.Der ursprüngliche Inhalt ist auf stackexchange verfügbar. Wir danken ihm für die cc by-sa 3.0-Lizenz, unter der er vertrieben wird.
Loading...