Wie funktioniert die Sicherheitsanfälligkeit JPEG of Death?

94

Ich habe über einen älteren Exploit gegen GDI + unter Windows XP und Windows Server 2003 gelesen, der als JPEG des Todes für ein Projekt bezeichnet wird, an dem ich arbeite.

Der Exploit wird unter folgendem Link ausführlich erläutert: http://www.infosecwriters.com/text_resources/pdf/JPEG.pdf

Grundsätzlich enthält eine JPEG-Datei einen Abschnitt namens COM mit einem (möglicherweise leeren) Kommentarfeld und einen Zwei-Byte-Wert mit der Größe von COM. Wenn keine Kommentare vorhanden sind, beträgt die Größe 2. Der Leser (GDI +) liest die Größe, subtrahiert zwei und weist einen Puffer der entsprechenden Größe zu, um die Kommentare in den Heap zu kopieren. Der Angriff beinhaltet das Platzieren eines Wertes von 0im Feld. GDI + subtrahiert 2, was dazu führt, dass ein Wert von -2 (0xFFFe)in die vorzeichenlose Ganzzahl 0XFFFFFFFEvon konvertiert wird memcpy.

Beispielcode:

unsigned int size;
size = len - 2;
char *comment = (char *)malloc(size + 1);
memcpy(comment, src, size);

Beachten Sie, dass malloc(0)in der dritten Zeile ein Zeiger auf den nicht zugewiesenen Speicher auf dem Heap zurückgegeben werden sollte. Wie kann das Schreiben von 0XFFFFFFFEBytes ( 4GB!!!!) das Programm möglicherweise nicht zum Absturz bringen? Schreibt dies über den Heap-Bereich hinaus und in den Bereich anderer Programme und des Betriebssystems? Was passiert dann?

Soweit ich memcpyweiß, werden einfach nZeichen vom Ziel in die Quelle kopiert . In diesem Fall sollte sich die Quelle auf dem Stapel befinden, das Ziel auf dem Heap und nist 4GB.

Rafa
quelle
malloc reserviert Speicher vom Heap. Ich denke, der Exploit wurde vor memcpy und nach der Zuweisung des Speichers durchgeführt
iedoc
Nur als Randnotiz: Es ist nicht memcpy, was den Wert auf eine vorzeichenlose Ganzzahl (4 Bytes) heraufstuft, sondern die Subtraktion.
Rev.
1
Meine vorherige Antwort wurde mit einem Live-Beispiel aktualisiert. Die mallocED-Größe beträgt nur 2 Bytes anstatt 0xFFFFFFFE. Diese enorme Größe wird nur für die Kopiengröße verwendet, nicht für die Zuordnungsgröße.
Neitsa

Antworten:

96

Diese Sicherheitsanfälligkeit war definitiv ein Haufenüberlauf .

Wie kann das Schreiben von 0XFFFFFFFE-Bytes (4 GB !!!!) das Programm möglicherweise nicht zum Absturz bringen?

Dies wird wahrscheinlich der Fall sein, aber in einigen Fällen haben Sie Zeit zum Ausnutzen, bevor der Absturz auftritt (manchmal können Sie das Programm auf seine normale Ausführung zurücksetzen und den Absturz vermeiden).

Wenn memcpy () gestartet wird, überschreibt die Kopie entweder einige andere Heap-Blöcke oder einige Teile der Heap-Verwaltungsstruktur (z. B. freie Liste, Besetztliste usw.).

Irgendwann stößt die Kopie auf eine nicht zugewiesene Seite und löst beim Schreiben eine AV (Zugriffsverletzung) aus. GDI + wird dann versuchen, einen neuen Block im Heap zuzuweisen (siehe ntdll! RtlAllocateHeap ) ... aber die Heap-Strukturen sind jetzt alle durcheinander.

An diesem Punkt können Sie durch sorgfältiges Erstellen Ihres JPEG-Bilds die Heap-Verwaltungsstrukturen mit kontrollierten Daten überschreiben. Wenn das System versucht, den neuen Block zuzuweisen, wird wahrscheinlich die Verknüpfung eines (freien) Blocks mit der freien Liste aufgehoben.

