On se retrouve donc avec un fichier possédant des octets qui n'apportent aucune information et qui sont même complètement ignorés par la totalité des programmes. Il est donc possible d'utiliser ce padding pour cacher des données à l'intérieur. L'image apparaitra totalement inchangée dans votre viewer préférée. Cette technique très simple n'est possible que si une ligne n'est pas codée avec un nombre d'octets multiple de 4 bien entendu, sinon le padding sera inexistant. On peut donc écrire la formule suivante qui donnera la taille du nombre d'octet que l'on peut cacher dans une image BMP en utilisant cette technique :
Nb_Bytes = L*((4-((C*S)%4))%4) L : Nombre de lignes de l'image C : Nombre de colonnes de l'image S : Taille en octet d'un pixel
Petit exemple : la première image ici est l'image originale.
La deuxième image contient un message caché de 173 octets (on aurait pu aller jusqu'à 508 !).
Comme on peut le voir elles ont exactement le même aspect et font exactement le même poids. Le programme python suivant permet de savoir si oui ou non l'image que l'on passe en paramètre ne contient que des octets nuls dans son padding (attention à n'utiliser que sur des images BMP non indexées).
#!/usr/bin/env python # -*- coding: utf-8 -*- import sys,struct class BMP: def __init__(self,path): f = open(path,"rb") data = f.read() f.close() names = ["magick","size_file","creator1","creator2","start_data","header_image","width","height","plan","colors","compression","raw_size","horizontal","vertical","color_palette","important_colors"] values = struct.unpack('<2sLHHLLLLHHLLLLLL',data[:54]) self.data = data[54:] self.header = dict() for i in range(len(names)): self.header[names[i]] = values[i] # On stocke quelques éléments pour ne pas être obligé de toujours les calculer self.size_pixel = self.header['colors']/8 self.padding_size = self.padding_size() # Retourne la taille du padding par ligne def padding_size(self): return ((4-(self.header['width']*self.size_pixel)%4)%4) # Retourne un tableau contenant le padding dans le fichier bmp def padding(self): r = [] if self.padding_size != 0: start = 0 for row in range(self.header['height']): start += (self.size_pixel*self.header['width']) r += struct.unpack('c'*self.padding_size,self.data[start:start+self.padding_size]) start += self.padding_size return r if len(sys.argv) != 2: print "Manque le fichier bmp" sys.exit(1) bmp = BMP(sys.argv[1]) print "Taille padding : %i" % (bmp.padding_size*bmp.header["height"]) for e in bmp.padding(): if e != 0: print "Padding non nul !" sys.exit(0) print "Padding nul" sys.exit(0)
Cette technique à l'avantage de ne pas du tout altérer l'image, et de ne pas modifier sa taille. Cependant, outre le fait qu'elle soit très basique, il faut bien faire attention aux informations cachées dans le padding. La simple ouverture de l'image dans un fichier texte peut révéler des parties de l'information cachée, qui se trouvera en clair. Un élément important avec cette technique à préciser est la structure d'un fichier BMP. Contrairement à la plupart des formats d'images les lignes sont stockées dans l'ordre inverse : la dernière ligne est codée, puis l'avant dernière, ... et enfin la première. De plus (mais ça n'a pas réellement d'impact ici), le codage d'un pixel se fait dans l'ordre bleu-vert-rouge, contrairement aux rouge-vert-bleu habituels.
Ton article est clair et instructif. Ton code est bien utile. Merci.
RépondreSupprimerSalut ymvunjq
RépondreSupprimerPour information, suite au changement d’hébergeur il y a une petite coquille dans le code sur le struct :
Ligne 13 corrigé :
values = struct.unpack('<2sLHHLLLLHHLLLLLL',data[:54]);
self.data = data[54:];
Corrigé, merci UfoX ;)
Supprimer