Explanations

Les images PNG, comment ça marche ? Comment corriger manuellement les erreurs ?

Hello,

Dans cet article je vais vous parler de mon format préféré: les PNG (Portable Networks Graphics). L’extension PNG est un format d’images très commun au même titre que les JPEG, Tiff et autres joyeusetés. J’ai écris cet article avec l’aide de la RFC 2083 qui définit le standard PNG, avec Wikipédia et avec ce que j’ai appris au fil du temps et des challenges que j’ai réussi (ou pas d’ailleurs). Vu que j’y met mon expérience personnelle, c’est sans prétentions hein ! Je vais essayer de vous guider étapes par étapes dans la compréhension du format et la correction des problèmes.

1. Le format du fichier

Commençons par le commencement (\0/), le magic number. Chaque fichier normalement constitué dispose de ce que l’on appelle un “magic number”. Idéalement, ce magic number est constitué d’une série hexadécimale permettant d’identifier à coup sûr le type du fichier, car non, l’extension d’un fichier ne sert absolument pas a déterminer quel type de fichier c’est 🙂

Pour le cas d’un PNG, le magic number est le suivant: 89 50 4E 47 0D 0A 1A 0A, ou pour les intimes: 

La suite du fichier est constituée de Chunks.

2. Les chunks, késako ?

Une image PNG, c’est très structuré, faut pas croire ! Ce que l’on appelle un “chunk”, c’est tout simplement une “section” voire une “catégorie” définissant une certaine partie du fichier PNG. Par exemple, le chunk IEND définit la fin du fichier PNG et se présente sous sa forme hexadécimale complète: 00 00 00 49 45 4E 44 AE 42 60 82.

Les différents types de chunks

Il existe pléthore de chunks, allant du plus important pour le bon fonctionnement du shmilblik, au plus inutile comme iTXt qui sert à rajouter des commentaires en texte international directement dans le fichier:

  • acTL pour APNG animation control qui est utile dans les fichiers APNG (Animated Portable Network Graphics) qui est l’équivalent GIF mais avec le format PNG et des images de bien plus haute qualité.
  • bKGD  Pour background color,  ce chunk permet de définir une couleur d’arrière plan au cas où des parties soient transparentes
  • cHRM définit les chromaticités primaires de l’image (cf 4.2.2 de la RFC)
  • fcTL Sert dans le format APNG pour contrôler les frames (1 frame = 1 image).
  • gAMA définit le gamma de l’image
  • IEND Définit la fin du fichier PNG
  • IHDR C’est le chunk principal de l’image. Il permet de définir plein d’aspects de l’image, comme le hauteur, la largeur, la profondeur des couleurs, la méthode de compression de l’image, la méthode de filtre ou encore si il y a de l’entrelacement.
  • IDAT Ce chunk contient les données compressées relatives au contenu de l’image. Il se présente sous la forme de données compressées avec ZLIB (c’est d’ailleurs pourquoi vous avez des archives zlib dans le résultat d’un binwalk sur un PNG. N’essayez pas de les décompresser, ça ne vous servira a rien 🙂 )
  • oFFs Définit l’offset de l’image (peu utilisé)
  • pHYs Pour physical Pixel dimensions
  • PLTE Définit la palette de couleurs RBG utilisée, 255 valeurs par couleurs.
  • sBIT Sert à stocker le nombre de bits utilisé dans l’image d’origine pour chaque couleurs. Certains décodeurs PNG n’utilisent qu’une certaine partie de la valeur des couleurs si elle est trop complexe (i.e codée sur trop de bits)
  • sCAL Similaire au pHYs, mais est utilisé dans différentes conditions pour des images plus complexes
  • sRGB Est un indicateur de la profondeur des couleurs
  • sTER Stereo image indicator (?)
  • tEXt/zTXt/iTXt Sont des chunks de texte. Ils agissent tous de la même méthode sur le fichier mais ont des utilisations différentes. Retenez que c’est pour stocker, et qu’on peut décider de compresser le texte avec ZLIB, comme pour le chunk IDAT.
  • tIME Sert à définit la date de dernière midification
  • tRNS Définit le taux de transparence de l’image
  • vpAg Virtual Page (?)

Bon comme vous vous en doutez, il y en a encore plus, mais je peux pas m’amuser à tous les citer, ça prendrait trop de temps et ce n’est pas le but ici. 

