Windows NT: Privilege Escalation | Part 2

Nun möchte ich besprechen, genau der Angriff ablaufen wird und was die Schwierigkeiten sind. Es gibt nämlich mehrere Probleme, welche den Exploit erheblich schwieriger machen. Hier werden wir uns anschauen, wie wir diese Probleme umgehen können.

1: DEP
DEP (Data Execution Prevention) ist eine sehr effektive Art des Speicherschutzes, welche durch das NX-Bit (No-Execute) realisiert wird. Es ist ein Eintrag in der Page Table und verhindert, dass dieser Code ausgeführt werden kann, wenn das Bit gesetzt ist.

2: SMEP/SMAP
Hier haben wir SMEP (Supervisor Mode Execution Prevention). Dies ist ein Feature der CPU und führt dazu, dass die CPU im Ring 0 keinen Code mehr ausführt, welcher im Usermode gemappt ist. Die CPU schaut hierzu einfach in die Page Table.

Dieses Feature verhindert, dass wir direkt zu dem Code im Usermode springen können, was sehr einfach wäre, da wir ja den RIP kontrolliern durch die Return-Addresse und die Addresse im Usermode kennen.

3: KASLR
KASLR (Kernel Address Space Layout Randomisation) führt dazu, dass alle Treiber an einer zufälligen Stelle geladen werden. So kann man nie wissen, wo sich gerade welches Modul befindet. Das ist aber ein Problem, da wir schliesslich ROP anwenden müssen.

Wir kennen also keine Addresse mit 100% Sicherheit und können uns keinen Fehler erlauben, da sonst alles crasht. Das sind nicht gerade die besten Voraussetzungen. Es gibt 2 verschiedene Herangehensweisen:

  • a) Man versucht, irgendwie an die Addressen der Module heranzukommen. Dies kann man eventuell auch durch einen Exploit machen. Vielleicht könnte man auch durch geschickte Seitenkanalattacken einige Addressen leaken. Doch hierzu habe ich nicht wirklich viel Informationen gefunden. Zudem bräuchte man eine bessere Testumgebung und mehr Kenntnisse, wie genau die CPU arbeitet (Mikroarchitektur, Caching, …).
  • b) Trotz ASLR können wir immer noch relative Addressen aufrufen. Dies ist so, weil die Return-Addresse so auf den Stack geschrieben wird, sodass der untere Teil der Addresse bei einer tieferen Addresse steht als der höhere Teil. Dies erlaubt es uns, nur die hinteren 1,2 oder mehr Bytes zu überschreiben und so einen relativen Sprung innerhalb des Treibers zu machen. Doch wir sind sehr limitiert, da wir nur einen einzigen Sprung machen können.
Hier sieht man, dass der tiefe Teil der Addresse zuerst überschrieben wird bei einem Overflow der Returnaddresse. Dies führt dazu. dass man in einer Range springen kann.

Nun kann man sich folgendes Szenario vorstellen: Die Funktion TriggerBufferOverflowStack wird von der Funktion BufferOverflowStackIoctlHandler aufgerufen. Das heisst, während die Funktion TriggerBufferOverflowStack aktiv ist, steht zeigt der Return-Pointer an eine Stelle in der Funktion BufferOverflowStackIoctlHandler.

Der Return Pointer zeigt also auf eine Stelle in BufferOverflowStackIoctlHandler

Die Genaue Struktur kann man mit dem Tool PE-Bear anschauen. Es ist ein Tool, mit welchem man PE-Dateien analysieren kann. Hier sieht man, dass es zwischen der .data Sektion und der .pdata Sektion einen grossen Abstand gibt (also erst wenn das Modul geladen wurde, im virtuellen Addressraum).

Die Funktion BufferOverflowStackIoctlHandler befindet sich in der PAGE-Sektion. Die genaue Addresse spielt nicht so eine Rolle für das vorhaben. Was aber vorausgesetzt wird, ist dass das Alignment von ASLR auf 2 Bytes angesetzt ist (also wenn eine zufällige Addresse für das Modul generiert wird, dass die letzten 2 Bytes der Addresse immer 0 sind). Dies ist nach meinen neusten Erkenntnisse der Fall, aber dies könnte sich vielleicht irgendwann ändern. Wenn es sich ändern würde, wäre unsere Range nur noch 256 Bytes gross, anstatt 65’000 Bytes. Dies würde die Wahrscheinlichkeit für die richtigen Instruktionen stark verringern.

