Solution
Je ne connaissais pas le sujet, je commence par ouvrir l’archive pcap
avec Wireshark. Bonne nouvelle, il
existe un dissecteur pour ce trafic. On est donc en présence du protocole DMX
, encapsulé dans du Art-Net
, reposant sur UDP
.
Un paquet DMX
ressemble à ceci :
On voit trois choses importantes en se baladant dans les paquets :
- il semble y avoir un numéro de séquence dans chacun des paquets. D’ailleurs, parfois, les séquences n’apparaissent
pas dans le bon ordre de la trace (logique, pour de l’
UDP
). - le champ
universe
varie entre 0 et 1. - si le dissecteur fait apparaître un tableau de pourcentages, il s’avère qu’il s’agit en fait d’entiers compris entre 0 et 255.
Reportons-nous sur le PDF fourni, qui confirme les deux univers
observés dans la trace. On voit d’ailleurs que seule la moitié du
deuxième univers semble utilisée, et c’est confirmé dans la trace :
Universe: 1
a toujours la dernière moitié de ses valeurs à 0. On
comprend aussi que chaque LED est décrite par un triplet RGB (Red -
Green - Blue) avec des valeurs de 0 à 255 comme les tableaux de la
trace. Enfin, le parcours dans les univers fait un zig-zag : la première
ligne est décrite de gauche à droite, la deuxième de droite à gauche,
et ainsi de suite…
On a toutes les infos pour écrire un parseur. J’ai choisi d’utiliser python, en décodant la trace réseau à l’aide de dpkt et en générant une suite de frames assemblées à la fin en un gif animé grâce à Pillow. Voyons le code (j’ai omis quelques éléments pour la lisibilité, le script complet est ci-dessous).
On commence par initialiser les numéros de séquence et d’univers, et on prépare un tableau qui va recevoir les frames du gif final:
frame = 0
universe0, universe1 = None, None
images = []
On ouvre la trace réseau et… on jette le premier paquet. Car si on regarde la trace, on se rend compte qu’elle débute par une moitié d’image (on n’a qu’un univers sur cette séquence). Flemme de faire propre, donc :
f = open('capture.pcap','rb')
for ts, pkt in dpkt.pcap.Reader(f):
# On jette le premier paquet
if frame == 0:
frame += 1
continue
On récupère les données qui nous intéressent dans chaque paquet : la séquence, l’univers et les données DMX. En réalité, la séquence ne sera pas utilisée car, coup de bol, elles sont dans l’ordre dans la capture.
payload = udp.data
sequence = payload[12]
universe = payload[14]
channels = payload[18:]
On transforme les valeurs DMX
en une liste de triplets (R, G, B)
:
colors = [ tuple(channels[i*3:i*3+3]) for i in range(len(channels)//3) ]
On découpe cette liste en ligne de 16 diodes :
lines = [ colors[i*16:i*16+16] for i in range(len(colors)//16) ]
Une ligne sur deux, on inverse le sens des triplets (vous vous souvenez ?) :
lines = [ line if i%2==0 else line[::-1] for i, line in enumerate(lines)]
Dans le cas de l’univers n°1, on n’en conserve que la moitié.
if universe == 0:
universe0 = lines
else:
universe1 = lines[:6]
Lorsqu’on a récupéré les deux univers de la séquence, on les concatène et l’on crée une image RGB correspondante.
if universe1 is not None and universe0 is not None:
frame_data = universe0 + universe1
frame_data = [ item for line in frame_data for item in line ]
img = Image.new('RGB', (16,16))
img.putdata(frame_data)
images.append(img)
frame+=1
universe0, universe1 = None, None
Arrivé à la fin de la trace, on sauvegarde toutes nos frames dans un gif.
images[0].save('result.gif', save_all=True, append_images=images[1:], duration=40, loop=0)
Le script complet est ci-dessous, et à l’issue de son exécution, on obtient :
Oui, faut zoomer ! :) Le flag qui s’affiche est FCSC{L1ghtD3sign3rCr-gg!}
.
Script de résolution
#!/usr/bin/env python3
import dpkt
from PIL import Image
frame = 0
universe0, universe1 = None, None
images = []
f = open('capture.pcap','rb')
for ts, pkt in dpkt.pcap.Reader(f):
eth=dpkt.ethernet.Ethernet(pkt)
if eth.type!=dpkt.ethernet.ETH_TYPE_IP:
continue
ip=eth.data
if ip.p!=dpkt.ip.IP_PROTO_UDP:
continue
udp=ip.data
# On jette le premier paquet
if frame == 0:
frame += 1
continue
payload = udp.data
sequence = payload[12]
universe = payload[14]
channels = payload[18:]
colors = [ tuple(channels[i*3:i*3+3]) for i in range(len(channels)//3) ]
lines = [ colors[i*16:i*16+16] for i in range(len(colors)//16) ]
lines = [ line if i%2==0 else line[::-1] for i, line in enumerate(lines)]
if universe == 0:
universe0 = lines
else:
universe1 = lines[:6]
print(f'{sequence=} {universe=}: {len(lines)} lines.')
if universe1 is not None and universe0 is not None:
frame_data = universe0 + universe1
print('\n'.join([str(line) for line in frame_data]))
frame_data = [ item for line in frame_data for item in line ]
img = Image.new('RGB', (16,16))
img.putdata(frame_data)
images.append(img)
frame+=1
universe0, universe1 = None, None
images[0].save('result.gif', save_all=True, append_images=images[1:], duration=40, loop=0)