it-swarm.com.de

Wie funktioniert die JPEG of Death-Sicherheitsanfälligkeit?

Ich habe über einen älteren Exploit gegen GDI + unter Windows XP gelesen und Windows Server 20 das JPEG des Todes für ein Projekt genannt, an dem ich arbeite.

Der Exploit wird unter folgendem Link gut erklärt: http://www.infosecwriters.com/text_resources/pdf/JPEG.pdf

Grundsätzlich enthält eine JPEG-Datei einen Abschnitt namens COM, der ein (möglicherweise leeres) Kommentarfeld enthält, und einen Zwei-Byte-Wert, der die Größe von COM enthält. Wenn keine Kommentare vorhanden sind, beträgt die Größe 2. Der Reader (GDI +) liest die Größe, subtrahiert zwei und weist einen Puffer mit der entsprechenden Größe zu, um die Kommentare in den Heap zu kopieren. Bei dem Angriff wird ein Wert von 0 In das Feld eingegeben. GDI + subtrahiert 2, Was zu einem Wert von -2 (0xFFFe) führt, der durch memcpy in die vorzeichenlose Ganzzahl 0XFFFFFFFE Konvertiert wird.

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 einen Zeiger auf nicht zugewiesenen Speicher auf dem Heap zurückgeben sollte. Wie kann das Schreiben von 0XFFFFFFFE Bytes (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?

Nach meinem Verständnis kopiert memcpy einfach n Zeichen vom Ziel in die Quelle. In diesem Fall sollte sich die Quelle auf dem Stapel befinden, das Ziel auf dem Heap, und n ist 4GB.

94
Rafa

Diese Sicherheitsanfälligkeit war definitiv ein Heap-Ü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, dies auszunutzen, bevor der Absturz auftritt (manchmal können Sie die normale Ausführung des Programms wiederherstellen und den Absturz vermeiden).

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

Irgendwann stößt die Kopie auf eine nicht zugewiesene Seite und löst beim Schreiben eine AV (Access Violation) 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-Bildes 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 (insbesondere) mit einem blinkenden (Vorwärtslink; der nächste Block in der Liste) und einem blinkenden (Rückwärtslink; der vorherige Block in der Liste) Zeiger verwaltet. Wenn Sie sowohl das Blinken als auch das Blinken steuern, haben Sie möglicherweise eine WRITE4-Bedingung (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 [Structured Exception Handlers] Zeiger waren zu diesem Zeitpunkt im Jahr 2004 ein bevorzugtes Ziel) und Code-Ausführung erhalten.

Siehe Blogeintrag Heap Corruption: A Case Study.

Hinweis: Obwohl ich über die Ausnutzung mithilfe der Freelist geschrieben habe, kann ein Angreifer mithilfe anderer Heap-Metadaten einen anderen Pfad auswählen ("Heap-Metadaten" sind Strukturen, die vom System zum Verwalten des Heaps verwendet werden; Flink und Blink sind Teil der Heap-Metadaten) Die Unlink-Nutzung 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 über einen eigenen virtuellen Adressraum verfügt, 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 (es sei denn, er fragt den Kernel über einen Dienst/eine API danach, der Kernel prüft jedoch, ob der Aufrufer dazu berechtigt ist).


Ich habe mich entschlossen, diese Sicherheitsanfälligkeit an diesem Wochenende zu testen, damit wir eine gute Vorstellung davon bekommen, was vor sich geht, anstatt reine Spekulation. 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 im Jahr 2004 war :)

Dann habe ich ein JPEG-Bild heruntergeladen, das nur aus einem einzigen Pixel besteht (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 Markierungen (die Segmente einführen). In der obigen Abbildung ist FF D8 Die Markierung SOI (Start Of Image)), während FF E0 Beispielsweise eine Anwendungsmarkierung 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 auf 00 00 Festgelegt, um die Sicherheitsanfälligkeit auszulösen. Ich habe auch 0xFFFC-Bytes direkt nach dem COM-Marker mit einem wiederkehrenden Muster, einer 4-Byte-Zahl in hexadezimaler Schreibweise, injiziert, was nützlich sein wird, wenn die Sicherheitsanfälligkeit "ausgenutzt" wird.

