Aller au contenu

Stéganographie⚓︎

1. Description⚓︎

La stéganographie consiste à dissimuler un message dans un autre. Par exemple, on veut cacher l'image A femme.bmp dans l'image B cypres.bmp. Ici les images sont de mêmes dimensions mais il suffit que l'image invisible puisse s'intégrer comme un bloc de pixels dans l'image visible. Le format de stockage est important, les formats de compression avec perte comme JPEG ne permettent pas la stéganographie d'images.

Image visible

image

Image cachée

image

Image dévoilée

image

Pour réaliser cette opération, nous allons utiliser les représentations binaires des valeurs des pixels, et en particulier des opérateurs bit à bit, que nous présentons brièvement ci-dessous :

Méthode

Pour convertir un entier en une série de bits, il suffit de l'écrire en base \(2\), on parle d'écriture binaire.

L'écriture décimale de treize est \(13\) car \(1 \times 10 + 3 = 13\).

Son écriture binaire est \((1101)_{2}\) car \(1 \times 2^{3} + 1 \times 2^{2} + 0 \times 2^{1} + 1 \times 2^{0} = 13\).

  • En Python la fonction bin retour l'écriture binaire d'un entier sous forme de chaîne de caractère préfixée de '0b' et sa réciproque est la fonction int avec le paramètre optionnel \(2\).
>>> bin(13)
'0b1101'
>>> int('0b1101', 2)
13
  • L'opérateur >> permet de décaler les bits vers la droite, tandis que << les décale vers la gauche :
>>> 13 >> 1
6
>>> bin(6)
'0b110'
>>> 13 << 1
26
>>> bin(26)
'0b11010'
  • L'opérateur & permet de réaliser une opération logique ET bit par bit entre deux entiers, tandis que l'opérateur | permet de réaliser une opération logique OU bit par bit entre deux entiers:

    Table de vérité du ET

    x y x ET y
    0 0 0
    0 1 0
    1 0 0
    1 1 1

    Table de vérité du OU

    x y x OU y
    0 0 0
    0 1 1
    1 0 1
    1 1 1
>>> bin(13), bin(10)
('0b1101', '0b1010')
>>> 13 & 10, bin(13 & 10)
(8, '0b1000')
>>> 13 | 10, bin(13 | 10)
(15, '0b1111')

