Dissimuler un texte dans une image

Formation

En Ligne

Prix sur demande

Appeler le centre

Avez-vous besoin d'un coach de formation?

Il vous aidera à comparer différents cours et à trouver la solution la plus abordable.

Description

  • Typologie

    Formation

  • Méthodologie

    En ligne

Grâce à cette formation vous pourrez acquérir les connaissances nécessaires qui vous permettrons d’ajouter des compétences à votre profil et obtenir de solides aptitude qui vous offriront de nombreuses opportunités professionnelles.

Questions / Réponses

Ajoutez votre question

Nos conseillers et autres utilisateurs pourront vous répondre

À qui souhaitez-vous addresser votre question?

Saisissez vos coordonnées pour recevoir une réponse

Nous ne publierons que votre nom et votre question

Les Avis

Les matières

  • Image

Le programme

Introduction du cours

Salut tout le monde !

Prêts pour une incursion dans le monde de la stéganographie, dans l'art de la dissimulation ? Alors allons-y !

Dans ce tutoriel, vous allez apprendre à dissimuler un message quelconque au sein d'une image Windows bitmap BMP (eh oui, c'est malheureusement une limitation bien embêtante pour ce genre de choses, mais nous verrons pourquoi on ne peut pas utiliser d'image JPEG... mais par contre, on peut utiliser (sous certaines conditions !) des images PNG !). Prêts à tenter l'aventure ?!

Le code que je vais donner est du code PHP, parce que c'est le langage que je maîtrise le mieux. Il est par contre très facile de porter le code en C++ ou en Java (je l'ai par ailleurs réécrit en C++ sans jamais en avoir fait auparavant, donc ce n'est vraiment pas dur). Le seul problème qui se pose alors est que la console Windows gère très mal les accents ; c'est pourquoi une page HTML semble plus appropriée comme moyen d'affichage.

L'idée de départ

L'idée est la suivante. On a un texte en clair (donc pas chiffré du tout, et lisible par n'importe qui) que l'on veut dissimuler « dans » une image (un fichier bitmap). Évidemment, il ne s'agit pas d'écrire le texte en plein milieu du dessin, ni d'ajouter du texte en commentaire dans l'image (comme les champs EXIF des images JPEG). Non, ça ne serait pas super discret. L'idée est plutôt de modifier les bits de poids faible des pixels par un bout de lettre…

Bit de poids faible ? Quésako ?

Ah oui, un petit rappel s'impose je crois. ^^

La représentation binaire

Depuis… allez… le CP, vous savez certainement compter en base 10. Prenons le nombre « 1992 ». Vous savez tous que la valeur de ce nombre s'obtient comme suit :

1992 = 2*100 + 9*101 + 9*102 + 1*103

C'est-à-dire :