Blöcke werden mit (insbesondere) einem Flink- (Vorwärtslink; der nächste Block in der Liste) und einem Blink- (Rückwärtslink; der vorherige Block in der Liste) Zeiger verwaltet. Wenn Sie sowohl das Flinken als auch das Blinken steuern, haben Sie möglicherweise eine mögliche WRITE4 (Write What / Where-Bedingung), in der Sie steuern, was Sie schreiben und wo Sie schreiben können.

Zu diesem Zeitpunkt können Sie einen Funktionszeiger überschreiben ( SEH- Zeiger ( Structured Exception Handlers) waren zu diesem Zeitpunkt im Jahr 2004 ein Ziel der Wahl) und die Codeausführung erhalten.

Siehe Blog-Beitrag Heap Corruption: Eine Fallstudie .

Hinweis: Obwohl ich über die Ausnutzung mithilfe der Freelist geschrieben habe, kann ein Angreifer einen anderen Pfad unter Verwendung anderer Heap-Metadaten auswählen ("Heap-Metadaten" sind Strukturen, die vom System zur Verwaltung des Heaps verwendet werden; Flink und Blink sind Teil der Heap-Metadaten) Die Unlink-Ausnutzung ist wahrscheinlich die "einfachste". Eine Google-Suche nach "Heap Exploitation" wird zahlreiche Studien dazu liefern.

Schreibt dies über den Heap-Bereich hinaus und in den Bereich anderer Programme und des Betriebssystems?

Noch nie. Moderne Betriebssysteme basieren auf dem Konzept des virtuellen Adressraums, sodass jeder Prozess seinen eigenen virtuellen Adressraum hat, der die Adressierung von bis zu 4 Gigabyte Speicher auf einem 32-Bit-System ermöglicht (in der Praxis haben Sie nur die Hälfte davon im Benutzerland). der Rest ist für den Kernel).

Kurz gesagt, ein Prozess kann nicht auf den Speicher eines anderen Prozesses zugreifen (außer wenn er den Kernel über einen Dienst / eine API danach fragt, der Kernel jedoch prüft, ob der Aufrufer das Recht dazu hat).


Ich habe beschlossen, diese Sicherheitsanfälligkeit am Wochenende zu testen, damit wir eine gute Vorstellung davon bekommen, was vor sich geht, und nicht nur über Spekulationen. Die Sicherheitsanfälligkeit ist jetzt 10 Jahre alt, daher dachte ich, es sei in Ordnung, darüber zu schreiben, obwohl ich den Ausnutzungsteil in dieser Antwort nicht erklärt habe.

Planung

Die schwierigste Aufgabe war es, ein Windows XP mit nur SP1 zu finden, wie es 2004 war :)

Dann habe ich ein JPEG-Bild heruntergeladen, das nur aus einem einzelnen Pixel besteht, wie unten gezeigt (der Kürze halber geschnitten):

File 1x1_pixel.JPG
Address   Hex dump                                         ASCII
00000000  FF D8 FF E0|00 10 4A 46|49 46 00 01|01 01 00 60| ÿØÿà JFIF  `
00000010  00 60 00 00|FF E1 00 16|45 78 69 66|00 00 49 49|  `  ÿá Exif  II
00000020  2A 00 08 00|00 00 00 00|00 00 00 00|FF DB 00 43| *          ÿÛ C
[...]

Ein JPEG-Bild besteht aus binären Markern (die Segmente einführen). Im obigen Bild FF D8befindet sich der SOI-Marker (Start Of Image), während FF E0er beispielsweise ein Anwendungsmarker ist.

Der erste Parameter in einem Markersegment (mit Ausnahme einiger Marker wie SOI) ist ein Zwei-Byte-Längenparameter, der die Anzahl der Bytes im Markersegment einschließlich des Längenparameters und ohne den Zwei-Byte-Marker codiert.

Ich habe einfach einen COM-Marker (0x FFFE) direkt nach dem SOI hinzugefügt , da die Marker keine strenge Reihenfolge haben.

File 1x1_pixel_comment_mod1.JPG
Address   Hex dump                                         ASCII
00000000  FF D8 FF FE|00 00 30 30|30 30 30 30|30 31 30 30| ÿØÿþ  0000000100
00000010  30 32 30 30|30 33 30 30|30 34 30 30|30 35 30 30| 0200030004000500
00000020  30 36 30 30|30 37 30 30|30 38 30 30|30 39 30 30| 0600070008000900
00000030  30 61 30 30|30 62 30 30|30 63 30 30|30 64 30 30| 0a000b000c000d00
[...]

