U.S. Cyber Command Valentine’s Day 2021 Crypto Challenge (Puzzle 2)

Pour la Saint Valentin 2021, l'U.S. Cyber Command propose dans un tweet de résoudre une douzaine de puzzles de cryptographie. Cliquez ici pour les récupérer. Une excellente initiative qui suscitera peut-être des vocations...
Pourquoi ne pas s'y essayer ? Nous verrons bien où cela nous mène... Je publierai sur ce site les solutions auxquelles je serai parvenu, sous la forme des notes prises chemin faisant, rendant donc compte d'éventuels errements, ce qui sera plus vivant.
Aujourd'hui, la solution du puzzle 2.
Comme j'ai travaillé sur ce puzzle avant le suivant, mais que je les ai poursuivis par la suite parallèlement, et que j'ai finalement résolu le suivant avant ce dernier, les étapes que j'ai suivies sont à peu près les mêmes. C'est un assez bon exemple de ce qu'il ne faut pas faire, c'est-à-dire chercher midi à quatorze heures en se lançant sans trop réfléchir...
Il s'agit donc d'un MP3. J'installe Audacity pour pouvoir travailler dessus.
Une extraction des chaînes de caractères à l'aide de strings64 débouche sur un résultat majeur : "https://www.cybercom.mil/Employment-Opportunities/"...
Blague à part, je note des métadonnées que je consulte dans le détail dans VLC pour savoir comment elles s'interprètent :
  • l'auteur est "ehaggish" ;
  • un commentaire étrange : "00000C1D 00000BC8 00008F6C 00009709 00008163 00004563 00007EFF 00007EFF 000005D0 000005D0".
En fait, en consultant le fichier dans HxD, je note qu'il y a plus d'informations au début, avec une mention notamment à des champs "engiTunNORM" et "engiTunSMPB". Je chercher des infos sur ces termes, et j'en trouve ici. Il s'agirait d'informations pour indiquer à iTunes comment jouer le son, donc sans intérêt a priori pour résoudre le puzzle.
Je vais donc devoir pythonner les données. Comment isoler les données un MP3 ? Pour commencer, le convertir en WAV, puis lire la spécification. Elle me renseigne sur l'organisation des données, qui débutent par un en-tête dont je vérifie la validité manuellement :
52 49 46 46		"RIFF"
7c 1c 65 00		Fichier de 6 626 428 octets (en fait taille de ce qui suit)
57 41 56 45		"WAVE"
66 6d 74 20		"fmt"
10 00 00 00		16 octets de données dans le chunk "fmt"
01 00			Format PCM
02 00			Nombre de canaux : 2
44 ac 00 00		Fréquence d'échantillonnage : 44 100 Hz
10 b1 02 00		Fréquence * Bits par échantillon * Nombre de canaux / 8 = 176 400, donc 16 bits par échantillon
04 00			Bits par échantillon * Nombre de canaux / 8 = 4, donc 16 bits en stéréo
10 00			Bits par échantillon : 16 bits
4c 49 53 54		"LIST"
50 00 00 00
49 4e 46 4f		"INFO"
49 41 52 54		"IART"
09 00 00 00
65 68 61 67 67 69 73 68 00
00
49 43 52 44		"IRCD"
11 00 00 00
32 30 32 31 2d 30 32 2d 30 36 20 31 31 3a 32 37 00
00
49 4e 41 4d		"INAM"
02 00 00 00 3f 00
49 53 46 54		"ISFT"
0e 00 00 00
4c 61 76 66 35 38 2e 36 35 2e 31 30 30 00
64 61 74 61		"data"
00 1c 65 00		6 626 304 octets de données suivent
Et d'après d'autres explications, les données qui suivent sont celles du morceau, qui se présenteraient sous la forme de 4 octets par échantillon, l'échantillon étant stéréo, donc deux octets pour un canal (L) puis 2 octets pour l'autre (R).
Cela étant, il très improbable que le flag soit stocké dans les LSB, étant donné que les données sont compressées.
Il est possible d'essayer de voir si le nom de l'auteur "ehaggish" peut servir de mot de passe, comme dans un challenge que j'avais résolu à une autre époque. J'avais alors dû utiliser steghide. Mais steghide ne gère pas le MP3, donc ce n'est pas la bonne piste.
D'ailleurs à quoi correspond "ehaggish" ? Rien dans Google, qui renvoie sur "waggish", un vieux mot qui signifie "facétieux". Sans intérêt, donc.
J'installe DeepSound pour voir s'il reconnaît le fichier, mais ce n'est pas le cas.
A la recherche d'informations sur les techniques de stéganographie audio, je tombe sur d'intéressantes pages ici et . Ce qui m'apprend qu'il existe un support natif du format WAV dans Python. Me renseigner sur ce sujet m'aurait épargné mon décodage de l'en-tête...
Les données sont-elles en big ou little endian ? Réponse ici, c'est du little endian si "RIFF" et du big endian si "RIFX". Donc little endian dans le cas présent.
Je programme de quoi extraite les LSB :
import wave
wav = wave.open (f'{path}{filename}', mode='rb')
sound = wav.readframes (wav.getnframes ())
print (f'Channels: {wav.getnchannels ()}')
print (f'Sample frequency: {wav.getsampwidth ()}')
print (f'Frame rate: {wav.getframerate ()}')
print (f'Number of frames: {wav.getnframes ()}')
print (f'Compression mode: {wav.getcomptype ()}')
print (f'Data size: {len (sound)}')
formats = [
	('L 0-7', 0, 4, True),
	('R 0-7', 2, 4, True),
	('LR 0-7', 0, 2, True),
	('L 7-0', 0, 4, False),
	('R 7-0', 2, 4, False),
	('LR 7-0', 0, 2, False)
]
for f in formats:
	b = 0
	bit = 0
	data = bytearray ()
	for i in range (f[1], len (sound), f[2]):
		if f[3]:
			b |= (sound[i] & 1) << bit
		else:
			b |= (sound[i] & 1) << (7 - bit)
		if bit == 7:
			data.append (b)
			b = 0
			bit = 0
		else:
			bit += 1
	dumpBytes (path, f'{filename[:-4]} ({f[0]}).bin', data, True)
	print (f)
	strings = findStringsInBytes (data, minLength=4)
	for s in strings:
		print (f'\t{s["offset"]:08d}: {s["text"]}')
