Plus grand qu'il n'y paraît : une bombe de décompression dans l'en-tête PNG
Le défi
Chaque nuit à 02h00, le générateur de vignettes s'écroule sur un seul fichier et emporte tout le pool de workers. Le fichier fait 46 Ko sur le disque, c'est un PNG valide, il s'ouvre dans un navigateur, et le journal d'envoi l'enregistre comme une icône de 64 par 64. Rien dans la taille sur disque n'explique qu'une machine tombe à court de mémoire. Un PNG porte ses dimensions en pixels dans son en-tête, bien visibles, longtemps avant qu'un décodeur ne touche aux données compressées. Lisez-les dans le vidage et donnez la vraie largeur. Les huit premiers octets sont surlignés, survoler un octet affiche son offset et son caractère ASCII, et la référence de signatures sous le vidage nomme chaque bloc.
Ce que tu vas apprendre
- Repérer un bloc dans un PNG grâce à son type ASCII de quatre lettres
- Lire un entier big-endian de 4 octets dans un vidage hexadécimal
- Expliquer pourquoi la taille du fichier sur le disque ne dit rien de la taille décodée
- Reconnaître une bombe de décompression comme une attaque par épuisement de ressources, pas un fichier malformé
- Distinguer les métadonnées écrites par un outil de la structure que traite un décodeur
Compétences testées
Prérequis
- Conversion hexadécimal vers décimal
- Ce qu'est un en-tête de fichier
Comment ça marche
Un PNG est une signature suivie d'une succession de blocs, et chaque bloc a la même forme : une longueur de 4 octets, un type ASCII de 4 octets, les données, puis un checksum de 4 octets. Cette régularité est ce qui permet de naviguer dans un vidage sans analyseur. Trouvez les quatre lettres qui nomment un bloc et vous savez exactement où commencent ses données.
Le premier bloc est toujours IHDR, et ses huit premiers octets sont la largeur et la hauteur, chacune un entier big-endian de 32 bits. Big-endian signifie que l'octet de poids fort est écrit en premier, donc 00 00 75 30 vaut 0x7530 et non 0x3075. Chaque décodeur lit ces huit octets avant tout le reste, car il doit allouer le tampon de sortie avant de pouvoir y décompresser les données.
Cette allocation est l'attaque. La taille compressée et la taille décodée n'ont aucun rapport : une seule couleur uniforme sur une immense image se compresse en quelques kilo-octets et se développe en gigaoctets. Tout service qui accepte une image, vérifie que l'envoi est petit, puis la transmet à une bibliothèque a déjà perdu, parce que la bibliothèque dimensionne son tampon à partir de l'en-tête et que l'en-tête est contrôlé par l'attaquant.
Erreurs fréquentes
- Répondre 64. Le
64x64visible dans la colonne ASCII se trouve dans un bloc de commentairetEXt. C'est une note, et le journal d'envoi l'a recopiée sans regarder l'en-tête. - Répondre 20000. C'est la hauteur. La largeur vient en premier dans
IHDR. - Lire les octets en little-endian. Les entiers PNG sont en big-endian, donc
00 00 75 30vaut 30000, et non 807534592. - Commencer à compter à l'offset 8. Les offsets 8 à 11 sont la longueur du bloc et 12 à 15 le type. La largeur commence à l'offset 16.
- Lire le bloc pHYs. Il contient les pixels par mètre, ce qui est la résolution, pas la taille.
Comment s'en protéger
Décidez si le fichier doit être décodé en vous basant sur l'en-tête, avant de le décoder.
- Analysez vous-même
IHDRet rejetez tout ce qui dépasse un budget de pixels. Le nombre total de pixels compte plus que chaque dimension prise seule : 30000 par 20000 fait 600 mégapixels, et un plafond de 50 mégapixels aurait arrêté cela dès l'entrée. - Fixez les limites que votre bibliothèque d'images propose déjà. La plupart exposent un nombre maximal de pixels et refusent les fichiers surdimensionnés si vous l'activez.
- Faites ce travail dans un processus avec une limite de mémoire stricte, pour qu'un seul mauvais fichier tue un seul worker au lieu de tout le pool, et jamais dans le thread de la requête.
- Traitez chaque bloc de texte comme une décoration non fiable. Les dimensions enregistrées au moment de l'envoi doivent venir de votre propre analyse, pas d'un commentaire du fichier.