Hackflash #2 : Lister les segments d’un JPEG

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.
Le flag est dans le JPEG...
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:xxxx 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...
Hackflash #2 : Lister les segments d’un JPEG