~/

Cyber on Board 2025 writeups [IoT/Hardware] 🇫🇷

Introduction

Du 14 au 15 mai 2025 avec la fine équipe nous avons eu l’occasion de représenter l’ecole 2600 en tant que Phreaks 2600 au premier CTF du cyber on board, une convention dédiée à la sécuritée des systèmes embarqués.

Le challenge avait été réalisé par neverhack et comportait deux circuits imprimés reliés entre eux. Il représentait le système d’un drone et exposait des valeurs comme l’altitude sur un petit écran. Il y a aussi d’autres composants comme un qui servait à envoyer des données sur les bus CAN, et une EEPROM qui communiquait en I²C.

photo de la board

Parmi le matériel fournit on avait :

Après un début difficle on a fini par se classer 5/13.

Challenges

Liste des challenges

Serial Killer exposed

serial killer exposed

Pour ce challenge, le titre nous indique la voie à suivre avec le “Serial”. Une communication série est la voie de communication la plus présente en hardware. C’est lorsque deux appareils communiquent directement en étant connecté avec un cable par exemple. USB est un protocole de communication série.

mini usb

En se branchant en USB on peut directement se connecter à un des composants (teensy). Maintenant pour surveiller ce qu’il se passe, on peut utiliser un outil qui va directement lire ce que la teensy nous envoie avec des outils comme minicom ou screen.

En connectant le cable, en redémarrant le circuit électronique et en lançant rapidement screen /dev/ttyACM0, on voit apparaitre des messages d’initialisation du circuit. Puis ensuite en appuyant sur n’importe qu’elle touche on voit Unknown command s’afficher. Donc en faisant un script pour pouvoir entrer plus de caractères, on arrive à faire afficher un menu avec la commande help. Voici le script:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import serial
import time
import random
import string

# Open the serial connection
ser = serial.Serial('/dev/ttyACM0', baudrate=115200, timeout=1)
time.sleep(2)  # Let device initialize

ser.write(b'help')
time.sleep(0.2)
print(ser.read(ser.in_waiting).decode(errors='replace'))

Puis les commandes dispos étaient :

1
Available commands: help, reset_screen, gyro_reset, gyro_stop_update, gyro_resume_update, craft_can_message, flag, secret

En envoyant flag on récupère : COB{FEnw3mfUFlopd_0c=MuWi_2T70C4wUeiE2o3-tHE}.

Dumpsters

dumpsters

Avec le nom “dumpsters” ca n’a fait qu’un tour, on a compris qu’il fallait dump un firmware. Parmi tous les composants, un seul correspondait à une ROM qui pouvait contenir un firmware :

eeprom

La référence dessus est 24LC512 I/P RVD 2019. Après une courte recherche on tombe sur une datahseet du fabricant MICROCHIP : https://www.alldatasheet.fr/datasheet-pdf/download/348628/MICROCHIP/24LC512.html.

Il s’agirait donc d’une EEPROM (Electrically Erasable Programmable Read-Only Memory), pour faire court, une puce qui sert de stockage persistant. Le protocole utilisé par la puce est I²C.

A partir de ce moment on a tenté de ce brancher à la puce avec des cables pour extraire le firmware avec flashrom et le programmeur flash CH341A mais ca passait pas. Après un long moment de galère, on nous a indiqué que les puces n’étaient pas soudées donc on pouvait les retirer et on a tenté avec un autre outil IMSProg et c’est passé.

imsprog