Cela ne donne rien, que ce soit pour L, R ou LR, et que les bits soient considérés comme stockés en bit ou little endian. Je passe les binaires produits à binwalk par acquis de conscience, mais rien.
Observant une différence entre les canaux L et R dans Audacity, j'extrais la différence entre les canaux, en L - R et R - L :
import wave
wav = wave.open (f'{path}{filename}', mode='rb')
sound = wav.readframes (wav.getnframes ())
print (f'Channels: {wav.getnchannels ()}')
print (f'Sample frequency: {wav.getsampwidth ()}')
print (f'Frame rate: {wav.getframerate ()}')
print (f'Number of frames: {wav.getnframes ()}')
print (f'Compression mode: {wav.getcomptype ()}')
print (f'Data size: {len (sound)}')
data = bytearray ()
for i in range (0, len (sound), 4):
	l = sound[i] + (sound[i + 1] << 8)
	r = sound[i + 2] + (sound[i + 3] << 8)
	# data.append ((l - r) & 0xff)
	data.append ((r - l) & 0xff)
file = open (f'{path}{filename[0:-4]}_diff.bin', 'wb')
file.write (data)
file.close ()
quit ()
Ce qui ne produit rien de plus.
Je me dis qu'il faut d'évidence viser un truc simple, comme repérer les notes. Bon je cherche un logiciel en ligne gratuit pour cela, et je trouve piano2notes. Après test, il semble faire le job, mais ne me fournit que 30 secondes en version gratuite. Peu importe, car les notes ont trop variées pour que je puisse espérer deviner comment les déchiffrer dans le contexte.
Finalement, après avoir résolu le puzzle suivant où il fallait simplement remplacer un son par un 1 ou un 0, je tente la même manoeuvre ici. Cette fois je n'écoute pas le morceau, je lis d'après l'aspect de la courbe sur laquelle j'ai zoomé dans Audacity. Au début, avant que les courbes ne change d'aspect, cela donne :
01110111011010000110000101110100
J'utilise le même programme en Python pour déchiffrer en espérant qu'il s'agisse bien d'une suite de codes ASCII :
bits = '01110111011010000110000101110100'
s = ''
for i in range (0, len (bits), 8):
	s += (chr (int (bits[i:i+8], 2)))
print (s)
De fait ! Cela "what". C'est donc clairement ainsi qu'il faut déchiffrer, et je continue. Toutefois, l'apparence du signal change à partir de là. Peut-être ce changement de tonalité signifie-t-il qu'il faut inverser le codage en bits ? Je vais voir :
11011111
Effectivement, ce n'est pas un code ASCII de caractère imprimable, qui doit être compris entre 0x20 et 0x7fe. J'inverse donc, ce qui produit " ", l'espace qui fait sens après "what".
Je passe sur la suite, qui est du même accabit. Simplement, le signal est un peu plus confus un moment, mais cela ne gêne pas outre-mesure le déchiffrer en s'appuyant sur la courbe. Au total, j'obtiens :
011101110110100001100001011101000010000001101001011100110010000001101100011011110111011001100101
Ce qui se déchiffre en "what is love".
C'est ainsi que je tombe sur cette image :
Au suivant !
U.S. Cyber Command Valentine’s Day 2021 Crypto Challenge (Puzzle 2)