STEGANO LSB/MSB
Introduction:
Bonjour à tous ! Aujourd'hui nous allons voir comment utiliser les LSB et MSB d'une image pour cacher du texte dans une image. Ce tutoriel est réalisé en Java, mais avec un peu d'imagination il peut être reproduit dans n'importe quel langage.
Je suis pour ma part loin d'être un expert en Java, donc si vous trouvez des erreurs qui ne m'auront pas nécessairement sautées aux yeux, c'est normal.
Présentation théorique:
Avant de s'attaquer à la pratique, penchons nous sur la théorie, puisque recopier le code qui va suivre sans en comprendre le fonctionnement serait un peu dommage. Ceux qui connaissent déjà les termes que nous allons aborder peuvent sauter à la partie pratique.
Pour commencer : Comment ça marche une couleur?
Vulgairement une couleur c'est un composé de 3 couleurs, le Rouge, le Vert et le Bleu, chacun représentés par un octet. Chaque image sur votre ordinateur est composée de millions de trios de rouge, de vert et de bleu, chaque trio représente un pixel. Chaque partie d'une couleur étant présente sous forme d'octet. On reviendra sur les détails techniques plus loin.
Ensuite : LSB/MSB késaco ?
LSB => Least Significant Bit.
MSB => Most Significant Bit.
Qu'on traduit en français par Bit de Poids Faible et Bit de Poids Fort. Autant dire que ça nous avance beaucoup...
On voudrait bien éviter la longue et douloureuse explication d'informatique théorique pour aller directement à ce qui nous intéresse mais on va quand même se la faire ^^.
Classiquement vous avez déjà tous vu un octet, ou au moins vous avez une vague notion de ce que c'est.
Mais si cette jolie suite de 0 et de 1 qu'on voit partout à la télé dès qu'un mec avec des lunettes pirate la NASA avec son 3310 "branché" à son micro ondes en infrarouge.
Pour ceux ayant raté cette incroyable scène de l'histoire du cinéma, ceci par exemple est un Octet : 01001101 !
Il n'est pas franchement beau à voir, il parle difficilement notre langue, mais il est gentil et il va nous aider le long de ce tutoriel. On va l'appeler Bob
Bob, comme tous les octets, est divisé en huit chiffres ayant soit la valeur 1 soit la valeur 0, et représente un chiffre entre 0 et 255. Mais comment me direz-vous?
Tout simplement en fait, chaque 0 ou 1 représente une puissance de 2 allant de 0 à 7 en fonction de son emplacement dans l'octet, ce sont les bits (pas la peine de tenter une vanne sur le nombre de bits de Bob, la langue française en à fait le tour il y a un bon moment ).
Prenons [Bob et calculons sa valeur:
0 1 0 0 1 1 0 1 ce qui nous donne 0(2^7)+64(2^6)+0(2^5)+0(2^4) +8(2^3)+4(2^2)+0(2^1)+1(2^0) = 77
Et c'est là qu'on en vient à ce qui nous intéresse !!! Car on vient de croiser innocemment nos amis MSB et LSB !
Ils sont seuls et sans défense le long d'une route de campagne abandonnée et notre dernière poupée gonflable vient de rendre l'âme. Et dans ce tutoriel nous allons littéralement les retourner comme des gants ^^
Donc pour ceux qui n'ont pas saisie la signification de LSB et MSB, je vais gentiment vous mettre des couleurs et on recommence ! Donc suivez bien, le LSB est en vert et le MSBen rouge :
0 1 0 0 1 1 0 1
Voilà ! Ceux qui ont suivi l'histoire des puissances de 2 devraient commencer à faire le lien entre les appellations de ces pauvres bits et ce que l'on vient de voir. En effet le MSB vaut 2^7, soit 128, ce qui fait que sa valeur à beaucoup d'importance dans la valeur de l'octet, il est signifiant. Et le LSB vaut 2^0, soit 1, ce qui fait qu'il vaut presque que dalle, il est triste et suicidaire, il est insignifiant.
Pour rester dans les couleurs je vous donne un petit exemple en changeant la valeur du MSB.
En considérant notre fidèle Bob comme le Rouge d'une couleur, et on va choisir arbitrairement dans le public deux autres octets pour gentiment jouer le Vert et le Bleu.
Applaudissez bien fort Alice 1001 1110 et Jack 0000 0001.
On se retrouve donc avec une couleur ayant pour valeurs 77, 158 et 1. Ce qui nous donne ce magnifique vert dégueulasse
Or si on s'amuse à faire de la chirurgie esthétique sur le MSB de notre ami Bob, pour qu'il devienne un 1, Bob devient 11001101, et notre vert dégueulasse devient un un magnifique jaune dégueulasse.
Autant dire que ça se voit beaucoup, et que si on s'amuse à péter la gueule d'un millions de MSB dans un photo de Clara Morgane on va rapidement se retrouver avec un monstre fluo sur l'image.
Mais en revanche, si on laisse le MSB tranquille, et qu'on s'acharne sur son pote que personne n'aime, le LSB, ça devient totalement différent.
Le LSB de Bob était à 1, passons le à 0 et observons ce qu'il advient de notre trio. SURPRISE ! On retrouve notre vert dégueulasse !
Et en fait, pas du tout, l’œil humain est persuadé qu'il s'agit du même vert, mais il y a une différence. Ceci nous prouvant bien que tout le monde s'en foutait du pauvre petit LSB.
Et ceux qui se demandaient jusque là pourquoi je passais trois plombes à vous présenter la chose, commencent à saisir l'intérêt de tout ce fatras !
Parce que si personne ne remarque la présence ou non du petit LSB, peut-être qu'on peut s'en servir nous, pour par exemple...dissimuler des informations !
Car à l'instar des couleurs, le texte aussi est écrit en octets, ce qui avouons-le, est vachement pratique (et surtout logique). Sans oublier que là, on a touché qu'au rouge du trio Bob, Alice et Jack. Mais si nous jouons avec les LSB de tout le trio, on peut cacher trois bits dans chaque couleur, même quatre si on appelle Franck, notre gentil canal Alpha dont le but est de gérer la transparence et de faire des photomontages bien pourris.
Explications: Imaginons que nous voulons cacher la lettre M dans le LSB du rouge d'une suite de couleurs. On sait que M dans les normes ASCII est égal à 77 soit la valeur exacte de notre ami Bob avant qu'on vienne jouer avec ses bits. On veut donc dissimuler Bob (01001101) dans les LSB de 8 couleurs différentes.
Prenons une série de rouges au hasard et en ignorant le reste des couleurs:
0001 0000
0001 0000
0010 1001
0010 1001
1010 1111
1010 1110
1110 0110
1110 0110
0011 0010
0011 0011
1001 1111
1001 1111
0000 1001
0000 1000
0101 0100
0101 0101
On voit bien Bob apparaitre en gras dans cette série de rouges, si on y ajoutait autant de séries de bleu et de vert, on se retrouverait avec une ligne de pixels bien moches. Mais dans le cas d'une photo, personne ne verrait la différence entre cette série de rougeNotre ami Bob a donc été dissimulé dans cette suite de 8 couleurs sans le moindre problème, et on a donc notre L dissimulé dans 8 pixels sur une image. Enfin ça c'est en théorie, dans les faits vous avez juste huit octets sous les yeux et vous vous demandez sérieusement ce que vous foutez encore là
Et bien là réponse est simple, car nous allons voir maintenant comment faire en sorte de cacher des milliers de Bob (notre texte) dans des millions de Alice, Jack et compagnie (nos pixels). Vous avez deviné, nous passons maintenant à
La Pratique !
Nous allons donc maintenant voir comment mettre toutes ces informations en pratique, afin d'ouvrir une image, d'en modifier tous les LSB et MSb qu'on veut pour cacher notre texte avant de l’enregistrer.
Avant de commencer je tiens à préciser qu'il existe évidement des façons mieux optimisées de faire la même chose, en utilisant moins de variables et en s'épargnant des étapes, mais vu que ceci est un tutoriel j'essaie surtout de faire en sorte que le code soit compréhensible.
J'ai fait en sorte que le code soit le plus lisible possible et commenté de façon claire et précise, donc ça devrait aller, mais je vais quand même vous accompagner dans la lecture du code ^^
Pour commencer on va assumer que vous savez créer un nouveau projet sur Eclipse, avec une nouvelle classe Main pour lancer le tout. Si ce n'est pas le cas, renseignez vous et revenez dans quinze secondes
Commençons par créer notre classe ainsi que son constructeur, que l'on va sobrement baptiser LessAndMostSignificantBitStegano, ce qui est relativement explicite comme nom de classe :
Code:
public class LessAndMostSignificantBitStegano{ public BufferedImage ImgToSave; // L'image que l'on enregistrera une fois le traitement terminé public BufferedImage ImgToLoad; // L'image que l'on va utiliser comme support public Color[][] Pixels; // Le magnifique tableau qui va contenir les couleurs à traiter public String TextToEncrypt; // Notre texte en clair public String Datas; // Notre texte sous forme d'octets public LessAndMostSignificantBitStegano() { TextToEncrypt="Je suis un texte destiné à être dissimulé"; Datas = CastStringToBitArray(TextToEncrypt); Initialize("Capture.png"); if(ImgToSave!=null) { SetPixels(); } else { System.out.println("Ce programme vient de se vautrer"); } Action("imageFinale.png"); } public void Initialize(String imageToLoadName) { //Du très classique, on charge l'image dans ImgToLoad et on initialise ImgToSave aux bonnes dimensions ImgToLoad = null; try { ImgToLoad = ImageIO.read(new File(imageToLoadName)); } catch (IOException e) { e.printStackTrace(); } if(ImgToLoad != null) { ImgToSave = new BufferedImage(ImgToLoad.getWidth(), ImgToLoad.getHeight(), BufferedImage.TYPE_INT_ARGB); } else { System.out.println("Erreur chargement image"); } } public void SetPixels() { /* Et là on se contente de mettre CHAQUE pixel de l'image source dans un tableau de couleurs histoire des les avoir sous la main quand on voudra jouer avec leur bit de poids faible*/ Pixels = new Color[ImgToLoad.getWidth()][ImgToLoad.getHeight()]; for(int i=0; i<ImgToLoad.getWidth();i++) { for(int j=0; j<ImgToLoad.getHeight();j++) { Pixels[i][j] = new Color(ImgToLoad.getRGB(i, j)); } } }
Nous n'allons pas nous attarder sur Initialize et SetPixels, ces deux fonctions se contentent d'initialiser nos variables.
Nous avons donc deux fonctions inconnues qui sont respectivement CastStringToBitArray(), et Action(). Fort heureusement leurs noms sont assez explicites, mais voici leur présentation en détail :
Code:
public String CastStringToBitArray(String stringToCast) { /*ici rien de bien compliqué, on se content de mettre la valeur en octet de chaque lettre dans une chaine de caractères, aussi nommé String en java.*/ String conv= new String(); for(int i=0; i<stringToCast.length();i++) { conv+=String.format("%8s", Integer.toBinaryString((byte)stringToCast.charAt(i) & 0xFF)).replace(' ', '0'); } return conv; }
conv+=String.format("%8s", Integer.toBinaryString((byte)stringToCast.charAt(i) & 0xFF)).replace(' ', '0');
Et en plus c'est moche, mais au moins vous devriez comprendre étape par étape comment on transforme la lettre en octet. En fait c'est tout con:
- on prend notre ième caractère dans la chaine à transformer en octet
- on le cast en byte/octet (qui connement en java est un entier compris entre 0 et 255, ce qui nous arrange pas)
- donc tout aussi connement on le convertit en huit 0 et 1 (sauf que connement en java il remplace les zéros par des espaces)
- donc toujours aussi connement on remplace les espaces par des 0 et on balance ce groupe de huit 0 et 1 à la suite de la chaine de caractères qu'on retourne à la fin
Dans les faits, après ce traitement notre chaine est devenue une longue série de Bob : 0100101001100101001000000111001101110101011010010111001100100000011101010110111000100000011101000110 0101011110000111010001100101001000000110010001100101011100110111010001101001011011101110100100100000 1110000000100000111010100111010001110010011001010010000001100100011010010111001101110011011010010110 1101011101010110110011101001
Et vu que nous sommes très paranoïaque nous allons cacher ces 328 bits dans 328 couleurs différentes, plus précisément dans le rouge de 328 pixels différents sur notre image.
Passons maintenant à la fonction Action() !
Code:
public void Action(String nomFichierToSave) { int incrChar=0; for(int i=0; i<Pixels.length;i++) { for(int j=0; j<Pixels[i].length;j++) { if(incrChar<Datas.length()) { /* tant qu'on a pas utilisé toute notre chaine on répète l'opération*/ System.out.println("Le pixel rentre et vaut sa valeur Red = "+Pixels[i][j].getRed()); Pixels[i][j] =LSBOnRed(Datas.charAt(incrChar),Pixels[i][j]); //On change la valeur Red avec celle de notre chaine de bits System.out.println("Le pixel sort et vaut "+Pixels[i][j].getRed()); incrChar++; } /*dès qu'on a fini on mets le reste des pixels de l'image*/ ImgToSave.setRGB(i, j, Pixels[i][j].getRGB()); } } try { ImageIO.write(ImgToSave, "png", new File(nomFichierToSave)); //Et on sauvegarde l'image } catch (IOException e) { e.printStackTrace(); } }
Donc là on voit bien qu'Action() se contente de parcourir tous les pixels de notre tableau, d'effectuer l'opération sur le LSB du rouge de chaque pixel et puis de remplir l'image avec le reste. Une fois l'opération terminée il sauvegarde notre image.
Maintenant venons à l'opération la plus importante, celle à la base de ce tuto, la fonction LSBonRed(), qui prend en argument le bit à écrire et la couleur à modifier puis nous retourne la couleur avec le LSB modifié. A cause de la portée des variables en Java nous
sommes obligé de réaffecter dans la foulée notre Pixel[i][j], cela étant on aurait très bien pu utiliser une couleur temporaire .
Concentrons nous maintenant sur cette fonction miracle :
Code:
public Color LSBOnAlpha(char currbit, Color curpixel) { if(currbit%2==0) //on teste si le bit entré vaut 0 ou 1 { //si la valeur a entrer vaut 0 et que le LSB de la cible vaut 1 on le modifie, sinon on ne fait rien if(curpixel.getRed()%2==1) // on teste si le LSB du rouge vaut 0 et ou 1 { Color temp= curpixel; curpixel = new Color(temp.getRed()-1, temp.getGreen(), temp.getBlue(), temp.getAlpha()); } } else { //si la valeur a entrer vaut 1 et que le LSB de la cible vaut 0 on le modifie, sinon on ne fait rien if(curpixel.getRed()%2==0) { Color temp= curpixel; curpixel = new Color(temp.getRed()+1, temp.getGreen(), temp.getBlue(), temp.getAlpha()); } } Color temp = curpixel; return temp; }
Ceux qui ne connaissent pas l'opérateur modulo (%) doivent se poser une question ou deux. En réalité c'est très simple. Tout nombre est soit pair ou impair, or le bit cible vaut en théorie soit 2^0 soit 0. Ce qui fait qu'à l'échelle du nombre c'est lui qui détermine si ce dernier est pair ou impair.
Donc si notre Red est impair son LSB vaut 1, sinon 0. Et pour le modifier on a donc juste à ajouter ou soustraire 1.
Et vu que tout octet pair à forcément une valeur comprise entre 0 et 254 on n'a aucun risque à lui ajouter 1 pour le rendre impair. De même tout octet impair est compris entre 1 et 255 donc aucun problème non plus à lui soustraire 1.
Alors que si on se contentait d'ajouter 1 à chaque fois, on aurait un léger risque d'atteindre 256, ce qui pourrait éventuellement faire planter la machine (après j'ai pas testé peut être cela fait-il juste apparaitre une Megan Fox en bikini sur votre canapé).
Le problème est similaire en cas de soustraction systématique.
Et voilà avec ça vous avez de quoi cacher du texte dans une image. Ce qui ne nous sert absolument à rien... si on a pas ce qu'il faut pour récupérer le texte
Voici donc le code et les fonctions qui font le travail inverse :
Code:
public char UnLSBR(Color curpixel) { System.out.println(curpixel.getRed() +"valeur en entrée "); char bit='0'; if(curpixel.getRed()%2==1) { bit='1'; } System.out.println("Char retourné = "+bit); return bit; } public String CastBitArrayToString(String bitArrayToCast) { String resultat=""; String[] temp = bitArrayToCast.split("(?<=\\G........)"); System.out.println(temp[0].length()); for(int i=0; i<temp.length;i++) { resultat+=(char)Byte.parseByte(temp[i], 2); } return resultat; } public String Decode() { String decode=""; for(int i=0; i<1;i++) { for(int j=0; j<Datas.length();j++) { decode+=UnLSBR(new Color(ImgToLoad.getRGB(i, j))); } } return Decode; }
Et là c'est super simple, vous effectuez un System.out.println(CastBitArrayToString(Decode())); et vous avez votre texte en clair.
Niveau explications il n'y a rien de bien compliqué :
- On traite chaque pixel de l'image (ici j'ai mis directement la longueur de la chaine vu que je la connais)
- S'il est pair on ajoute un 0 à notre chaine de bit sinon on ajoute un 1
- La fonction CastBitArrayToString découpe la chaine en morceaux de 8 bits via une expression régulière bien moche (bitArrayToCast.split("(?<=\\G........)") et fait la conversion en lettres
- On affiche le tout à la fin !
Et voilà, vous avez tout ce qu'il vous faut à votre disposition. A partir de là de nombreuses possibilités s'offrent à vous, comme le fait de crypter le message avant, de répartir les bits encodés suivant une suite prédéfinie, de les mettre alternativement dans le rouge puis le bleu, puis le vert etc. Vous pouvez aussi faire le programme en fenêtré, faire une version pour android, ou que le mettre sur le système embarqué des A380 si vous bossez chez airbus ^^
Je déconseilles l'utilisation du canal alpha si l'image n'a pas de transparence à l'origine, sinon ça va se voir énormément.
Et en bonus je vous met le code pour le MSB (vous pouvez essayer, vous allez voir ça fait des jolis pixels fluos partout ^^), après libre à vous de le modifier pour qu'il rende toutes les images en noir et blanc, ce qui le rendrait moins détectable à l'oeil nu.
Code:
public Color MSBOnAlpha(char currbit, Color curpixel) { /* Meme système que pour le lsb ou presque. Si le bit a écrire vaut 0 on teste si le Red est divisble par 128 (donc que le bit cible est à 1), si c'est le cas on le met à 0 (on enlève 128), sinon on laisse comme tel */ if(currbit%2==0) { if(curpixel.getRed()>=128) { Color temp= curpixel; curpixel = new Color(temp.getRed()-128, temp.getGreen(), temp.getBlue(), temp.getAlpha()); } } else { if(curpixel.getRed()<128) { Color temp= curpixel; curpixel = new Color(temp.getRed()+128, temp.getGreen(), temp.getBlue(), temp.getAlpha()); } } Color temp = curpixel; return temp; } public char UnMSBR(Color curpixel) { System.out.println(curpixel.getRed() +"valeur en entrée "); char bit='0'; if(curpixel.getRed()>=128) { bit='1'; } System.out.println("Char retourné = "+bit); return bit; }
Commentaire