Die Länge des COM-Segments wird so eingestellt, dass 00 00die Sicherheitsanfälligkeit ausgelöst wird. Ich habe auch 0xFFFC-Bytes direkt nach dem COM-Marker mit einem wiederkehrenden Muster, einer 4-Byte-Zahl in hexadezimaler Form, eingefügt, was nützlich sein wird, wenn die Sicherheitsanfälligkeit "ausgenutzt" wird.

Debuggen

Ein Doppelklick auf das Bild löst sofort den Fehler in der Windows-Shell (auch bekannt als "explorer.exe") irgendwo gdiplus.dllin einer Funktion mit dem Namen aus GpJpegDecoder::read_jpeg_marker().

Diese Funktion wird für jeden Marker im Bild aufgerufen. Sie liest einfach die Markersegmentgröße, weist einen Puffer zu, dessen Länge der Segmentgröße entspricht, und kopiert den Inhalt des Segments in diesen neu zugewiesenen Puffer.

Hier der Start der Funktion:

.text:70E199D5  mov     ebx, [ebp+arg_0] ; ebx = *this (GpJpegDecoder instance)
.text:70E199D8  push    esi
.text:70E199D9  mov     esi, [ebx+18h]
.text:70E199DC  mov     eax, [esi]      ; eax = pointer to segment size
.text:70E199DE  push    edi
.text:70E199DF  mov     edi, [esi+4]    ; edi = bytes left to process in the image

eaxDas Register zeigt auf die Segmentgröße und edigibt die Anzahl der im Bild verbleibenden Bytes an.

Der Code liest dann die Segmentgröße, beginnend mit dem höchstwertigen Byte (Länge ist ein 16-Bit-Wert):

.text:70E199F7  xor     ecx, ecx        ; segment_size = 0
.text:70E199F9  mov     ch, [eax]       ; get most significant byte from size --> CH == 00
.text:70E199FB  dec     edi             ; bytes_to_process --
.text:70E199FC  inc     eax             ; pointer++
.text:70E199FD  test    edi, edi
.text:70E199FF  mov     [ebp+arg_0], ecx ; save segment_size

Und das niedrigstwertige Byte:

.text:70E19A15  movzx   cx, byte ptr [eax] ; get least significant byte from size --> CX == 0
.text:70E19A19  add     [ebp+arg_0], ecx   ; save segment_size
.text:70E19A1C  mov     ecx, [ebp+lpMem]
.text:70E19A1F  inc     eax             ; pointer ++
.text:70E19A20  mov     [esi], eax
.text:70E19A22  mov     eax, [ebp+arg_0] ; eax = segment_size

Sobald dies erledigt ist, wird die Segmentgröße verwendet, um einen Puffer nach dieser Berechnung zuzuweisen:

alloc_size = segment_size + 2

Dies geschieht durch den folgenden Code:

.text:70E19A29  movzx   esi, word ptr [ebp+arg_0] ; esi = segment size (cast from 16-bit to 32-bit)
.text:70E19A2D  add     eax, 2 
.text:70E19A30  mov     [ecx], ax 
.text:70E19A33  lea     eax, [esi+2] ; alloc_size = segment_size + 2
.text:70E19A36  push    eax             ; dwBytes
.text:70E19A37  call    _GpMalloc@4     ; GpMalloc(x)

In unserem Fall beträgt die zugewiesene Größe für den Puffer 2 Byte , da die Segmentgröße 0 ist .

Die Sicherheitsanfälligkeit liegt direkt nach der Zuweisung:

.text:70E19A37  call    _GpMalloc@4     ; GpMalloc(x)
.text:70E19A3C  test    eax, eax
.text:70E19A3E  mov     [ebp+lpMem], eax ; save pointer to allocation
.text:70E19A41  jz      loc_70E19AF1
.text:70E19A47  mov     cx, [ebp+arg_4]   ; low marker byte (0xFE)
.text:70E19A4B  mov     [eax], cx         ; save in alloc (offset 0)
;[...]
.text:70E19A52  lea     edx, [esi-2]      ; edx = segment_size - 2 = 0 - 2 = 0xFFFFFFFE!!!
;[...]
.text:70E19A61  mov     [ebp+arg_0], edx

