J'ai lu sur un ancien exploit contre GDI + sur Windows XP et Windows Server 2003 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, 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
sur le terrain. GDI + soustrait 2
, conduisant à une valeur -2 (0xFFFe)
dont la valeur est convertie en 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 écrire0XFFFFFFFE
octets (4GB
!!!!) peut-elle ne pas planter le programme? Cela écrit-il 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 n
caractères de la destination vers la source. Dans ce cas, la source doit être sur la pile, la destination sur le tas et l' n
est 4GB
.
malloc
taille ed n'est que de 2 octets au lieu de0xFFFFFFFE
. Cette taille énorme n'est utilisée que pour la taille de la copie, pas pour la taille d'allocation.Réponses:
Cette vulnérabilité était certainement un débordement de tas .
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.
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é):
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.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:
eax
register pointe vers la taille du segment etedi
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):
Et l'octet le moins significatif:
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:
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:
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:
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: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:
Maintenant, nous pouvons écrire ce que nous voulons, où nous voulons ...
la source
Puisque je ne connais pas le code de GDI, ce qui est ci-dessous n'est que 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 en avait) lors de l'allocation avec de nouveaux /
malloc
, vous pouvez en fait allouer plus que votre RAM, tant que vous n'écrivez pas dans cette mémoire.C'est en fait un comportement du noyau Linux.
Depuis www.kernel.org:
Pour entrer dans la mémoire résidente, une erreur de page doit être déclenchée.
Fondamentalement, vous devez rendre la mémoire sale avant qu'elle ne soit réellement allouée sur le système:
Parfois, il ne fera pas réellement une allocation réelle en RAM (votre programme n'utilisera toujours pas 4 Go). Je sais que j'ai vu ce comportement sur un Linux, mais je ne peux cependant pas le répliquer maintenant sur mon installation Windows 7.
À partir de ce comportement, le scénario suivant est possible.
Afin de rendre cette mémoire existante dans la RAM, vous devez la rendre sale (essentiellement memset ou une autre écriture dessus):
Cependant, la vulnérabilité exploite un débordement de tampon, pas un échec d'allocation.
En d'autres termes, si je devais avoir ceci:
Cela conduira à une écriture après tampon, car il n'existe pas de segment de 4 Go de mémoire continue.
Vous n'avez rien mis dans p pour salir les 4 Go de mémoire, et je ne sais pas si la
memcpy
mémoire est sale en même temps, ou juste page par page (je pense que c'est page par page).Finalement, il 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 (lecture du fichier entier dans la mémoire tampon), et la taille des commentaires était utilisée juste pour sauter des informations non vitales.
Par exemple
Comme vous l'avez mentionné, si le GDI n'alloue pas cette taille, le programme ne plantera jamais.
la source
malloc(-1U)
échouera sûrement, reviendraNULL
etmemcpy()
plantera.