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