Komment sé fé un chunque ?

Attardons nous rapidement sur le nom de ces chunks, car le fait qu’il y ait des majuscules n’est pas anodin ! Le nom du chunk désigne sa fonction, et le fait que les lettres soient en majuscule/minuscule désigne une fonction et un attribut bien particulier (majuscule = 0 et minuscule = 1 pour que vous compreniez la suite).

Analysons le cas théorique d’un chunk nommé “bLOb” (qui n’existe pas, c’est pour l’exemple):

bLOb the magnificient

Donc, le chunk commence par “b”, qui est une lettre minuscule, signifie que le chunk est “auxiliaire”, donc par conséquent il n’est pas de nécessité critique pour l’image.

Ensuite, on trouve la lettre “L” en majuscule, ce qui signifie que le chunk est public, donc reconnu (normalement) par tous les décodeurs PNG. Vous pouvez très bien créer votre propre chunk PNG ! mais cette lettre devra être en minuscule. il sera dit “privé”.

On passe à la lettre “O” en majuscules. Pour le moment elle désigne une feature non implémentée dans PNG, et cette lettre doit TOUJOURS être en majuscules.

Enfin, la lettre “b” en minuscule signifie que le chunk est “safe to copy”, ce qui équivaut à une autorisation. Cet attribut sert aux logiciels éditeurs de PNG pour leur signifier si un chunk non connu peut être copié sans soucis. Normalement, si un éditeur de PNG ne connait pas un chunk “critique” (première lettre en majuscule) et n’a pas le droit de le copier, alors il doit refuser le traitement de l’image et faire un rapport de bug.

Voilà pour l’explication des noms farfelus des chunks. Passons maintenant à la constitution d’un chunk ! (Hexadecimal intensifies boi)

Y’a de l’hexa dans l’air

Passons maintenant à la constitution d’un chunk. Tous les chunks n’ont pas la même longueur de données. Certains ne sont constitués que de 1 bit de donnée, d’autres peuvent prendre les 3/4 de la longueur du fichier complet. Donc dans cette partie nous allons voir de quoi est constitué un chunk standard. Il faut noter qu’un chunk est limité à (231)-1 octets.

Un chunk c’est constitué de 4 parties distinctes:

  • La taille de la donnée stockée dans le chunk (sur 4 octets), 0 étant valide. Le checksum n’est pas compté dans la taille du chunk.
  • Le type de chunk: c’est le nom qui identifie le chunk (sur 4 octets), composé de lettres ASCII (A-Z et a-z) ou de nombre décimaux correspondants ( entre 65-90 et 97-122).
  • Les données contenues dans le chunk (peuvent être de longueur 0)
  • Le CRC (Cyclic Redundancy Check) ou checksum, calculé a l’aide d’un algorithme. Cette valeur calculée sert à déterminer si il y a eu des erreurs lors de transmission. Elle s’appuie sur les valeurs précédentes du chunk : les données et le type de chunk.

Sur ces 4 points, il ne nous reste plus qu’a détailler le checksum et son algorithme.

La fonction polynomiale permettant de le calculer est la suivante:

 x32+x26+x23+x22+x16+x12+x11+x10+x8+x7+x5+x4+x2+x+1

Cette fonction polynomiale est utilisée sur un registre de 32 bits, tous initialisés à 1 au démarrage. On prend donc les données du chunk par blocs de 32 bits, et on calcule au fur et a mesure grâce au polynôme.

Voici un exemple concret sur un chunk, le cas d’un chunk gAMA:

Après avoir ouvert l’image sur notre éditeur hexadécimal favori (moi c’est HxD sur Windows), voici ce que l’on voit:

Soit en Hexadécimal:

On peut donc découper le chunk de cette manière:

Observons un moment l’hexadécimal:

  • 00 00 00 04 : C’est la longueur de la donnée stockée dans le chunk, soit ici 4 octets de données.
  • 67 41 4D 41 : C’est le nom du chunk, ici gAMA.
  • 00 00 B1 8F : c’est la donnée stockée dans le chunk.
  • 0B FC 61 05 : c’est le CRC ou checksum correspondant à la donnée.

Calcul du CRC:

Voici un exemple de script python permettant de calculer le CRC “0B FC 61 05”  (je ne vais pas détailler l’utilisation de fonction polynomiale ici car c’est un standard bien détaillé sur internet (ici par exemple) ). 

import struct, zlib
def test_chunk(crc, tag, data=''):
    checksum = zlib.crc32(data) & 0xffffffff
    print(hex(checksum))
    calc_crc = checksum
    print(crc == calc_crc)

test_chunk(0x0BFC6105,"sRGB", struct.pack("!I",0x67414D41) + struct.pack("!I",0x0000B18F) )

En retour, l’algorithme nous dit si le CRC entré est identique à celui calculé (ici c’est le cas).

Pour rappel, le checksum est calculé avec le type de chunk et la donnée du chunk (d’où le passage en paramètre de l’hexadécimal 67 41 4D 41 00 00 B1 8F uniquement).

Oké daque, mais qu’est ce qui est nécessaire et suffisant pour constituer une image ?

Très bonne question ! et la réponse est simple, une image basique est constituée de 4 choses:

  • Le magic number sinon le fichier n’est pas identifiable.
  • Un chunk IHDR définissant les propriétés principales de l’image (longueur, largeur etc.).
  • Un chunk IDAT contenant de la donnée (ou non) pour remplir l’image.
  • Un chunk IEND pour dire “Oh ééé ! stop le fichier s’arrête là !”

3. Cas concrets souvent repérés en stéganographie

Dans ce chapitre on va parler stéganographie (bon d’accord les 3/4 c’est plus du forensic que de le stega, mais c’est certifié  sans guessing), alors si vous n’aimez pas ça, accrochez vous mdr. On va détailler des cas pratiques sur des types de challenges récurrents en forensics/stéga, mais aussi des cas de la vraie vie véritable. Je vais aussi vous présenter un tool magique pour le traitement des PNGs, il est gold celui-là no joke, et on va l’utiliser pour tous les cas pratiques.

Des cas en vrac, des exemples à la con

Tool magique: Tweakpng.exe certifié no virus lol

Je rajouterais des cas au fur et à mesure, pour le moment j’en ai fait que deux ! (flemme).

1. le chunk IHDR modifié

Lorsque l’on attaque un chall de stéga avec pour seul sujet un PNG, il n’est pas rare de tomber sur un cas très répandu: le chunk IHDR modifié. Souvenez vous, ce chunk permet de définir les propriétés de l’image: longueur, largeur et autres. Pour bien comprendre ce qu’il va suivre on va détailler rapidement comment il est possible de comprendre le “remplissage” d’une image png (je dis “possible” car c’est une interprétation personnelle).

Imaginez que le chunk IHDR c’est une boite étanche (notre image), et que les chunk IDAT c’est un liquide qui va à l’inverse de la gravité terrestre (car une image PNG se remplit de haut en bas). 

Dans un cas normal voici ce qu’il se passe lorsque l’on “verse” les IDAT dans la boite IHDR:

Un chunk IDAT rempliera plus ou moins l’image selon sa taille.

lorsque tous les chunk IDAT sont positionnés dans l’image on a normalement ce qui suit:

Maintenant, imaginez que je demande à mon IHDR de réduite la taille de notre boite, voici ce qu’il se passe: 

La partie visible par un visionneur de photo sera le cadre noir, mais il y aura “trop” d’IDATs et ils vont “déborder” (et seron donc “hors champ” !). Donc vu qu’ils ne seront pas visibles, une partie de l’image est cachée ! Et normalement aucune alerte n’est affichée par le visionneur d’images.

Résolution du cas:

Voila une image que j’ai modifié de manière à ce que “la boite soit modifiée” (vous pouvez la télécharger directement en clic droit):

On va donc l’ouvrir dans Tweakpng, et double cliquer sur IHDR. on définit ensuite la hauteur de l’image (Height) à 570 (vous pouvez mettre la hauteur que vous voulez, cela n’a aucun impact ! Normalement lors des challenges je met 1080 personnellement), on sauvegarde, et voici ce qui apparaît:

N’hésitez pas à noter l’inspiration pour le texte /20 svp.

N’essayez pas de modifier la valeur de la largeur de votre image sinon il va se passer ceci : 

Autrement dit, un beau bordel ! exemple avec notre image: passage de 512 de largeur à 1024, on “dédouble” notre image ! Même si c’est très moche.