Der Code subtrahiert einfach die Größe segment_size (Segmentlänge ist ein Wert von 2 Byte) von der gesamten Segmentgröße (in unserem Fall 0) und führt zu einem ganzzahligen Unterlauf: 0 - 2 = 0xFFFFFFFE

Der Code prüft dann, ob noch Bytes im Bild zu analysieren sind (was wahr ist), und springt dann zur Kopie:

.text:70E19A69  mov     ecx, [eax+4]  ; ecx = bytes left to parse (0x133)
.text:70E19A6C  cmp     ecx, edx      ; edx = 0xFFFFFFFE
.text:70E19A6E  jg      short loc_70E19AB4 ; take jump to copy
;[...]
.text:70E19AB4  mov     eax, [ebx+18h]
.text:70E19AB7  mov     esi, [eax]      ; esi = source = points to segment content ("0000000100020003...")
.text:70E19AB9  mov     edi, dword ptr [ebp+arg_4] ; edi = destination buffer
.text:70E19ABC  mov     ecx, edx        ; ecx = copy size = segment content size = 0xFFFFFFFE
.text:70E19ABE  mov     eax, ecx
.text:70E19AC0  shr     ecx, 2          ; size / 4
.text:70E19AC3  rep movsd               ; copy segment content by 32-bit chunks

Das obige Snippet zeigt, dass die Kopiergröße 0xFFFFFFFE 32-Bit-Chunks beträgt. Der Quellpuffer wird gesteuert (Bildinhalt) und das Ziel ist ein Puffer auf dem Heap.

Schreibbedingung

Die Kopie löst eine Ausnahme von Zugriffsverletzungen (AV) aus, wenn sie das Ende der Speicherseite erreicht (dies kann entweder vom Quellzeiger oder vom Zielzeiger sein). Wenn der AV ausgelöst wird, befindet sich der Heap bereits in einem anfälligen Zustand, da die Kopie bereits alle folgenden Heap-Blöcke überschrieben hat, bis eine nicht zugeordnete Seite gefunden wurde.

Was diesen Fehler ausnutzbar macht, ist, dass 3 SEH (Structured Exception Handler; dies ist try / außer auf niedriger Ebene) Ausnahmen für diesen Teil des Codes abfangen. Genauer gesagt, der 1. SEH wickelt den Stapel ab, sodass er wieder einen anderen JPEG-Marker analysiert und den Marker, der die Ausnahme ausgelöst hat, vollständig überspringt.

Ohne SEH hätte der Code gerade das gesamte Programm zum Absturz gebracht. Der Code überspringt also das COM-Segment und analysiert ein anderes Segment. Wir kehren also GpJpegDecoder::read_jpeg_marker()mit einem neuen Segment zurück und wenn der Code einen neuen Puffer zuweist:

.text:70E19A33  lea     eax, [esi+2] ; alloc_size = semgent_size + 2
.text:70E19A36  push    eax             ; dwBytes
.text:70E19A37  call    _GpMalloc@4     ; GpMalloc(x)

Das System hebt die Verknüpfung eines Blocks mit der freien Liste auf. Es kommt vor, dass Metadatenstrukturen durch den Inhalt des Bildes überschrieben wurden; Daher steuern wir die Verknüpfung mit kontrollierten Metadaten. Der folgende Code befindet sich irgendwo im System (ntdll) im Heap-Manager:

CPU Disasm
Address   Command                                  Comments
77F52CBF  MOV ECX,DWORD PTR DS:[EAX]               ; eax points to '0003' ; ecx = 0x33303030
77F52CC1  MOV DWORD PTR SS:[EBP-0B0],ECX           ; save ecx
77F52CC7  MOV EAX,DWORD PTR DS:[EAX+4]             ; [eax+4] points to '0004' ; eax = 0x34303030
77F52CCA  MOV DWORD PTR SS:[EBP-0B4],EAX
77F52CD0  MOV DWORD PTR DS:[EAX],ECX               ; write 0x33303030 to 0x34303030!!!

Jetzt können wir schreiben, was wir wollen, wo wir wollen ...

Neitsa
quelle
3