1992 = 2+9*10+9*100+1*1000 (Oui j'ai fait exprès de commencer par la droite, vous verrez rapidement pourquoi.)

Remarquons la chose suivante : si je change la valeur du chiffre tout à droite du nombre (ici le 2), la valeur du nombre ne changera pas de beaucoup (si je change le 2 par un 9, j'ajoute 7 à la valeur du nombre), alors que si je change la valeur du chiffre tout à gauche (ici le « 1 »), la valeur du nombre changera beaucoup (jusqu'à 8000 de différence tout de même).
Bien.

Maintenant, place à la représentation binaire. En fait, en binaire, la valeur d'un nombre s'obtient de façon tout à fait analogue. Par exemple, le nombre dont l'écriture binaire est 100101000101 se décompose comme suit (je commence par la droite encore une fois, dans ce cas-ci c'est plus simple pour ne pas s'emmêler avec les puissances) :
100101000101 = 1*20 + 0*21 + 1*22 + 0*23 + 0*24 + 0*25 + 1*26 + 0*27 + 1*28 + 0*29 + 0*210 + 1*211

Ouf ! D'ailleurs, la même remarque que tout à l'heure s'impose : si je change la valeur de un ou deux voire trois chiffre(s) tout à droite, la valeur du nombre ne changera pas de beaucoup (au pire de 4 en changeant 2 chiffres, 8 en en changeant 3). Ce sont ces chiffres-là, dans une telle représentation binaire, qui sont appelés « bits de poids faible », tout simplement parce que si l'on change leur valeur, la valeur du nombre ne sera pas changée de beaucoup. A contrario, les bits les plus à gauche sont appelés « bits de poids fort », pour une raison analogue…

Bien, maintenant, l'idée est la suivante : on associe à chaque lettre une valeur, d'après la table ASCII, et ensuite…

Hé ! Stop ! C'est quoi la table ASCII ?

Argh, je m'attendais à cette question. En fait, c'est très simple. L'idée est d'associer à chaque caractère un nombre. Évidemment, on ne pouvait pas tout simplement prendre la position des lettres dans l'alphabet (A=1, B=2, …). Comment différencier majuscules et minuscules ? Et comment représenter les signes de ponctuation ? C'est pour cela que l'on a créé la table ASCII, qui justement, associe à chaque nombre de 0 à 127 un « caractère ». Notez que tous les caractères ne sont pas obligatoirement « imprimables » ; en sus des chiffres et des lettres, il existe aussi des caractères de contrôle, des trucs bizarres comme des carillons, etc.

Bien bien, mais pourquoi aller jusqu'à 127 seulement alors qu'on peut stocker 256 valeurs différentes dans un octet ?

Très bonne question, encore une fois. En fait, le code ASCII a été introduit alors que les ordinateurs n'en étaient qu'à leur début. En particulier, les transmissions n'étaient pas vraiment très fiables à cette époque. Quel est donc le rapport avec la choucroute ? En fait, les nombres de 0 à 127 ont tous un point commun... leur bit de poids fort (donc celui tout à gauche normalement) vaut 0. Ce bit était appelé bit de contrôle, et servait à vérifier que la transmission avait eu lieu sans trop de cafouillage…

Autre petit point intéressant. L'ASCII est ce qu'on appelle un encodage de caractères… et il en existe d'autres. En particulier l'Unicode (ou UTF-8), qui gère aussi les alphabets non latins (russe, arabe, grec, …) et bien plus de caractères accentués. Mais pour se faciliter les choses au début, on va se limiter à du ASCII pour notre texte, tout simplement parce qu'en ASCII, un caractère = un octet. Pour ceux que ça intéresse, voici la table ASCII qui donne la correspondance entre un nombre et un caractère : table ASCII . Une version française peut être trouvée ici.

Tous les problèmes ont été réglés ? Bien ? Dans ce cas, continuons notre explication…
On a donc vu que l'on peut associer à chaque caractère de notre message un chiffre (en théorie inférieur à 127, mais on va considérer que l'on utilise l'ASCII « étendu » qui va jusqu'à 255 — simple détail). Ce chiffre va pouvoir être représenté sous forme binaire et il tient dans un octet.
Prenons un exemple concret. En ASCII, l'apostrophe « ' » est codée par le chiffre 37. Ainsi, on a l'association « ' » = 37. De plus, en binaire, 37 s'écrit 00100101, d'où l'association « ' » = 00100101. Bien compris ? Passons donc à l'étape suivante.

On va maintenant « couper » ce chiffre en petits bouts. On va dire que la longueur de ces bouts va valoir 2 (on peut aussi utiliser 4, mais on risque de trop détériorer l'image. Une longueur de 3 complique tout (même si c'est faisable) et une longueur de 1 demande trop de place). Ainsi, en découpant 37, ça nous donne ça :

00100101 = 00 10 01 01

Bon, maintenant on va cacher ces bouts dans notre image. Mais comment faire ?

Le stockage de l'information dans une image bitmap

