mercredi 5 janvier 2011

Padding BMP

Suite à mon introduction sur le concept de la stéganographie, voilà un exemple appliqué sur les images bitmap plus communément appelées BMP. Cette technique assez basique fonctionne exclusivement sur les images BMP car elle utilise une des particularités de ce format : le padding (le bourrage en français). La norme BMP dit que chaque ligne de l'image doit être codée par un nombre d'octets multiple de 4. Si ce n'est pas il faut combler le manque d'octets avec la valeur nulle. On appelle ça le padding.
  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.

3 commentaires:

  1. Ton article est clair et instructif. Ton code est bien utile. Merci.

    RépondreSupprimer
  2. Salut ymvunjq

    Pour 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:];

    RépondreSupprimer