Da ich den Code von GDI nicht kenne, ist das Folgende nur Spekulation.

Nun, eine Sache, die mir in den Sinn kommt, ist ein Verhalten, das ich bei einigen Betriebssystemen bemerkt habe (ich weiß nicht, ob Windows XP dies hatte), wenn Sie new / malloczuweisen. Sie können tatsächlich mehr als Ihren RAM zuweisen, solange Sie schreiben nicht in diese Erinnerung.

Dies ist eigentlich ein Verhalten des Linux-Kernels.

Von www.kernel.org:

Seiten im linearen Adressraum des Prozesses befinden sich nicht unbedingt im Speicher. Beispielsweise werden Zuweisungen, die im Auftrag eines Prozesses vorgenommen werden, nicht sofort erfüllt, da der Speicherplatz nur innerhalb von vm_area_struct reserviert ist.

Um in den residenten Speicher zu gelangen, muss ein Seitenfehler ausgelöst werden.

Grundsätzlich müssen Sie den Speicher verschmutzen, bevor er tatsächlich auf dem System zugewiesen wird:

  unsigned int size=-1;
  char* comment = new char[size];

Manchmal wird keine echte RAM-Zuweisung vorgenommen (Ihr Programm verwendet immer noch keine 4 GB). Ich weiß, dass ich dieses Verhalten unter Linux gesehen habe, kann es jedoch jetzt in meiner Windows 7-Installation nicht replizieren.

Ausgehend von diesem Verhalten ist das folgende Szenario möglich.

Um diesen Speicher im RAM vorhanden zu machen, müssen Sie ihn verschmutzen (im Grunde Memset oder ein anderes Schreiben darauf):

  memset(comment, 0, size);

Die Sicherheitsanfälligkeit nutzt jedoch einen Pufferüberlauf und keinen Zuordnungsfehler.

Mit anderen Worten, wenn ich das hätte:

 unsinged int size =- 1;
 char* p = new char[size]; // Will not crash here
 memcpy(p, some_buffer, size);

Dies führt zu einem Schreib-nach-Puffer, da es kein 4-GB-Segment kontinuierlichen Speichers gibt.

Sie haben nichts in p eingefügt, um die gesamten 4 GB Speicher schmutzig zu machen, und ich weiß nicht, ob memcpyder Speicher auf einmal oder nur Seite für Seite schmutzig wird (ich denke, es ist Seite für Seite).

Schließlich wird der Stapelrahmen überschrieben (Stapelpufferüberlauf).

Eine weitere mögliche Sicherheitsanfälligkeit bestand darin, dass das Bild als Byte-Array im Speicher gehalten wurde (ganze Datei in Puffer lesen) und die Größe der Kommentare nur verwendet wurde, um nicht wichtige Informationen zu überspringen.

Beispielsweise

     unsigned int commentsSize = -1;
     char* wholePictureBytes; // Has size of file
     ...
     // Time to start processing the output color
     char* p = wholePictureButes;
     offset = (short) p[COM_OFFSET];
     char* dataP = p + offset;
     dataP[0] = EvilHackerValue; // Vulnerability here

Wie Sie bereits erwähnt haben, stürzt das Programm niemals ab, wenn der GDI diese Größe nicht zugewiesen hat.

MichaelCMS
quelle
4
Dies könnte bei einem 64-Bit-System der Fall sein, bei dem 4 GB keine große Rolle spielen (was den Addess-Speicher betrifft). In einem 32-Bit-System (sie scheinen ebenfalls anfällig zu sein) können Sie jedoch keine 4 GB Adressraum reservieren, da dies alles ist, was es gibt! Also malloc(-1U)wird ein sicherlich scheitern, zurückkehren NULLund memcpy()abstürzen.
Rodrigo
9
Ich glaube nicht, dass diese Zeile wahr ist: "Irgendwann wird sie in eine andere Prozessadresse geschrieben." Normalerweise kann ein Prozess nicht auf den Speicher eines anderen zugreifen. Siehe MMU-Vorteile .
Chue x
@ MMU Vorteile ja, Sie haben Recht. Ich sollte sagen, dass dies über die normalen Heap-Grenzen hinausgeht und den Stack-Frame überschreibt. Ich werde meine Antwort bearbeiten, danke für den Hinweis.
MichaelCMS