web-dev-qa-db-fra.com

Comment fonctionne la vulnérabilité JPEG of Death?

J'ai lu un ancien exploit contre GDI + sur Windows XP et Windows Server 20 appelé le JPEG de la mort pour un projet sur lequel je travaille.

L'exploit est bien expliqué dans le lien suivant: http://www.infosecwriters.com/text_resources/pdf/JPEG.pdf

Fondamentalement, un fichier JPEG contient une section appelée COM contenant un champ de commentaire (éventuellement vide) et une valeur de deux octets contenant la taille de COM. S'il n'y a aucun commentaire, la taille est 2. Le lecteur (GDI +) lit la taille, en soustrait deux et alloue un tampon de la taille appropriée pour copier les commentaires dans le tas. L'attaque consiste à placer une valeur de 0 Dans le champ. GDI + soustrait 2, Conduisant à une valeur de -2 (0xFFFe) qui est convertie en l'entier non signé 0XFFFFFFFE Par memcpy .

Exemple de code:

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

Observez que malloc(0) sur la troisième ligne doit renvoyer un pointeur vers la mémoire non allouée sur le tas. Comment l'écriture de 0XFFFFFFFE Octets (4GB !!!!) peut ne pas planter le programme? Est-ce que cela écrit au-delà de la zone de tas et dans l'espace d'autres programmes et du système d'exploitation? Que se passe-t-il alors?

Si je comprends bien memcpy, il copie simplement les caractères n de la destination à la source. Dans ce cas, la source doit être sur la pile, la destination sur le tas et n est 4GB.

94
Rafa

Cette vulnérabilité était définitivement un débordement de tas .

Comment l'écriture d'octets 0XFFFFFFFE (4 Go !!!!) peut ne pas planter le programme?

C'est probablement le cas, mais à certaines occasions, vous avez le temps d'exploiter avant le crash (parfois, vous pouvez remettre le programme à son exécution normale et éviter le crash).

Lorsque memcpy () démarre, la copie remplacera certains autres blocs de tas ou certaines parties de la structure de gestion de tas (par exemple, liste libre, liste occupée, etc.).

