Vous êtes sur la page 1sur 4

Stéganographie élémentaire avec un fichier BMP

V. Bénony A. Sedoglavic
Université des Sciences et Technologies de Lille
2003–2004

1 Présentation

La stéganographie (du grec steganos, couvert et graphein, écriture) c’est l’art de cacher un message au
sein d’un autre message de caractère anodin, de sorte que l’existence même du secret en soit dissimulée1 .

Le but de ce projet sera de dissimuler de l’information (sous la forme d’une chaı̂ne de caractères
quelconque) dans une image stockée en BMP. Pour ce faire, nous allons tout d’abord présenter le format
de fichier BMP2 .

2 Le format BMP 24 bits non compressé


Nous nous occuperons uniquement dans cette section des fichiers BMP non compressés et en 24 bits
puisqu’il s’agit du format le plus simple à manipuler.
Afin d’obtenir ce type de format à partir d’une image sous un autre format, vous pouvez utilisez un
utilitaire comme gimp (fonction Save as).
Le fichier est constitué de deux parties distinctes : le header et les données non compressées.

2.1 Header BMP


Le header est une représentation binaire des informations concernant une image. Cet en-tête contient
des informations sur la taille de l’image en largeur et en hauteur (en pixels), le nombre de couleurs ou
encore le type d’encodage des données.
C’est grâce à cet en-tête que nous allons pouvoir déterminer si les informations d’un fichier BMP sont
conformes à ce que l’on attend en entrée de notre projet.
L’entête est constitué d’une série d’entiers, codés sur 16 ou 32 bits (respectivement 2 ou 4 octets).
Ces entiers sont disposés dans un ordre précis :

Nom du champs Longueur en octets Signification

Identifier 2 Contient toujours l’octet ‘B’ suivit de l’octet ‘M’


FileSize 4 Taille totale du fichier en octets
Reserved 4 Champs réservé, doit être égal à 0
DataOffset 4 Nombre d’octets séparant le début du fichier des données de l’image
HeaderSize 4 Taille en octets de l’header
Width 4 Largeur de l’image en pixels
Height 4 Hauteur de l’image en pixels
Planes 2 Nombre de plans
BitsPerPixels 2 Nombre de bits nécessaires pour représenter un pixel
Compression 4 Type de compression
BitmapDataSize 4 Taille en octets des données de l’image
HResolution 4 Résolution horizontale de l’image en pixels par mètre
VResolution 4 Résolution verticale de l’image en pixels par mètre
Colors 4 Nombre de couleurs dans l’image
ImportantColors 4 Nombre de couleurs importantes
1 http ://lwh.free.fr/pages/algo/crypto/steganographie.htm
2 http ://www.wotsit.org/filestore/bmpfrmat.zip

1
Tous les champs ne nous concernent pas ; seuls les champs Identifier, DataOffset, Width, Height,
BitsPerPixels, Compression et BitmapDataSize seront réellement nécessaires.

Exercice 1 :
Définissez un type BMP HEADER contenant toutes les informations d’une en-tête BMP. Écrivez ensuite une
fonction permettant de lire l’entête d’un fichier BMP, et dont le prototype sera :
int loadBitmapHeader(FILE *fichier, BMP_HEADER *entete);
On considère donc que le fichier à donc été ouvert à l’extérieur de la fonction, et que le pointeur
retournée par une fonction fopen est donnée en paramètre (pointeur fichier). La fonction aura pour
but de remplir la structure pointée par entete. La fonction retournera 1 si l’opération s’est effectuée
normalement, et 0 sinon.

Exercice 2 :
Écrivez une fonction permettant de vérifier la validité d’un en-tête de fichier BMP. La fonction devra
vérifier :
1. qu’il s’agit bien d’un fichier BMP (en vérifiant que les deux premiers octets soient bien ‘B’ et ‘M’)
2. qu’il s’agit d’un fichier non compressé (le champs Compression doit être égal à 0)
3. que le fichier est bien un fichier en 24 bits (le champs BitPerPixels doit être égal à 24).
4. que la taille des données (BitmapDataSize) est bien égal à Width × Height × 3.
Le prototype de la fonction sera le suivant :
int isBitmapHeaderCorrect(BMP_HEADER *header);
La fonction retournera 1 si tout est correct, et 0 sinon.