On utilise souvent le ET binaire avec un entier particulier appelé masque qu'on applique à un autre entier pour faire apparaître une information par gommage des bits qui ne sont pas égaux à \(1\) comme dans le masque. Ce peut être pour récupérer l'adresse IP du sous-réseau dont dépend une machine (voir https://fr.wikipedia.org/wiki/Sous-réseau) ou pour fixer des droits par défaut à tout fichier créé en appliquant le masque à la série \(111111111\) représentant les droits RWXRWXRWX en lecture (R), écriture (W), exécution (X) pour le propriétaire, le groupe principal ou les autres utilisateurs du fichier.

Représentation d'une image

Une image sera représentée par une liste de listes (pour une image de 640 pixels de large par 480 pixels de hauteur, ce sera une liste de 480 listes de taille 640).

Chaque élément de cette liste de listes correspond à un pixel de cette image. Si c'est une image en couleur (RGB), chaque pixel est représenté par un tuple d'entiers entre 0 et 255 (exemple: (48, 52, 203) signifie que c'est un pixel comportant 48/255 rouge, 52/255 vert et 203/255 bleu). Si c'est une image en niveau de gris (L), chaque pixel est représenté par un entier entre 0 et 255.

schéma

L'utilisation de la bibliothèque PIL permet de charger une image est de la mettre sous la forme présentée ci-dessus.

Utilisation de Pillow

from PIL import Image
im = Image.open("image_exemple.png") # ouverture du fichier image "image_exemple.png" qui doit être dans le même dossier
pixels = im.getdata() 
width, height = im.size
pix_image = [[pixels[i * width + j] for j in range(width)] for i in range(height)] # Permet de créer une liste de listes comme voulu

Méthode

On va créer une image C contenant à la fois les images A et B, et l'apparence de C sera celle de A car les informations extraites de A seront placées au premier plan dans le codage numérique de chaque pixel :

  • Soit \(a\) la valeur d'une des trois composantes (R,G,B) d'un pixel de l'image A et \(b\) et \(c\) les valeur de la même composante du même pixel respectivement dans les images B et C. \(a\) et \(b\) sont des entiers compris entre entre \(0\) et \(255\) dont la représentation binaire compte \(8\) bits. Lorsqu'on lit de gauche à droite une écriture binaire de 8 bits, les premiers bits lus sont dits de poids forts et les derniers de poids faibles.

  • Pour construire les \(8\) bits de \(c\), on prend par exemple les \(5\)bit s de poids forts de \(a\) suivis des \(8-5=3\) bits de poids forts de \(b\). On perd de l'information sur \(a\) et \(b\) : plus on prend de bits de poids forts moins la perte est importante pour \(b\) mais plus l'apparence de l'image C s'éloigne de celle de A.

    Par exemple avec \(a=166\) d'écriture binaire \(a = (\overbrace{10100}^{\text{forts}}110)_{2}\) et \(b=234\) d'écriture binaire \(b=(\underbrace{111}_{\text{forts}}01010)_{2}\), on construit \(c= (\overbrace{10100}^{a}\underbrace{111}_{b})_{2}\)

    • Pour annuler les \(3\) bits de poids faible de \(166\) on faitd' abord un ET logique entre \(166\) et le nombre masque d'écriture binaire \((11111000)_{2}\) qui est \(248\), on obtient \((10100000)_{2}\).

    • Ensuite on décale de \(5\) bits vers la droite les bits de l'écriture binaire de \(234\) avec \(234>>5\) ce qui permet de les obtenir comme bits de poids faibles du nombre d'écriture binaire \((111)_{2}\) (c'est 7).

    • Enfin on effectue un OU logique entre \((\overbrace{10100}\underbrace{000})_{2}\) et \((\underbrace{111})_{2}\) et on obtient la réunion de tous ces bits : \(c=(\overbrace{10100}\underbrace{111})_{2}\) avec les trois bits de poids forts de \(b=234\) à la place des trois bits de poids faibles de \(a=166\).

    • On peut réaliser les mêmes opérations avec les opérateurs de division euclidienne // et % :

>>> a, b, nbits = 166, 234, 5                                              
>>> masque = (255 >> (8 - 5)) << (8 - 5)                                   
>>> bin(a), bin(b), bin(masque)                                            
('0b10100110', '0b11101010', '0b11111000')
>>> c = (a & masque) | (b >> 5)                                             
>>> bin(c)                                                                 
'0b10100111'
>>> c, bin(c)                                                              
(167, '0b10100111')
>>> (a & masque) == (a // 2**(8-5)) * 2 ** (8 - 5)                         
True
>>> (b >> 5) == (b // 2 ** 5)                                              
True
>>> c == (a // 2**(8-5)) * 2 ** (8 - 5) + (b >> 5)                         
True
>>> (c << 5) & 255 == (c % 2 ** (8 - 5)) * 2 ** 5                          
True

2. Objectifs⚓︎

Primaire : Créer les fonctions permettant :

  • de cacher une image dans une autre (avec possibilité de choisir le nombre de bits de poids forts à garder)
  • d'extraire une image cachée dans une autre

Secondaire :

  • Créer une interface permettant d'utiliser cet outil (en ligne de commande (facile), avec une interface graphique davistk (moyen), dans une appli web flask (difficile))
  • Extraire une image sans connaître le nombre de bits de poids forts gardés.
  • Proposer d'autres manières de 'cacher' les informations dans les bits de poids faibles.
  • Cacher d'autres informations que des images dans les bits de poids faibles.

3. Calendrier et contenu des rendus⚓︎

Date 1⚓︎

  • Description des structures de données (que doit-on représenter ? et comment va-t-on les représenter ?)
  • Liste des fonctions à créer avec leur description (docstring) et leurs tests (doctests)

Date 2⚓︎

  • Projet final et fonctionnel
  • Rapport de projet
  • Capsules audio

4. Quelques ressources⚓︎