Stéganographie avec Python et Stéganô
Par Cedric le 31/12/2011 15:20
Catégories : graphique,securite
Modules python : Python Imaging Library (PIL)
Version Python : 2.7
Introduction
Stéganographie
La stéganographie est l'art de la dissimulation. On dissimule généralement un message dans une photographie où dans un son (il existe de nombreuses techniques de stéganographies). Dans le cas de la stéganographie dite pure le message n'est pas crypté avant d'être caché.
Voici une technique de stéganographie simple connue des informaticiens:
$ zip text-to-hide.zip text-to-hide.txt $ cat song.ogg text-to-hide.zip > song-secret.ogg
Cette technique permet d'insérer un message secret à la fin par exemple d'un fichier audio. Le secret est donc caché et non pas crypté. Le problème est que la taille du fichier est modifiée car on ajoute les données à la fin. Nous allons faire un peu mieux en modifiant sensiblement le contenu du fichier qui cache le message sans en changer la taille.
L'objectif ici est d'étudier de simples techniques de stéganographies sur des images avec des examples basés sur le projet Stéganô.
Par la même occasion ce tutoriel permet d'aborder le module PIL ainsi que les générateurs. Les générateurs sont utilisés afin de générer des ensembles d'entiers (crible d'Eratosthénes, nombres de Mersenne, Carmichael, etc.) dans le but de sélectionner des pixels à modifier.
Nous allons voir trois techniques assez simples de stéganographie sur les images:
- utilisation de la composante rouge du pixel;
- méthode basée sur le bit de poids faible;
- méthode basée sur le bit de poids faible en utilisant des ensembles d'entiers pour la sélection des bits à altérer.
Stéganalyse par parité
La stéganalyse est à la stéganographie ce que la cryptanalyse est à la cryptographie. Son objectif est de déceller ce qui est imperceptible (visuellement dans notre cas), c'est-à-dire détecter/trouver un message caché.
Nous considérerons un pixel comme le triplet: (Red, Blue, Green). Une composante peut varier entre 0 et 255, par exemple (127, 224, 148).
Une technique connue de stéganalyse que nous allons utiliser, consiste tout simplement à remplacer les composantes pairs des pixels par 0 et les composantes impairs par 255. Il s'agit de la stéganalyse par parité. Par exemple le pixel (132, 247, 123) va devenir (0, 255, 255).
Pour illustrer cela, voici une image (sans texte caché) ainsi que l'image résultante d'une stéganalyse par parité.


$ steganalysis-parity -i ../examples/pictures/Montenach.png -o ~/Montenach-steg.png
Ces images seront utilisées comme références pour la suite. Nous allons utiliser cette steganalyse afin de comparer l'efficacité de nos deux techniques basées sur le bit de poids faible.
Voici une simple implémentation de cette stéganalyse par parité:
def steganalyse(img):
"""
Parity steganlysis.
"""
encoded = img.copy()
width, height = img.size
bits = ""
for row in range(height):
for col in range(width):
r, g, b = img.getpixel((col, row))
if r % 2 == 0:
r = 0
else:
r = 255
if g % 2 == 0:
g = 0
else:
g = 255
if b % 2 == 0:
b = 0
else:
b = 255
encoded.putpixel((col, row), (r, g , b))
return encoded
Installation de Stéganô
Afin de suivre les exemples on peut installer Stéganô
hg clone https://bitbucket.org/cedricbonhomme/stegano cd stegano sudo python setup.py install
Attention aux conséquences de cette installation, car Stéganô est en développement ;-)
Nous pouvons maitenant voir ces trois techniques.
Composante rouge du pixel
Cette technique consiste simplement à remplacer la composante rouge (un octet) de chaque pixel par la valeur ASCII d’un caractère du message à cacher. Ceci en partant du début du fichier. La transformation pour chaque pixel est donc la suivante:(R, G, B) -> (ord(ascii_character), G, B)
Par exemple: (74, 128, 234) va devenir: (ord(A), G, B) = (65, 128, 234) pour insérer le caractère 'A' dans un pixel de l'image.