À un moment donné, la copie rencontrera une page non allouée et déclenchera un AV (violation d'accès) lors de l'écriture. GDI + essaiera alors d'allouer un nouveau bloc dans le tas (voir ntdll! RtlAllocateHeap ) ... mais les structures de tas sont maintenant toutes gâchées.

À ce stade, en créant soigneusement votre image JPEG, vous pouvez remplacer les structures de gestion de tas par des données contrôlées. Lorsque le système essaie d'allouer le nouveau bloc, il dissociera probablement un bloc (gratuit) de la liste gratuite.

Les blocs sont gérés avec (notamment) des pointeurs flink (lien en avant; le bloc suivant dans la liste) et blink (lien en arrière; le bloc précédent dans la liste). Si vous contrôlez à la fois le clignotement et le clignotement, vous pourriez avoir une possibilité WRITE4 (écrire la condition Quoi/Où) où vous contrôlez ce que vous pouvez écrire et où vous pouvez écrire.

À ce stade, vous pouvez remplacer un pointeur de fonction ( les pointeurs SEH [Structured Exception Handlers] étaient une cible de choix à l'époque en 2004) et obtenir l'exécution de code.

Voir l'article de blog Corruption de tas: étude de cas.

Remarque: bien que j'aie écrit sur l'exploitation à l'aide de la liste de diffusion, un attaquant pourrait choisir un autre chemin en utilisant d'autres métadonnées de tas (les "métadonnées de tas" sont des structures utilisées par le système pour gérer le tas; flink et blink font partie des métadonnées de tas), mais l'exploitation non liée est probablement la plus "facile". Une recherche google pour "exploitation en tas" renverra de nombreuses études à ce sujet.

Est-ce que cela écrit au-delà de la zone de tas et dans l'espace d'autres programmes et du système d'exploitation?

Jamais. Les systèmes d'exploitation modernes sont basés sur le concept d'espace d'adressage virtuel, de sorte que chaque processus possède son propre espace d'adressage virtuel qui permet d'adresser jusqu'à 4 gigaoctets de mémoire sur un système 32 bits (en pratique, vous n'en avez obtenu que la moitié dans l'espace utilisateur, le reste est pour le noyau).

En bref, un processus ne peut pas accéder à la mémoire d'un autre processus (sauf s'il le demande au noyau via un service/API, mais le noyau vérifiera si l'appelant a le droit de le faire).


J'ai décidé de tester cette vulnérabilité ce week-end, afin que nous puissions avoir une bonne idée de ce qui se passait plutôt que de la pure spéculation. La vulnérabilité a maintenant 10 ans, j'ai donc pensé qu'il était correct d'écrire à ce sujet, même si je n'ai pas expliqué la partie exploitation dans cette réponse.

Planification

La tâche la plus difficile a été de trouver un Windows XP avec seulement SP1, comme c'était en 2004 :)

Ensuite, j'ai téléchargé une image JPEG composée uniquement d'un seul pixel, comme illustré ci-dessous (coupé pour plus de concision):

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
[...]

Une image JPEG est composée de marqueurs binaires (qui introduisent des segments). Dans l'image ci-dessus, FF D8 Est le marqueur SOI (Start Of Image)), tandis que FF E0, Par exemple, est un marqueur d'application.

Le premier paramètre d'un segment de marqueur (à l'exception de certains marqueurs comme SOI) est un paramètre de longueur à deux octets qui code le nombre d'octets dans le segment de marqueur, y compris le paramètre de longueur et à l'exclusion du marqueur à deux octets.

J'ai simplement ajouté un marqueur COM (0x FFFE) juste après le SOI, car les marqueurs n'ont pas d'ordre strict.

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 longueur du segment COM est définie sur 00 00 Pour déclencher la vulnérabilité. J'ai également injecté 0xFFFC octets juste après le marqueur COM avec un motif récurrent, un nombre de 4 octets en hexadécimal, qui deviendra pratique lors de "l'exploitation" de la vulnérabilité.

Débogage

Double-cliquez sur l'image déclenchera immédiatement le bogue dans le shell Windows (aka "Explorer.exe"), quelque part dans gdiplus.dll, Dans une fonction nommée GpJpegDecoder::read_jpeg_marker().

Cette fonction est appelée pour chaque marqueur dans l'image, elle simplement: lit la taille du segment de marqueur, alloue un tampon dont la longueur est la taille du segment et copie le contenu du segment dans ce tampon nouvellement alloué.

Voici le début de la fonction:

.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 registre pointe sur la taille du segment et edi est le nombre d'octets restant dans l'image.

Le code procède ensuite à la lecture de la taille du segment, en commençant par l'octet le plus significatif (la longueur est une valeur de 16 bits):

.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

Et l'octet le moins significatif:

.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

Une fois cela fait, la taille du segment est utilisée pour allouer un tampon, en suivant ce calcul:

alloc_size = segment_size + 2

Cela se fait par le code ci-dessous:

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

Dans notre cas, comme la taille du segment est 0, la taille allouée pour le tampon est de 2 octets .

La vulnérabilité est juste après l'allocation:

.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

Le code soustrait simplement la taille segment_size (la longueur du segment est une valeur de 2 octets) de la taille du segment entier (0 dans notre cas) et se termine par un sous-dépassement d'entier: 0 - 2 = 0xFFFFFFFE

Le code vérifie ensuite s'il reste des octets à analyser dans l'image (ce qui est vrai), puis passe à la copie:

.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

L'extrait ci-dessus montre que la taille de la copie est des morceaux de 32 bits 0xFFFFFFFE. Le tampon source est contrôlé (contenu de l'image) et la destination est un tampon sur le tas.

