Stéganographie avec Python et Stéganô

Document Actions

Par Cedric le 31/12/2011 15:20

Une petite introduction à la stéganographie avec Python.

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é.

Commande à exécuter avec Stéganô pour obtenir 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

Python.org : Le site officiel du langage Python.
Zope.org : Le site web officiel de Zope.
Daily Python-URL : Actus de l'univers Python.
Tribute to Zyons : Zyons notre ami et membre fondateur de l'Afpy, nous quittait en 2005.