Debugging

Ein Doppelklick auf das Bild löst sofort den Fehler in der Windows-Shell (auch "Explorer.exe" genannt) aus, der sich irgendwo in gdiplus.dll In einer Funktion namens GpJpegDecoder::read_jpeg_marker() befindet.

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

Das Register eax zeigt auf die Segmentgröße und edi gibt 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 niederwertigste 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 gemäß dieser Berechnung zuzuweisen:

alloc_size = segment_size + 2

Dies geschieht mit dem 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    [email protected]     ; GpMalloc(x)

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

Die Sicherheitsanfälligkeit tritt direkt nach der Zuweisung auf:

.text:70E19A37  call    [email protected]     ; 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 segment_size-Größe (Segmentlänge ist ein 2-Byte-Wert) von der gesamten Segmentgröße (in unserem Fall 0) und endet mit einem ganzzahligen Unterlauf: - 2 = 0xFFFFFFFE

Der Code prüft dann, ob noch Bytes im Bild vorhanden 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 Kopiengröße 0xFFFFFFFE 32-Bit-Chunks beträgt. Der Quellpuffer wird gesteuert (Inhalt des Bildes) und das Ziel ist ein Puffer auf dem Heap.

Schreibbedingung

Die Kopie löst eine Zugriffsverletzungsausnahme (AV) aus, wenn sie das Ende der Speicherseite erreicht (dies kann entweder vom Quellzeiger oder vom Zielzeiger stammen). 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/except auf niedriger Ebene) Ausnahmen für diesen Teil des Codes abfangen. Genauer gesagt, der 1. SEH wird den Stapel abwickeln, damit er wieder einen anderen JPEG-Marker parsen kann, wodurch der Marker, der die Ausnahme ausgelöst hat, vollständig übersprungen wird.

Ohne ein SEH hätte der Code nur das gesamte Programm zum Absturz gebracht. Der Code überspringt also das COM-Segment und analysiert ein anderes Segment. Also kehren wir mit einem neuen Segment zu GpJpegDecoder::read_jpeg_marker() 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    [email protected]     ; 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. Also kontrollieren 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 ...

94
Neitsa

Da ich den Code von GDI nicht kenne, handelt es sich im Folgenden nur um Spekulationen.

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), als Sie mit new/malloc allokierten, Sie können tatsächlich mehr als allokieren Ihr RAM, solange Sie nicht in diesen Speicher schreiben.

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 Zuordnungen, die für einen Prozess vorgenommen wurden, nicht sofort erfüllt, da der Speicherplatz nur innerhalb des vm_area_struct reserviert wird.

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

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

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

Manchmal nimmt es in RAM (Ihr Programm verwendet immer noch keine 4 GB) keine echte Zuordnung vor. Ich weiß, dass ich dieses Verhalten unter Linux gesehen habe, kann es aber nicht replizieren jetzt auf meiner Windows 7 Installation.

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

Damit dieser Speicher in RAM) vorhanden ist, müssen Sie ihn schmutzig machen (im Grunde genommen Memset oder ein anderer Schreibzugriff):

  memset(comment, 0, size);

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

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 Schreiben nach dem Puffern, da es kein 4-GB-Segment mit kontinuierlichem Speicher gibt.

Sie haben nichts in p geschrieben, um die gesamten 4 GB Speicher zu verschmutzen, und ich weiß nicht, ob memcpy den Speicher auf einmal verschmutzt oder nur Seite für Seite (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 (gesamte Datei in den Puffer einlesen) und die Größe der Kommentare nur zum Überspringen nicht wichtiger Informationen verwendet wurde.

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, wird das Programm niemals abstürzen, wenn GDI diese Größe nicht zugewiesen hat.

3
MichaelCMS