3 Organisation des données


Les données des fichiers BMP non compressés en 24 bits sont très simples à interpréter. En effet,
chaque élément de l’image (ou pixel pour picture element) est codé sur 3 octets (d’où 3 × 8 = 24 bits).
Chacun de ces octets représente la proportion de chacune des trois couleurs fondamentales composant
toute couleur.

Pixel = (Rouge, Vert, Bleu).


Le taux de rouge, de vert et de bleu est codé pour chaque composante sur 8 bits (donc entre 0 et 255
compris).
Nous allons utiliser le fait que l’oeil humain à beaucoup de mal à distinguer des nuances de couleurs
très proches l’une de l’autre pour cacher de l’information. Si l’on considère deux pixels voisins dans une
image, et dont les représentations sont les suivantes :
Pixel1 = (10010110(2) , 11010101(2) , 00101100(2) ),
Pixel2 = (10010110(2) , 11010100(2) , 00101100(2) ).
Alors, il est quasiment impossible de détecter une différence de couleur à l’oeil nu entre ces deux
couleurs. Or, elle diffèrent bien l’une de l’autre, puisque leurs taux de vert (deuxième composante) sont
différents. Nous pouvons donc conclure que modifier le bits de poids le plus faible dans une composante
chromatique n’a quasiment aucun impact sur notre perception visuelle de l’image.
Le principe du codage est alors le suivant ; si l’on considère un message m à cacher dans une image,
on note mi le iième octet de ce message, et [mi ]j le jième bit de ce iième octet (les octets sont numérotés
à partir de 0, et les bits sont numéroté 7 pour le bits de poids le plus fort, et 0 pour le bit de poids le plus
faible). Il est alors facile de définir une suite un qui contient la suite des bits composant le message m :

[m n8 ](7−(n%8)) si n ∈ [0 . . . 8 × strlen(m)],
un =
0 sinon
avec x/y la division entière de x par y, et x%y le reste de la division entière de x par y.

2
Exercice 3 :
Écrivez une fonction permettant de calculer un pour tout n connaissant le message m. Le prototype de
la fonction sera :
int getBit(char *m, int n);

Les pixels composant l’image se trouvent, dans le fichier, juste après l’en-tête. Le nombre d’octets
composant ces données est spécifié dans l’en-tête sous le champs BitmapDataSize.

Exercice 4 :
Écrivez une fonction permettant de lire les données d’un fichier BMP. Le header du fichier (qui aura
au préalablement été lu grâce à un appel à la fonction loadBitmapHeader) sera donnée en paramètre à
cette fonction. Elle retournera un pointeur de type unsigned char * qui pointera une zone en mémoire
allouée par cette fonction, et qui contiendra la suite des pixels lus à partir du fichier.
Le prototype de cette fonction sera :
unsigned char * loadBitmapDatas(FILE *fichier, BMP_HEADER *header);
La fonction retournera NULL si elle n’arrive pas à lire les données du fichier BMP. On considère que
la validité du fichier (via la fonction isBitmapHeaderCorrect) aura été vérifié.

Exercice 5 :
Écrivez une fonction permettant de sauvegarder un fichier BMP grâce aux informations d’un header qui
sera donné en paramètre, et d’un pointeur sur une zone en mémoire contenant les pixels de l’image. Le
prototype sera :

int saveBitmapDatas(FILE *fichier, BMP_HEADER *header, unsigned char *pixels);

La fonction retournera 1 si tout s’est passé correctement et 0 sinon.