Une simple implémentation:
def hide(img, message):
"""
Hide a message (string) in an image.
Use the red portion of a pixel (r, g, b) to
hide the message.
The red value of the first pixel is used to store the length of string.
"""
length = len(message)
# Limit length of message to 255
if length > 255:
return False
# Use a copy of image to hide the text in
encoded = img.copy()
width, height = img.size
index = 0
for row in range(height):
for col in range(width):
(r, g, b) = img.getpixel((col, row))
# first value is length of message
if row == 0 and col == 0 and index < length:
asc = length
elif index <= length:
c = message[index -1]
asc = ord(c)
else:
asc = r
encoded.putpixel((col, row), (asc, g , b))
index += 1
return encoded
La photo de Lenna ci-dessus contient un texte caché avec le petit algorithme présenté à l'instant. En faisant attention il est aisé de voir que la photo est modifiée. En effet, le texte a écrasé toutes les composantes rouges des premiers pixels de l'image (en haut à gauche de l'image).
$ python stegano/basic.py reveal -i ~/lenna-basic.png Cette technique de stéganographie est un peu trop simple. Elle altère beaucoup les pixels de la photo.
Une composante, la rouge, est donc totalement modifiée. De ce fait, autant de pixels que le message comporte de caractères sont grossièrement altérés.
Vous vous en doutez, il existe au moins une meilleure façon de dissimuler un message dans une image. Par exemple la méthode bien connue LSB (Least Significant Bit) que nous allons voir dans la partie suivante.
La technique Least Significant Bit
Les données sont insérées à la place des bits les moins importants. Pour chaque composante RGB nous faisons varier uniquement le bit de poids faible. Ainsi, la transformation est la suivante:
(R, G, B) = (00000000, 00000001, 00000000) -> (R, G, B) = (00000001, 00000000, 00000001)
Pour coder un caractère ASCII, nous aurons besoin de deux pixels et deux composantes. Cela fait plus de pixels à modifier qu'avec la méthode précédente, mais les changements sont minimes. La modification du bit de poids faible a peu d'impact sur la valeur d'une composante.
Pour tester, il faut utiliser la commande slsb de Stéganô:
$ slsb --hide -i ./pictures/Lenna.png -o ./pictures/Lenna_enc.png -m 'Hello World'
$ slsb --reveal -i ./pictures/Lenna_enc.png
Hello World
$ slsb --help
Usage: slsb [options]
Options:
--version show program's version number and exit
-h, --help show this help message and exit
--hide Hides a message in an image.
--reveal Reveals the message hided in an image.
-i INPUT_IMAGE_FILE, --input=INPUT_IMAGE_FILE
Input image file.
-o OUTPUT_IMAGE_FILE, --output=OUTPUT_IMAGE_FILE
Output image containing the secret.
-m SECRET_MESSAGE, --secret-message=SECRET_MESSAGE
Your secret message to hide (non binary).
-f SECRET_FILE, --secret-file=SECRET_FILE
Your secret to hide (Text or any binary file).
-b SECRET_BINARY, --binary=SECRET_BINARY
Output for the binary secret (Text or any binary
file).
Stéganalyse de la méthode LSB
$ slsb --hide -i ./examples/pictures/Montenach.png -o ~/Montenach-enc-gen.png -f ./examples/lorem_ipsum.txt # Steganalysis of the image with hidden text (LSB only) $ steganalysis-parity -i ~/Montenach-enc.png -o ~/Montenach-enc-steg.png


Sur l'image de droite on peut observer une certaine régularité à l'endroit où le texte a été caché.
Avec cette technique seul un bit de chaque composante est modifié, on ne peut pas déceller de modications à l'oeuil nu. Ce n'est qu'après avoir effectué une stéganalyse que l'on peut supposer qu'un message est caché.
Le majeure problème est que nous commençons systématiquement au début de l'image et que les pixels utilisés pour cacher l'information se suivent. Il serait bien de pouvoir choisir les bits où seront cachés les informations, puis de pouvoir les retrouver afin de réveller le message caché. Pour ce faire, nous allons utiliser la méthode LSB basée sur des ensembles d'entiers.
La technique Least Significant Bit basée sur des ensembles d'entiers
Le procédé est globalement le même que pour la technique présenté à la section précédente. La différence réside dans la sélection des pixels qui seront utilisés pour cacher les informations.
Des ensembles d'entiers sont utilisés afin de générer les listes de pixels à modifier (au lieu de modifier successivement chaque pixels en commençant au début du fichier). Ainsi les pixels modifiés ne se suivront plus.
Ces ensembles seront simplement construits grâce à des générateurs Python.

Cachons un message en générant un ensemble de points basé sur le crible d'Eratosthénes. Puis essayons de retrouver le message avec d'autres ensemble d'entiers.
Il faut utiliser la commande slsb-set de Stéganô.
$ slsb-set --hide -i examples/pictures/Lenna.png -o ~/lenna-aime-python.png --generator eratosthenes -m "Python c'est bon." $ slsb-set --reveal -i ~/lenna-aime-python.png --generator eratosthenes Python c'est bon. $ slsb-set --reveal -i ~/lenna-aime-python.png --generator mersenne Impossible to detect message. $ slsb-set --reveal -i ~/lenna-aime-python.png --generator fermat Impossible to detect message. $ slsb-set --reveal -i ~/lenna-aime-python.png --generator carmichael Impossible to detect message.
Il faut connaître le bon ensemble de points pour retrouver le message.
Voici la liste des générateurs déjà disponibles. Nous venons d'utiliser le crible d'Eratosthénes pour cacher le message, son implémentation:
def eratosthenes():
"""
Generate the prime numbers with the sieve of Eratosthenes.
"""
d = {}
for i in itertools.count(2):
if i in d:
for j in d[i]:
d[i + j] = d.get(i + j, []) + [j]
del d[i]
else:
d[i * i] = [i]
yield i
De préférence la fonction utilisée doit être injective (par exemple ne pas utiliser Syracuse, à moins de jouer avec les modulo) et ne pas croître trop vite, comme c'est le cas d'Ackermann.
Si vous désirez juste tester Syracuse et Ackermann:
def ackermann(m, n):
"""
Croît beacoup trop vite.
"""
if m == 0:
return n + 1
elif n == 0:
return ackermann(m - 1, 1)
else:
return ackermann(m - 1, ackermann(m, n - 1))
def syracuse(l=15):
"""
Répartition intéressante mais non injectif.
"""
y = l
while True:
yield y
q,r = divmod(y,2)
if r == 0:
y = q
else:
y = 3*y + 1
Évidemment, si on utilise la fonction identité (f(x) = x) afin de générer l'ensemble de pixels, la commande slsb-set sera équivalente à la commande slsb de Stéganô. Pour s'en convaincre on pourra tester les commandes:
# Hide the message - LSB with a set defined by the identity function (f(x) = x). slsb-set --hide -i examples/pictures/Montenach.png -o ~/enc-identity.png --generator identity -m 'I like steganography.' # Hide the message - LSB only. slsb --hide -i examples/pictures/Montenach.png -o ~/enc.png -m 'I like steganography.' # Check if the two generated files are the same. sha1sum ~/enc-identity.png ~/enc.png # The output of slsb is given to slsb-set. slsb-set --reveal -i ~/enc.png --generator identity # The output of slsb-set is given to slsb. slsb --reveal -i ~/enc-identity.png
Stéganalyse de la méthode LSB basée sur les ensembles d'entiers
À nouveau générons une image contenant un message caché et effectuons une stéganalyse de l'image obtenue.
$ slsb-set --hide -i ./examples/pictures/Montenach.png -o ~/Montenach-enc-gen.png --generator eratosthenes -f ./examples/lorem_ipsum.txt # Steganalysis of the image with hidden text (LSB + Eratosthenes) $ steganalysis-parity -i ~/Montenach-enc-gen.png -o ~/Montenach-enc-gen-steg.png


Il est maintenant bien plus difficile de détecter une anomalie dans la stéganalyse de l'image car les pixels altérés sont répartis suivant la suite sélectionnée. La qualité du résultat dépend donc du générateur utilisé pour obtenir l'ensemble d'entiers.
Conclusion
Voilà c'est déjà terminé. J'espère que cette petite introduction à la stéganographie avec Python vous aura convaincu que Python (avec le module PIL) est parfait pour réaliser des traitements intéressants et simples sur des images. Si par la même occasion vous avez appris quelque chose, c'est super!
Ressources
- sur Wikipédia;
- tutoriel de Stéganô;
- d'autres exemples avec Stéganô;
- les billets sur mon blog à propos de la stéganographie.