Condition d'écriture

La copie déclenchera une exception de violation d'accès (AV) lorsqu'elle atteindra la fin de la page de mémoire (cela pourrait provenir du pointeur source ou du pointeur de destination). Lorsque l'AV est déclenché, le segment de mémoire est déjà dans un état vulnérable car la copie a déjà remplacé tous les blocs de segment de mémoire suivants jusqu'à ce qu'une page non mappée soit rencontrée.

Ce qui rend ce bug exploitable, c'est que 3 SEH (Structured Exception Handler; c'est try/except à bas niveau) interceptent des exceptions sur cette partie du code. Plus précisément, le 1er SEH déroulera la pile afin de pouvoir analyser un autre marqueur JPEG, ignorant ainsi complètement le marqueur qui a déclenché l'exception.

Sans SEH, le code aurait juste écrasé tout le programme. Le code ignore donc le segment COM et analyse un autre segment. On revient donc à GpJpegDecoder::read_jpeg_marker() avec un nouveau segment et quand le code alloue un nouveau buffer:

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

Le système dissociera un bloc de la liste gratuite. Il arrive que les structures de métadonnées soient écrasées par le contenu de l'image; nous contrôlons donc la dissociation avec des métadonnées contrôlées. Le code ci-dessous quelque part dans le système (ntdll) dans le gestionnaire de tas:

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!!!

Maintenant, nous pouvons écrire ce que nous voulons, où nous voulons ...

94
Neitsa

Comme je ne connais pas le code de GDI, ce qui est ci-dessous n'est que de la spéculation.

Eh bien, une chose qui me vient à l'esprit est un comportement que j'ai remarqué sur certains systèmes d'exploitation (je ne sais pas si Windows XP l'avait) lors de l'allocation avec le nouveau/malloc, vous pouvez réellement allouer plus de votre RAM, tant que vous n'écrivez pas dans cette mémoire.

Il s'agit en fait d'un comportement du noyau Linux.

De www.kernel.org:

Les pages de l'espace d'adressage linéaire du processus ne résident pas nécessairement en mémoire. Par exemple, les allocations faites au nom d'un processus ne sont pas satisfaites immédiatement car l'espace est simplement réservé dans vm_area_struct.

Pour accéder à la mémoire résidente, une erreur de page doit être déclenchée.

Fondamentalement, vous devez salir la mémoire avant qu'elle ne soit réellement allouée sur le système:

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

Parfois, il ne fera pas réellement d'allocation réelle dans RAM (votre programme n'utilisera toujours pas 4 Go). Je sais que j'ai vu ce comportement sur Linux, mais je ne peux cependant pas le reproduire maintenant sur mon installation de Windows 7.

À partir de ce comportement, le scénario suivant est possible.

Afin de rendre cette mémoire existante dans RAM vous devez la rendre sale (essentiellement memset ou une autre écriture):

  memset(comment, 0, size);

Cependant, la vulnérabilité exploite un débordement de tampon, pas un échec d'allocation.

En d'autres termes, si je devais avoir ceci:

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

Cela entraînera une écriture après tampon, car il n'y a pas de segment de 4 Go de mémoire continue.

Vous n'avez rien mis dans p pour salir l'ensemble des 4 Go de mémoire, et je ne sais pas si memcpy rend la mémoire sale à la fois, ou juste page par page (je pense que c'est page par page ).

Finalement, cela finira par écraser le cadre de la pile (Stack Buffer Overflow).

Une autre vulnérabilité plus possible était si l'image était conservée en mémoire sous forme de tableau d'octets (lire le fichier entier dans le tampon), et la taille des commentaires était utilisée simplement pour ignorer les informations non vitales.

Par exemple

     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

Comme vous l'avez mentionné, si le GDI n'a pas alloué cette taille, le programme ne se bloquera jamais.

3
MichaelCMS