Toutes les infos étaient sur la datasheet, et en cherchant “COB{” dans la donnée extraite, on trouve le flag COB{U:gtxv4HPEruC2rg1Fxl7TfSiQmJmI_=FXKw:QAH}.

CAN u replay it

can u replay it

L’objectif du challenge est de faire en sorte que les valeurs affichées sur l’écran soient toutes à 0 pendant 10 secondes.

Dans son comportement normal, l’écran diffuse des informations mises à jour tout les demi secondes comme par exemple l’altitude du drone qui varie quand on souleve la board :

ecran drone

Ces valeurs sont transmises, par différents éléments, il y a la carte teensy ainsi qu’un module MCP2515 couplé au module MCP2551 qui permettent d’en envoyer. Mais la carte teensy est directement reliée au MCP2515 donc on suppose qu’elle est là car on peut la programmer (voir le challenge serial killer exposed).

composants

Maintenant l’objectif était de savoir quelles valeurs rejouer pour pouvoir tout mettre à zéro.

Pour commencer on a du observer les paquets envoyés durant le comportement normal du drone..

En connectant un crochet de test sur CANH, un sur CANL et sur GND, on a pu observer des données transiter sur l’analyseur logique (saleae logic 2).

Mais aucune donnée n’était compréhensible. (J’ai pas de captures d’écran malheureusement.)

Ce qu’il se passe était que notre outil était réglé sur le mauvais baudrate (ou bitrate). C’est-à-dire la fréquence de bits reçu par secondes. Le circuit est programmé à en envoyer sur une certaine fréquence mais si nous de notre coté on en utilise une différente, les données seront illisibles car on reçoit les données à une fréquence différente que celle effective.

Pour trouver le bon baudrate il y a deux choix :

  • la méthode naïve : on tatonne jusqu’à trouver le bon baudrate
  • la méthode datasheet : on utilise le baudrate par défaut définie dans la datasheet
  • la méthode smart : on prend l’impulsion avec la période T la plus faible dans la capture, et de faire le calcul : $\frac{1}{T}$. Dans notre cas, sur l’analyseur logique on a T = 8 microsecodes = 0.000008 secondes. Donc $\frac{1}{T} = 125000$ Hz. Et ainsi on a notre baudrate.

Maintenant qu’on a notre baudrate les données sont lisibles et on voit que le meme type de paquets CAN sont envoyés en boucle.

Il fallait garder les valeurs qui ne bougent pas dans les paquets et mettre des zéros pour celles qui varient. Sauf que, le drone continue à avoir ses valeurs qui se mettent à jour donc ça nous empechait de tout mettre à zéro. Il y avait une commande intéressante durant le challenge serial killer exposed qui était gyro_stop_update. En faisant ça, il n’y a plus rien qui s’envoie sauf nous paquets à nous, on peut ainsi envoyer plein des paquets avec zéro comme valeur en boucle :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import serial
import time
import random
import string

ser = serial.Serial('/dev/ttyACM0', baudrate=115200, timeout=1)
time.sleep(2)

FUNC = ['help', 'reset_screen', 'gyro_reset', 'gyro_stop_update', 'gyro_resume_update', 'craft_can_message', 'flag', 'secret']

ser.write(b'gyro_stop_update')
time.sleep(1)

for i in range(0, 100):
    ser.write(b'craft_can_message')
    time.sleep(0.2)
    print(ser.read(ser.in_waiting).decode(errors='replace'))
    ser.write(b'\x51')
    time.sleep(0.2)
    print(ser.read(ser.in_waiting).decode(errors='replace'))
    ser.write(<data-to-send>)
    time.sleep(0.2)
    print(ser.read(ser.in_waiting).decode(errors='replace'))

    print("sleeping 0.5 sec...")
    time.sleep(0.5)

Et on récupère le flag :

flag can u replay

COB{uAGm9f2:mAEEzPIX7L3T}

CAN u sniff it

can u sniff it

Pour ce challenge là on devait intéragir avec une commande secrète. Et la seule ocmmande secrete qu’on avait était la commande secret du challenge serial killer exposed.

En activant la commande on nous demande un mot de passe et ensuite ce-dernier est vérifié.

En regardant sur l’analyseur logique ce qu’il se passe au niveau du CAN bus, on voit d’avantage de choses:

logic 2 sniff

On reçoit toujours trois paquets, un remplit de 0xff, et deux autres avec des caractères ascii imprimables.

mot de passe

En tâtonnant on comprend qu’il faut concaténer les 5 derniers caractères : rvNdRm86YW. Et en renvoyant ça on obtient le flag.

La fine équipe