Hackflash #7 : Extraire les PNG et les JPEG d’un binaire (même partiels)

Lors d'un challenge de forensics, il arrive souvent d'avoir à extraire des informations d'un dump de la mémoire, un gros fichier binaire.
Evidemment, il existe des outils pour cela, et le plus simple est sans doute d'utiliser un bon vieux binwalk. Toutefois, il peut être nécessaire d'avoir le faire soi-même.
Extraire les PNG et les JPEG d'un binaire (même partiels)
La solution la plus simple est alors encore de rédiger un programme en Python.
Le programme suivant recherche les PNG et le JPEG dans un binaire. Il dumpe les PNG et JPEG même partiels, c'est-à-dire dont le header n'a pas de trailer correspondant. Cette dernière situation survient quand après avoir trouvé un header, le programme tombe sur un autre header avant de tomber sur un trailer.
Le fichier binaire est lu bloc par bloc pour ne pas encombrer la mémoire s'il est volumineux. La taille d'un bloc, BLOCK_SIZE, est ajustable, pourvu qu'elle soit supérieure ou égale à la longueur du plus long des headers et trailers.
# Extraction des PNG / JPEG identifiés comme des séquences débutant par un HEADER et suivi d'un TRAILER. Les fichiers partiels, c'est-à-dire les séquences débutant par un HEADER et s'arrêtant à un autre HEADER sans tomber sur un TRAILER entre temps sont aussi extraits (nom de fichier précédé par "_"). Doit tenir compte du fait qu'un header peut devoir être abandonné car avant de tomber ensuite sur un trailer, on tombe sur d'autres headers. Il faudrait tester tous les cas avec des données, header et trailer fictifs, mais je n'ai pas envie. Ca semble fonctionner en l'état...

# Attention à qui voudrait adapter ce code pour rechercher d'autres motifs que les headers et trailers de PNG et JFIF dans le binaire. A la fin de la recherche dans une plage, la recherche est reprise dans une nouvelle plage qui commence par un morceau de la fin de la plage où la recherche vient de se dérouler. On risque donc de trouver dans la nouvelle plage des motifs qui ont déjà été trouvés. Par exemple, si les motifs recherché sont "x" et "abcd", si la plage se termine par "x ab", alors la recherche a permis de trouver le "x", et elle reprend dans une plage qui débute par "x ab" pour ménager la possibilité de trouver "abcd", si bien que le "x" va être trouvé de nouveau...

def dumpBytes (filename, data):
	t = open (filename, 'wb')
	t.write (data)
	t.close ()

BLOCK_SIZE = 10 * 1024 * 1024	# Supposé au moins égal à la longueur du plus long de patterns
# PNG : Décommenter les trois lignes qui suivent pour chercher des PNG
HEADER = b'\x89\x50\x4E\x47\x0D\x0A\x1A\x0A'
TRAILER = b'\x00\x00\x00\x00\x49\x45\x4E\x44\xAE\x42\x60\x82'
TYPE = 'PNG'
EXTENSION = 'png'
# JPEG : Décommenter les trois lignes qui suivent pour chercher des JPEG
HEADER = b'\xFF\xD8\xFF\xE0\x00\x10\x4A\x46\x49\x46'
TRAILER = b'\xff\xd9'
TYPE = 'JFIF'
EXTENSION = 'jpg'

PATH = 'extracts/'
data = bytearray ()

f = open ('dump.raw', 'rb')
block = f.read (BLOCK_SIZE)
data = bytearray ()
data.extend (block)		# L'idée sera de réduire data chaque fois que c'est possible pour libérer de la mémoire
offset = 0
s = 0
h = -1
n = 0
while True:
	if h != -1:	# ie : HEADER déjà trouvé
		if (s + len (TRAILER)) > len (data):
			# Pas assez de données pour trouver un TRAILER entier : rajouter un bloc au données depuis le HEADER (nécessairement au début de data)
			block = f.read (BLOCK_SIZE)
			if (len (data) - s + len (block)) < len (TRAILER):	# Pas assez de données pour trouver un TRAILER entier
				break
			data.extend (block)
		t = data.find (TRAILER, h + len (HEADER))
		if t != - 1:
			# Chercher si un autre HEADER entier ne se trouve pas avant le TRAILER, donc entre h + len (HEADER) et t - len (HEADER) inclus
			_h = h + len (HEADER)
			while True:
				_h = data.find (HEADER, _h, t - len (HEADER) + 1)
				if _h == -1:
					break
				# Dumper le fichier partiel
				dumpBytes (f'{PATH}_{n:04d}.{EXTENSION}', data[h:_h])
				print (f'[{offset + h:08d}-{offset + _h:08d}]: {TYPE} dumped as {PATH}_{n:04d}.{EXTENSION} ({_h - h} bytes)')
				n = n + 1
				# Adopter nouveau TRAILER
				h = _h
				_h = _h + len (HEADER)	# Un HEADER ne contient pas le début h'un autre HEADER
			# Dumper le fichier complet
			dumpBytes (f'{PATH}{n:04d}.{EXTENSION}', data[h:t + len (TRAILER)])
			print (f'[{offset + h:08d}-{offset + t + len (TRAILER) - 1:08d}]: {TYPE} dumped as {PATH}{n:04d}.{EXTENSION} ({t - h + len (TRAILER)} bytes)')
			n = n + 1
			h = -1
			offset = offset + t + len (TRAILER)
			data = data[t + len (TRAILER):]
			s = 0	# Reprise des recherches (h'un HEADER) après le TRAILER
		else:
			s = len (data) - len (TRAILER) + 1	# Reprise des recherches (h'un TRAILER) en présumant qu'au mieux TRAILER amputé de son dernier octet pourrait figurer à la fin
	else:
		# Noter que s vaut nécessairement 0
		if len (HEADER) > len (data):
			# Pas assez de données pour trouver un HEADER entier : rajouter un bloc au données
			block = f.read (BLOCK_SIZE)
			if (len (data) + len (block)) < len (HEADER):	# Pas assez de données pour trouver un HEADER entier
				break
			data.extend (block)
		h = data.find (HEADER, s)
		if h != -1:
			data = data[h:]
			offset = offset + h
			h = 0
			s = len (HEADER)	# Reprise des recherches après le HEADER
		else:
			offset = offset + len (data) - len (HEADER) + 1
			data = data[len (data) - len (HEADER) + 1:]
			s = 0	# Reprise des recherches (d'un HEADER) en présumant qu'au mieux HEADER amputé de son dernier octet pourrait figurer à la fin
f.close ()
print ('Done!')
Hackflash #7 : Extraire les PNG et les JPEG d’un binaire (même partiels)