Un grosse astuce en matière de stéganographie consiste à dissimuler des données dans un fichier image. Dans le cas d'un JPEG, il s'agira de jouer avec les segments.
La technique de base pour trouver ces données cachées est assez pénible, puisqu'elle consiste à passer en revue le contenu du fichier avec un éditeur hexadécimal tel que l'incontournable HxD.
Fort heureusement, il est possible d'automatiser cela en quelques lignes de Python...
La liste des segments possibles dans un JPEG peut être trouvée ici. Comme indiqué dans la colonne "Payload" du tableau des marqueurs communs, tout segment débute par un marqueur sur deux octets ff:xx.
Dans ces conditions, le programme à écrire est des plus simples, puisqu'il consiste à vérifier si un tel marqueur se trouve bien à l'endroit attendu, et à rapporter l'offset dans le fichier auquel ce marqueur est trouvé, ainsi que la nature du segment auquel il correspond.
Toutefois, il y a une petite subtilité. Pour la saisir, il faut consulter ici des explications complémentaires qui renseignent sur la structure d'un fichier JFIF. Il apparaît qu'après un segment SOS, les données suivent sans aucune indication de la taille qu'elles occupent. Dès lors, comment en détecter la fin ?
On trouve ici une réponse : il suffit de rechercher la première occurrence d'une séquence ff:xx où xx est différent de 0x00. Bref, un potentiel marqueur, qui peut d'ailleurs très bien être celui de la fin du fichier.
Le programme présenté ici en tient compte pour dresser la liste de tous les segments que contient un JPEG. Il offre ainsi un moyen simple pour visualiser si des segments dignes d'intérêt sont présents dans le fichier.
Un exemple de résultat sera :
00000002: Application specific (0) (APP0) (ff:e0) of 16 bytes 00000020: Application specific (1) (APP1) (ff:e1) of 9494 bytes 00009516: Comment (COM) (ff:fe) of 52668 bytes 00062186: Application specific (1) (APP1) (ff:e1) of 5377 bytes 00067565: Application specific (2) (APP2) (ff:e2) of 3160 bytes 00070727: Define Quantization Table(s) (DQT) (ff:db) of 67 bytes 00070796: Define Quantization Table(s) (DQT) (ff:db) of 67 bytes 00070865: Start Of Frame (progressive) (SOF2) (ff:c2) of 17 bytes 00070884: Define Huffman Table(s) (DHT) (ff:c4) of 30 bytes 00070916: Define Huffman Table(s) (DHT) (ff:c4) of 28 bytes 00070946: Start Of Scan (SOS) (ff:da) of 12 bytes 00098133: Define Huffman Table(s) (DHT) (ff:c4) of 53 bytes 00098188: Start Of Scan (SOS) (ff:da) of 8 bytes 00136560: Define Huffman Table(s) (DHT) (ff:c4) of 71 bytes 00136633: Start Of Scan (SOS) (ff:da) of 8 bytes 00187649: Define Huffman Table(s) (DHT) (ff:c4) of 72 bytes 00187723: Start Of Scan (SOS) (ff:da) of 8 bytes 00243717: Define Huffman Table(s) (DHT) (ff:c4) of 87 bytes 00243806: Start Of Scan (SOS) (ff:da) of 8 bytes 00383518: Define Huffman Table(s) (DHT) (ff:c4) of 41 bytes 00383561: Start Of Scan (SOS) (ff:da) of 8 bytes 00463948: Start Of Scan (SOS) (ff:da) of 12 bytes 00468056: Define Huffman Table(s) (DHT) (ff:c4) of 40 bytes 00468098: Start Of Scan (SOS) (ff:da) of 8 bytes 00524666: Define Huffman Table(s) (DHT) (ff:c4) of 40 bytes 00524708: Start Of Scan (SOS) (ff:da) of 8 bytes 00582313: Define Huffman Table(s) (DHT) (ff:c4) of 40 bytes 00582355: Start Of Scan (SOS) (ff:da) of 8 bytes
Par ailleurs, le programme peut écrire chaque segment dans un fichier dont le nom mentionne les offets du premier et du dernier octet :
18 ch15. (000002-000019).bin 9 496 ch15. (000020-009515).bin 52 670 ch15. (009516-062185).bin 5 379 ch15. (062186-067564).bin 3 162 ch15. (067565-070726).bin 69 ch15. (070727-070795).bin 69 ch15. (070796-070864).bin 19 ch15. (070865-070883).bin 32 ch15. (070884-070915).bin 30 ch15. (070916-070945).bin 14 ch15. (070946-070959).bin 55 ch15. (098133-098187).bin 10 ch15. (098188-098197).bin 73 ch15. (136560-136632).bin 10 ch15. (136633-136642).bin 74 ch15. (187649-187722).bin 10 ch15. (187723-187732).bin 89 ch15. (243717-243805).bin 10 ch15. (243806-243815).bin 43 ch15. (383518-383560).bin 10 ch15. (383561-383570).bin 14 ch15. (463948-463961).bin 42 ch15. (468056-468097).bin 10 ch15. (468098-468107).bin 42 ch15. (524666-524707).bin 10 ch15. (524708-524717).bin 42 ch15. (582313-582354).bin 10 ch15. (582355-582364).bin
Le code du programme en Python est le suivant :
def listSegments (path, filename, dump=False): segments = { 0xd8:'Start Of Image (SOI)', 0xc0:'Start Of Frame (baseline DCT) (SOF1)', 0xc2:'Start Of Frame (progressive) (SOF2)', 0xc4:'Define Huffman Table(s) (DHT)', 0xdb:'Define Quantization Table(s) (DQT)', 0xdd:'Define Restart Interval (DRI)', 0xda:'Start Of Scan (SOS)', 0xd0:'Restart (0) (RST0)', 0xd1:'Restart (1) (RST1)', 0xd2:'Restart (2) (RST2)', 0xd3:'Restart (3) (RST3)', 0xd4:'Restart (4) (RST4)', 0xd5:'Restart (5) (RST5)', 0xd6:'Restart (6) (RST6)', 0xd7:'Restart (7) (RST7)', 0xe0:'Application specific (0) (APP0)', 0xe1:'Application specific (1) (APP1)', 0xe2:'Application specific (2) (APP2)', 0xe3:'Application specific (3) (APP3)', 0xe4:'Application specific (4) (APP4)', 0xe5:'Application specific (5) (APP5)', 0xe6:'Application specific (6) (APP6)', 0xe7:'Application specific (7) (APP7)', 0xe8:'Application specific (8) (APP8)', 0xe9:'Application specific (9) (APP9)', 0xea:'Application specific (10) (APP10)', 0xeb:'Application specific (11) (APP11)', 0xec:'Application specific (12) (APP12)', 0xed:'Application specific (13) (APP13)', 0xee:'Application specific (14) (APP14)', 0xef:'Application specific (15) (APP15)', 0xfe:'Comment (COM)', 0xd9:'End Of Image (EOI)' } file = open (f'{path}{filename}', 'rb') data = file.read () file.close () if (data[0] != 0xff) or (data[1] != 0xd8): print (f'File does not start with ff:d8 ({data[0]:02x}:{data[1]:02x} found)') return False if (data[len (data) - 2] != 0xff) or (data[len (data) - 1] != 0xd9): print (f'File does end with ff:d9 ({data[len (data) - 2]:02x}:{data[len (data) - 1]:02x} found)') return False i = 2 while True: if (i + 5) > len (data): print (f'Error at {i:08d}: Too few remaining bytes for a complete segment (need 5 bytes or more)') return False if data[i] != 0xff: print (f'Error at {i:08d}: Start of segment (ff) not found ({data[i]:02x} found)') return False if data[i + 1] not in segments: print (f'Error at {i:08d}: Unknown segment({data[i]:02x})') size = (data[i + 2] << 8) | data[i + 3] if (i + 2 + size) > len (data): print (f'Error at {i:08d}: Too few remaining bytes for this segment (need {size} bytes)') return False print (f'{i:08d}: {segments[data[i + 1]]} ({data[i]:02x}:{data[i + 1]:02x}) of {size} bytes') if dump: file = open (f'{path}{filename[0:len (filename) - 4]} ({i:06d}-{(i + 2 + size -1):06d}).bin', 'wb') file.write (data[i:i + 2 + size]) file.close () if data[i + 1] == 0xda: i += 2 + size while True: if data[i] == 0xff: if (i + 1) >= len (data): print (f'Error at {i:08d}: Too few remaining bytes for a segment after the data section') return False if data[i + 1] == 0x00: i += 1 continue if data[i + 1] == 0xd9: return True break else: if (i + 1) >= len (data): print (f'Error at {i:08d}: Too few remaining bytes for a segment after the data section') return False i += 1 else: i += 2 + size return True path = '../challenges/' listSegments (path, 'ch15.jpg')
Il est probable que le programme ne fonctionne pas bien si le fichier JPEG contient un marqueur de segment DRI (ff:dd) ou RSTn (ff:dn), car ces marqueurs ne sont pas suivis d'un couple d'octets précisant une taille. Toutefois, ce n'était pas requis dans le contexte où il a été élaboré. A vous donc d'adapter le code si nécessaire...