Cas n°2, les headers foirés sur un fichier corrompu

Voici le fichier: wrong.png

Lorsqu’on l’ouvre avec TweakPNG, on se prend un warning assez foireux: 

 

Qui nous dit que notre fichier et pété sur un chunk (mais sans nous dire lequel, personne n’est parfait). Une fois OK cliqué, voilà ce dont on dispose : 

Soit, pas grand chose, on a même pas le minimum requis pour une image valide. C’est dû à ce fameux chunk foireux ! On dégaine donc HxD pour analyser l’hexadécimal après le dernier chunk valide (ici le dernier étant gAMA):

Non de zeus ! les 4 octets servant à définir la longueur tu chunk PLTE ont été remplacés par “HELP” ! Fuck !

Donc on a deux solutions, faire sauter le chunk PLTE (ce qui va accessoirement bousiller toute l’image, vous allez voir c’est drôle), soit compter les octets nécessaires au chunk pour être valide ! Pour l’exemple on va faire les deux solutions.

1. On fait tout sauter

Très bien, essayons d’identifier le prochain chunk après “PLTE”. Pas mal de lignes après (offset 213) on voit ceci :

Chouette, donc on conserve les 4 octets définissant la taille du chunk tRNS et on fait tout sauter entre “HELP” et ces 4 octets (de l’offset 76 à l’offset 20E inclus). 

Après sauvegarde, voici ce qu’il se passe lorsque l’on recharge le fichier dans TweakPNG: 

Youpi ! bon cette image est tirée d’un challenge en plusieurs étapes lors d’un CTF, son contenu nous importe peu !

 

Attention Avec cette méthode vous allez bousiller l’image ! Il est même probable que vous ne voyiez pas ce qu’il y a sur l’image ! Si vous voulez révéler ce qu’il y avait à l’origine, chargez la sur Aperisolve.fr (cc @Zeecka si tu passes par là 😛 ), un coup de LSB sur les pixels devrait faire apparaître le contenu de l’image d’origine (c’est le cas ici, car on a touché à la palette des couleurs, donc notre image est noire car le décodeur PNG ne sait pas comment afficher les couleurs), sinon attelez vous à la partie chiante, mais drôle, qui suit, on va faire ça proprement.

Voici le résultat d’un LSB via le site sur notre image salement corrigée (et qui est complètement noire à la sortie de notre correction):

2. On répare le chunk PLTE

Dans cette step, on va s’appuyer sur les connaissances que j’ai essayé de vous apporter dans les chapitres précédents.

Commençons par identifier le CRC (checksum) du chunk. Il s’agit des 4 derniers octets avant le chunk suivant (à partir de l’offset 20E):

Donc, il ne nous reste plus qu’à prier pour que l’auteur du challenge ait seulement modifié la longueur du chunk sans en altérer les données ou le CRC, sinon c’est un poil plus complexe.

Voilà donc l’ensemble des données contenues dans le chunk:

Soit une longueur de 20A16-4616 = 45210 ! (merci HxD). 

Donc 452 décimal converti en hexadécimal, ça donne: 0x1C5, ce qui donne 0x000001C5 avec le bon padding. On remplace donc “48 45 4C 50” (le fameux “HELP”), par “00 00 01 C5”. On sauvegarde et on ouvre l’image avec tweakPNG:

Youpille

Et si on ouvre l’image directement avec notre visionneur de photo favori, on a notre image corrigée avec ses vraies couleurs 🙂

On peut également visionner la palette de couleurs définie à l’origine pour cette image grâce à tweakPNG:

C’est parce que cette palette était manquante que l’image était noire dans la méthode précédente. Mais encore une fois, un LSB depuis le site Aperisolve.fr (ou avec un tool adapté cf: mon Github) sur les trois couleurs des pixels permet de voir le contenu essentiel de l’image, si il nous est impossible de corriger le chunk proprement.

Bon encore une fois, cet exemple est tiré d’un challenge en plusieurs étapes d’un CTF 😛

 

Voilà les amis, j’espère que je vous ai au moins appris un truc ou deux ! En attendant, merci d’avoir eu le courage de lire cet article 🙂

 

La bise !

Auteur

Sicarius
Étudiant @ENSIBS et apprenti ingénieur en cybersécurité chez GIE CBP

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *