Solution de lrstx pour Illuminated

misc protocole industriel

1 mai 2024

Table des matières

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)