Questa vulnerabilità è stata sicuramente un enorme overflow .
Come può la scrittura di 0XFFFFFFFE byte (4 GB !!!!) non mandare in crash il programma?
Probabilmente lo farà, ma in alcune occasioni hai tempo per sfruttare prima che si verifichi il crash (a volte, puoi riportare il programma alla sua normale esecuzione ed evitare il crash).
Quando si avvia memcpy (), la copia sovrascriverà alcuni altri blocchi di heap o alcune parti della struttura di gestione dell'heap (ad es. Lista libera, lista occupata, ecc.).
Ad un certo punto la copia incontrerà una pagina non allocata e attiverà un AV (violazione di accesso) in scrittura. GDI + proverà quindi ad allocare un nuovo blocco nell'heap (vedere ntdll! RtlAllocateHeap ) ... ma le strutture dell'heap sono ora tutte incasinate.
A quel punto, creando con cura la tua immagine JPEG puoi sovrascrivere le strutture di gestione dell'heap con dati controllati. Quando il sistema tenta di allocare il nuovo blocco, probabilmente scollegherà un blocco (libero) dall'elenco libero.
I blocchi vengono gestiti con (in particolare) un puntatore lampeggiante (collegamento in avanti; il blocco successivo nell'elenco) e lampeggiante (collegamento all'indietro; il blocco precedente nell'elenco). Se controlli sia il flink che il blink, potresti avere una possibile WRITE4 (write What / Where condition) in cui controlli cosa puoi scrivere e dove puoi scrivere.
A quel punto è possibile sovrascrivere un puntatore a funzione (i puntatori SEH [Structured Exception Handlers] erano un obiettivo di scelta in quel momento nel 2004) e ottenere l'esecuzione del codice.
Vedi il post del blog Heap Corruption: A Case Study .
Nota: sebbene abbia scritto sull'exploitation utilizzando la freelist, un utente malintenzionato potrebbe scegliere un altro percorso utilizzando altri metadati dell'heap (i "metadati dell'heap" sono strutture utilizzate dal sistema per gestire l'heap; flink e blink fanno parte dei metadati dell'heap), ma lo sfruttamento di unlink è probabilmente il "più semplice". Una ricerca su Google per "sfruttamento dell'heap" restituirà numerosi studi su questo.
Questo scrive oltre l'area dell'heap e nello spazio di altri programmi e del sistema operativo?
Mai. I sistemi operativi moderni si basano sul concetto di spazio degli indirizzi virtuale, quindi ogni processo ha il proprio spazio degli indirizzi virtuale che consente di indirizzare fino a 4 gigabyte di memoria su un sistema a 32 bit (in pratica ne hai solo la metà nella terra degli utenti, il resto è per il kernel).
In breve, un processo non può accedere alla memoria di un altro processo (tranne se lo richiede al kernel tramite qualche servizio / API, ma il kernel controllerà se il chiamante ha il diritto di farlo).
Ho deciso di testare questa vulnerabilità questo fine settimana, in modo da poter avere una buona idea di cosa stava succedendo piuttosto che pura speculazione. La vulnerabilità ha ormai 10 anni, quindi ho pensato che fosse giusto scriverne, anche se non ho spiegato la parte dello sfruttamento in questa risposta.
Pianificazione
Il compito più difficile è stato trovare un Windows XP con solo SP1, come nel 2004 :)
Quindi, ho scaricato un'immagine JPEG composta solo da un singolo pixel, come mostrato di seguito (tagliato per brevità):
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
[...]
Un'immagine JPEG è composta da marcatori binari (che introducono segmenti). Nell'immagine sopra, FF D8
è il marker SOI (Start Of Image), mentre FF E0
, ad esempio, è un marker dell'applicazione.
Il primo parametro in un segmento marker (ad eccezione di alcuni marker come SOI) è un parametro di lunghezza a due byte che codifica il numero di byte nel segmento marker, incluso il parametro length ed escluso il marker a due byte.
Ho semplicemente aggiunto un marcatore COM (0x FFFE
) subito dopo il SOI, poiché i marcatori non hanno un ordine preciso.
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
[...]
La lunghezza del segmento COM è impostata 00 00
per attivare la vulnerabilità. Ho anche iniettato 0xFFFC byte subito dopo il marcatore COM con uno schema ricorrente, un numero di 4 byte in esadecimale, che diventerà utile quando si "sfrutta" la vulnerabilità.
Debug
Fare doppio clic sull'immagine attiverà immediatamente il bug nella shell di Windows (noto anche come "explorer.exe"), da qualche parte gdiplus.dll
in una funzione denominata GpJpegDecoder::read_jpeg_marker()
.
Questa funzione viene chiamata per ogni marker nell'immagine, semplicemente: legge la dimensione del segmento del marker, alloca un buffer la cui lunghezza è la dimensione del segmento e copia il contenuto del segmento in questo buffer appena allocato.
Ecco l'inizio della funzione:
.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
register punta alla dimensione del segmento ed edi
è il numero di byte rimasti nell'immagine.
Il codice procede quindi alla lettura della dimensione del segmento, iniziando dal byte più significativo (la lunghezza è un valore di 16 bit):
.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
E il byte meno significativo:
.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
Fatto ciò, la dimensione del segmento viene utilizzata per allocare un buffer, seguendo questo calcolo:
alloc_size = segment_size + 2
Questo viene fatto dal codice seguente:
.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)
Nel nostro caso, poiché la dimensione del segmento è 0, la dimensione allocata per il buffer è di 2 byte .
La vulnerabilità è subito dopo l'assegnazione:
.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
Il codice sottrae semplicemente la dimensione segment_size (la lunghezza del segmento è un valore di 2 byte) dalla dimensione dell'intero segmento (0 nel nostro caso) e finisce con un underflow intero: 0-2 = 0xFFFFFFFE
Il codice quindi controlla se ci sono byte rimasti da analizzare nell'immagine (il che è vero), quindi salta alla copia:
.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
Lo snippet sopra mostra che la dimensione della copia è 0xFFFFFFFE blocchi di 32 bit. Il buffer di origine è controllato (contenuto dell'immagine) e la destinazione è un buffer sull'heap.
Condizione di scrittura
La copia attiverà un'eccezione di violazione di accesso (AV) quando raggiunge la fine della pagina di memoria (potrebbe provenire dal puntatore di origine o dal puntatore di destinazione). Quando l'AV viene attivato, l'heap è già in uno stato vulnerabile perché la copia ha già sovrascritto tutti i seguenti blocchi dell'heap fino a quando non è stata rilevata una pagina non mappata.
Ciò che rende questo bug sfruttabile è che 3 SEH (Structured Exception Handler; questo è try / tranne a basso livello) stanno rilevando eccezioni su questa parte del codice. Più precisamente, il 1 ° SEH srotolerà lo stack in modo che torni ad analizzare un altro marker JPEG, saltando così completamente il marker che ha attivato l'eccezione.
Senza un SEH il codice avrebbe semplicemente bloccato l'intero programma. Quindi il codice salta il segmento COM e analizza un altro segmento. Quindi torniamo aGpJpegDecoder::read_jpeg_marker()
con un nuovo segmento e quando il codice alloca un nuovo buffer:
.text:70E19A33 lea eax, [esi+2] ; alloc_size = semgent_size + 2
.text:70E19A36 push eax ; dwBytes
.text:70E19A37 call _GpMalloc@4 ; GpMalloc(x)
Il sistema scollegherà un blocco dall'elenco libero. Succede che le strutture dei metadati siano state sovrascritte dal contenuto dell'immagine; quindi controlliamo lo scollegamento con metadati controllati. Il codice seguente da qualche parte nel sistema (ntdll) nel gestore di heap:
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!!!
Ora possiamo scrivere quello che vogliamo, dove vogliamo ...