Comme vous le savez tous, un pixel est défini par 3 (ou 4 avec la transparence, le fameux canal alpha) composantes : une composante rouge, une bleue, et une verte ; c'est le fameux RVB (il existe d'autres représentations, notamment la représentation CMJN pour l'imprimerie, mais elle ne nous intéresse pas ici). Une image de type BMP (Windows bitmap) est, quant à elle, composée comme suit : après un header, qui indique notamment les dimensions de l'image, suit une suite d'octets, qui, pour chaque pixel, en commençant en bas à gauche, indique successivement les valeurs de la composante bleue, puis verte, puis rouge (oui, c'est le sens inverse de RVB). Après 3 octets commence le codage du deuxième pixel, et ainsi de suite. De plus, d'après Wikipédia (version anglaise, voir l'exemple donné), nous savons que, dans une image BMP standard à 24 bits (c'est-à-dire un octet pour le bleu, un octet pour le vert, un octet pour le rouge), le header a une taille fixe de 54 octets. Nous avons toutes les informations pour commencer notre insertion dans l'image…

Euh comment ça, on a toutes les informations ? À quoi ça nous sert tout ça ?

Bon, prenons un exemple. On va prendre n'importe quelle image bitmap, l'ouvrir avec un éditeur hexadécimal (comme Ghex sous Linux), sauter le header et regarder ce qu'il nous donne :

Citation : ghex

00010110000101000001111000010101 …

(Oui normalement vous aurez une représentation hexadécimale et non pas binaire, mais en théorie le programme donne aussi une représentation binaire.)
Chaque groupe de 8 bits (donc chaque octet) indique (en binaire) les quantités respectives de bleu, vert et rouge du premier pixel (en bas à gauche pour rappel). Ainsi, le premier pixel aura une intensité de bleu de 22, une intensité de vert de 20 et une intensité de rouge de 30. Si on définit une telle couleur dans Paint, on aura très probablement une couleur très foncée...Le quatrième octet, quant à lui, indique la quantité de bleu du deuxième pixel. Mais bon, à vrai dire, on s'en fiche un peu de tout ça. Nous ce qu'on veut, c'est dissimuler notre texte dans cette image. Comment allons nous donc faire ? Substituer tout simplement les bits de poids faible de l'image (c'est-à-dire les bits les plus à droite pour chaque couleur) par les morceaux de notre caractère ! En pratique, ça donne ça :

' = 37 = 00100101 = 00100101

Une fois qu'on a découpé l'octet représentant notre caractère, on va modifier les bits de poids faible de l'image en conséquence. Ce qui nous donne donc trois nouveaux octets :

Citation : ghex

00010100 00010110 00011101 00101001 …

Vous voyez l'idée ? On échange les deux bits de poids faible de chaque couleur par deux bits correspondant à une partie du caractère. Ainsi, on ne change que très peu l'image finale (l'œil humain est incapable de voir une différence de 4 sur une teinte de 256 tons), mais on peut très facilement retrouver le message final : il suffit de lire tous les deux bits de poids faible de chaque octet, et de les assembler en octets pour reconstituer les caractères du message.

Pour ceux qui se demandent « et en prenant une image JPEG ? », la réponse est la suivante. Contrairement au « Windows bitmap », le format JPEG compresse l'image. Cela signifie qu'on n'a plus l'équivalence « un octet = une valeur de couleur d'un pixel ». Donc, si on change les bits de poids faible des octets d'une image JPEG, on n'a aucune idée de ce qu'on modifie. On pourrait alors penser qu'il suffirait de décompresser l'image JPEG, de changer les bits de poids faible pour chaque pixel, puis de compresser à nouveau. Sauf que voilà, la compression modifie la valeur de chaque teinte de chaque pixel. La variation est de 2 en moyenne et peut atteindre 4 voire plus. Difficile (parfois) à distinguer pour un œil humain, mais notre message devient alors illisible…

Quelques considérations mathématiques

Avant de nous lancer dans le code, il reste un ou deux problème(s) à régler.

Premier problème : comment modifier les bits de poids faible ?

Très bonne question. En d'autres termes, comment arriver à partir de 00011110 et de 01 à 00011101 ?

Vous ne voyez pas ? Avec les modulos pardi ! Ou plus exactement, les restes de la division euclidienne…

Petite explication. Lorsque vous avez le nombre dont l'écriture décimale est 1992 et que vous voulez avoir 1990, vous faites comment ? Vous enlevez 2, tout simplement. Pourquoi 2 ? Parce que c'est le reste de la division euclidienne de 1992 par 10 (en effet, 1992 = 10*199 + 2). Bien. Si maintenant on veut avoir 1900 à partir de 1992, on va tout simplement enlever 92. Eh oui, vous l'aurez deviné, 92 est le reste de la division euclidienne de 1992 par 100. Eh bien, en binaire, c'est la même chose ! Pour passer de 00011110 à 00011100, il suffit de soustraire à 00011110 le reste de la division euclidienne de 00011110 par 4 (soit 10 en binaire, c'est-à-dire 2), et pour passer de 00011100 à 00011101, il suffit d'ajouter 01 (donc 1, en gros…). Ce n'est pas plus dur que ça. Ah oui, dernier détail, l'opérateur modulo (qui, en réalité, donne le reste de la division euclidienne d'un chiffre par un autre) est le signe « % » dans la plupart des langages (en particulier dans ceux basés sur le C : C++ mais aussi PHP, Java, etc.).

Petite remarque : pour tous ceux qui sont à l'aise avec les opérateurs de bits, il est également possible de résoudre ce petit problème avec (petit rappel ici : Introduction aux opérateurs de bits). Mais malheureusement, il faut aussi le faire en deux étapes. En effet, 00011110 & 11111100 (=28 - 22) donnera 00011100, et il suffit ensuite de rajouter les deux bits intéressants (ou de faire une opération de OU binaire — ça revient au même). Mais bon, c'est juste pour la petite histoire, hein… ça ne risque que de compliquer le code à la fin…

Comment lire les bits de poids faible d'un octet ?

Avec la méthode vue précédemment, vous devriez avoir deviné… Il suffit de prendre le reste de la DE de la valeur de l'octet par 4, et le tour est joué !

Une fois que l'on a récupéré toutes les valeurs des bits de poids faible, comment fait-on pour retrouver la valeur de l'octet du caractère codé ?

Une manière simple est de tout stocker dans une chaîne de caractères, puis de passer par des fonctions qui donnent la valeur décimale d'un nombre stocké en représentation binaire. En PHP, il s'agit de la fonction bindec(). À noter que la fonction decbin() fait exactement l'inverse (on passe d'un nombre décimal à un nombre en base 2). On aura besoin des deux fonctions à un moment ou un autre de notre programme. Et si vous voulez écrire le programme en un autre langage, il faudra coder vous-mêmes ces deux fonctions (avec des substr() ce n'est pas très dur…), voici ce que ça pourrait donner en C++ :

int bindec(string binaire) { int rep_decimale=0; string bit, zeros = ""; if(binaire.size() < 8) // Si on se retrouve avec moins de 8 caractères. { zeros.append(8-binaire.size(), '0'); binaire = zeros.append(binaire); } for(int i=0;i<=7;i++) { bit = binaire.substr(i,1); if(bit == "1") { rep_decimale += pow(2, 7-i); } } return rep_decimale; } string decbin(int decimal) { string rep_binaire = ""; if(decimal > 255) { cout << "Trop grand"; return "0"; } else { for(int i = 7;i>=0;i--) { if(decimal >= pow(2, i)) { rep_binaire.append("1"); decimal -= pow(2, i); } else { rep_binaire.append( "0" ); } } return rep_binaire; } }

Ces deux fonctions ne fonctionnent qu'avec les nombres dont la représentation décimale ne dépasse pas 255, donc les nombres tenant dans un octet… On ne veut pas plus en même temps (évidemment, on pourrait les coder de nouveau pour que les fonctions acceptent de plus grands nombres aussi, mais ce serait une perte de temps ici)…

À présent que tout est réglé… à nos éditeurs de texte préférés. Que le codage commence !

La partie Insertion

Commençons par la partie insertion du message dans l'image. Ce n'est pas forcément la plus simple, mais au moins, on pourra plus facilement tester si tout s'est bien passé. Parce qu'en fait, l'implémentation de l'algorithme pose quelques problèmes… Regardons le code suivant (du PHP parce que c'est plus simple à tester et qu'il n'y a pas de problèmes d'accents...) :

<?php $message = 'Top secret ultra confidentiel !'; $octet_decoupe = array(); for($i=0;$i<strlen($message);$i++) { $caractere = $message[$i]; $valeur_octet = ord($caractere); $octet_binaire = decbin($valeur_octet); $octet_decoupe = str_split($octet_binaire, 2); foreach($octet_decoupe AS $partie_octet) { //... } } ?>

Rien de bien compliqué là-dedans. Il faut juste savoir que la fonction ord() donne le code ASCII d'un caractère, que strlen() indique la longueur d'une chaîne de caractères (oui, j'utilise une syntaxe un peu spéciale mais tout à fait valide en PHP comme en C ou en C++), et que str_split() découpe une chaîne de caractères en petits morceaux. Mais voilà, nous avons un petit problème. Essayons de prévoir ce que donnera ce code (du moins pour la première itération). Un T majuscule est codé par le nombre 84 en ASCII, ce qui correspond à 1010100 en base 2. Une fois que l'on aura découpé ça en morceaux, on aura le découpage suivant :

1010100 = 10 10 10 0

Aïe ! Vous voyez où ça coince ? Le dernier zéro se trouve tout seul. En effet, votre langage est intelligent, il ne va pas...

Appeler le centre

Avez-vous besoin d'un coach de formation?

Il vous aidera à comparer différents cours et à trouver la solution la plus abordable.

Dissimuler un texte dans une image

Prix sur demande