4 Fonction de permutation
Afin de ne pas placer le message dans les octets les uns à la suite des autres dans le fichier BMP
résultant, nous allons écrire une fonction capable de construire des fonctions de permutation définie par
un paramètre que l’on appellera la clé du système. Cette fonction de permutation nous permettra de
savoir quels sont les octets de l’image à modifier, et dans quel ordre. Pour représenter f , nous allons
utiliser un tableau de N entiers, où la iième case du tableau contiendra la valeur de f (i).
Une fonction de permutation est une fonction agissant d’un ensemble K = [0 . . . N − 1] d’entiers vers
ce même ensemble K, et dont le but est de définir une bijection.

f : K −→ K

∀(x, y) ∈ K2 , f (x) = f (y) ⇐⇒ x = y


Pour arriver à notre but, nous allons définir f comme la plus simple permutation qui soit, c’est à
dire :
f (x) = x.
C’est à dire que dans le tableau représentant cette fonction, nous allons placer dans la ii ème case la
valeur i. Ensuite, nous allons échanger certaines cases de ce tableau de manière aléatoire. Le processus
est le suivant :

Faire N fois
tirer au hasard deux entiers i et j
les contraindre à l’ensemble [0..N − 1] en calculant
i = i modulo N
j = j modulo N
échanger les cases i et j du tableau représentant la fonction

3
Le problème est qu’il faut être capable de construire à nouveau cette même fonction à tout moment.
Or la notion de hasard ne nous permet pas de connaı̂tre à l’avance la fonction qui va être construite. Pour
contraindre le hasard, nous allons utiliser la fonction C srand(unsigned int seed) afin de modifier
l’état initial du générateur pseudo-aléatoire. Il suffira donc, avant tout appel à la fonction de génération
d’aléa rand d’appeler la fonction srand, paramétrée par la clé du système.

Exercice 6 :
Écrivez une fonction capable de construire une fonction de permutation selon le principe évoqué plus
haut. La fonction retournera un pointeur sur la fonction construite (c’est donc à cette fonction d’allouer
la place en mémoire nécessaire à la représentation de la fonction de permutation)3 . Le prototype sera le
suivant :

int * createPermutationFunction(int N, unsigned int key);

La fonction retourne donc un tableau d’entiers représentant la fonction de permutation construite.

5 Camouflage du message dans l’image


Nous allons maintenant modifier une image BMP afin d’y cacher un texte. Le processus est alors le
suivant :

pour i allant de 0 à (1 + strlen(m)) × 8


calculer b = getBit(m, i)
calculer o = permutation[i] le numéro de l’octet des pixels qui sera modifié
effacer le bit de poids faible du oi ème octet du tableau de pixels
remplacer ce bit par b

Le fait de placer (1 + strlen(m)) × 8 bits dans l’image permet de sauvegarder tous les bits du
message m (soit strlen(m) × 8 bits) plus le zéro qui termine toute chaı̂ne de caractères en C (8 bits
supplémentaires).

Exercice 7 :
Écrivez une fonction capable d’ouvrir un fichier BMP, lire les informations concernant l’en-tête et le
tableau de pixels, cacher un message, et sauvegarder un fichier résultant. Le nom des fichiers à lire et
créer, ainsi que le message et la clé permettant de construire la fonction de permutation seront donnée
en paramètre à la fonction. Le prototype sera :

int hideMessage(char *source, char *dest, char *message, unsigned int key);

source et dest représentent donc les noms des fichiers à respectivement lire, et créer. La fonction
retournera 1 si tout s’est passé correctement, et 0 sinon.

Exercice 8 :
Déterminez l’algorithme capable d’effectuer l’opération inverse, puis écrivez la fonction correspondante
en C4 . Le prototype sera le suivant :
char * retrieveMessage(char *source, unsigned int key);
source sera le nom du fichier BMP à lire.

3 pour les étudiants de mon groupe : la fonction que je vous ai demandé en séance de TP est à modifier (très légèrement)

afin d’allouer la mémoire nécessaire AVANT de construire la fonction


4 Il suffit de construire la fonction de permutation de la même manière, puis de concaténer les bits de poids faible afin

de reconstruire le message jusqu’à ce que l’on ai 8 zéros de suite (fin de chaı̂ne).