Auf dieser Grafik sieht man all diese Informationen als Skizze. Die Skizze ist aber nicht Massstabgetreu muss man anmerken.

Nun habe ich versucht, irgendwelche nützlichen Instruktionen in diesem Bereich zu finden. Leider habe ich durch längeres betrachten auch nichts gefunden, Das heisst aber nicht, dass es keine gibt. Ich konnte die Suche leider nicht automatisieren und selber habe ich natürlich nicht alle Möglichkeiten ausprobiert. Ein Tool, welches dies übernimmt, wäre auch mal eine gute Sache, aber ich kenne gerade keins, welches hierfür geeignet ist.

Nun: Da ich das Ganze eh nur zu Demonstrationszwecken durchführe, darf ich jetzt einfach in die Trickkiste greifen und uns ein schönes Szenario herbeiführen (indem ich den Source-Code minimal abändere). Kleine Bemerkung am Rande: Bösartige Hacker könnten natürlich auch versuchen, bewusst solchen Code irgendwie in ein Open-Source Projekt einfliessen zu lassen, um später Exploits zu ermöglichen. Bei Fragwürdigen Konstanten sollte man sich also immer Fragen, was die eigentlich bedeuten.

Diese Funktion ermöglicht uns später den Exploit. Man kann sich jetzt Fragen, was denn diese Funktion so speziell macht. Die Antwort ist, dass der Wert MAGIC hardgecodet ist. Das heisst, er wird genau so im Programmcode stehen. wie er hier ist. Die entsprechende Instruktion wäre

REX.W + B8+ rd io	MOV r64, imm64	      Move imm64 to r64.

Der Immediate-Wert steht also auch im ausführbaren Bereich. Nun kann man die Semantik der Instruktion natürlich verändern, indem man nicht an den Anfang springt, sondern um zwei Bytes danach:

Hier die usrsprüngliche Funktion someFunction +0x0
Hier die gleiche Opcodes, wenn man an die Stelle someFuncion +0x2 springt

Man sieht, dass sich Sinn der Funktion komplett verändert hat, je nachdem ab welcher Addresse die Instruktionen interpretiert werden. Dies ist vorallem bei Intels x86 ISA der Fall, da Instruktionen hier eine Variable Länge (valide Instruktionen haben eine Länge von 1 bis zu 15 Bytes) haben können und deshalb kein Alignment haben. Bei ARM zum Beispiel ist dies anders: Hier haben alle Instruktionen eine feste Länge von 4 Bytes. Wer diese Diskussion über CISC und RISC interessiert, kann ja mal im Internet nachschauen. Aber nun zurück zum Thema: Es wird nun etwas komplett anderes gemacht. Nämlich wird der MAGIC Wert zu den Instruktionen

  • mov cr3, rbx
  • jmp rsi

umgewandelt. Da rbx und rsi von der Funktion TriggerBufferOverflowStack auf dem Stack gespeichert werden, können wird den Wert beliebig setzen, indem wir die Speicherstelle überschreiben. Das mov cr3, rbx führt dazu, dass wir SMEP und SMAP-Kontrollbits setzen können und diese nun löschen werden. Dabei müssen wir aber noch darauf achten, dass wir die restlichen Kontrollbits nicht verändern (sonst crasht wahrscheinlich der PC).

Ab nun können wird also zu unserem Code springen, welches sich im Usermode befindet. Dies geschieht mit call rsi. Die Addresse im Usermode kennen wir ja.

Damit wäre soweit alles getan. Der Angriff kann nun genau geplant werden.

Als nächstes müssen wir noch den Shellcode genau konstruieren und die RVA der Funktion someFunction herausfinden. Das Ganze wird ein bisschen wie tüfteln, da man nie alles vorhersehen kann (tatsächliche Grösse des Stacks, relative Addresse des Buffers, weitere Sicherheitsvorkehrungen, …). Es kann also gut sein, dass unser Plan, wie wir ihn hier haben, nicht ganz aufgeht und dann müssen wir uns etwas anderen überlegen.

Das aber alles im nächsten Teil

1 thought on “Windows NT: Privilege Escalation | Part 2

  1. Noch eine kleine Korrektur: Es ist nicht das CR3 Register, welches SMEP/SMAP steuert, sondern CR4. CR3 wäre für Paging. Es ändert sich aber nicht viel für den Exploit, Die Opcodes sind fast identisch (0x0f2223). In weiteren Teilen dieser Serie wird diese Änderung verwendet.

Schreiben Sie einen Kommentar

Ihre E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.