Objets connectés
L’objectif de ce challenge est de retrouver un flag dans une capture radio.
La description de ce challenge est brève mais instructive :
Dans votre nouveau laboratoire, vous trouvez un micro-onde appelé Louis et une cafetière nommée Rachèle qui vous semblent être tout à fait sympathiques. Selon les employés du labo, ils sont capables d'émettre quand ils ont fini. En les regardant de plus près, vous remarquez que des capteurs de température leur sont accolés. Les étiquettes de ces capteurs contiennent des termes que vous ne comprenez pas dont une mention liée à une fréquence, `LoRa : 433.242 MHz`. Vous sortez vos outils de capture de signal que vous avez sous la main. Vous extrayez un fichier en prenant une fréquence de capture de `1 MHz`.
Les capteurs n'ont pas l'air d'être neufs et leur aspect vous fait penser que certaines données ont été corrompues mais le vieux briscard du labo vous assure que la correction se fait toute seule et que le message est tout à fait compréhensible.
Le nom des objets connectés (Louis et Rachel) ainsi que l’étiquette où il y a écrit LoRa : 433.32 MHz
permettent de commencer à chercher des informations sur le LoRa.
Dans le contexte des objets connectés, LoRa est une modulation sous-jacente à un certain nombre de protocoles, dont le plus connu d’entre eux est LoRaWAN.
Le LoRa est une modulation par étalement de spectre à base de rampes de fréquence (chirp spread spectrum) et une bonne introduction à son fonctionnement peut être trouvée dans la présentation de Pieter Robyns au FOSDEM en 2018.
L’objectif initial de la modulation LoRa peut se retrouver dans son nom : Long Range. C’est une modulation dont l’objectif est de transporter des données sur de longues distances, pour un coût en énergie minimal. Un signal LoRa peut donc se démoduler et décoder même s’il y a beaucoup de bruit, par conception.
Pour voir un peu à quoi on a à faire, il est possible d’utiliser le logiciel Inspectrum qui affiche de jolis diagramme en chute d’eau.
Sur la chute d’eau, on voit effectivement deux canaux de fréquence utilisés, contenant un total de trois trames. On constate aussi qu’il y a beaucoup de bruit, ce qui donne peu de contraste sur la chute d’eau.
Pour décoder les trames, il faut retrouver les paramètres de transmission. Un canal LoRa est défini par :
- Une fréquence centrale;
- Une largeur de bande;
- Un facteur d’étalement.
Pour la fréquence du signal du haut, une inspection manuelle avec Inspectrum montre qu’il est à 225kHz par rapport à la fréquence centrale de la capture. Pour la fréquence du signal du bas, on constate qu’il est à -300kHz par rapport à la fréquence centrale de la capture.
Les deux signaux ont la même largeur de bande, c’est à dire 250kHz.
Il est possible de décaler ces deux signaux en fréquences pour les recaler en 0, puis de les filtrer avec un filtre passe-bas pour atténuer le bruit autour.
Voilà le code Python qui fait le décalage, le filtre et la démodulation en fréquence de chacun des deux signaux :
import numpy as np
from scipy.signal import butter, lfilter, freqz
from matplotlib import pyplot as plt
def shift_freq(data, samp_rate, shift):
duration = len(data) / samp_rate
samples = np.linspace(0, duration, int(samp_rate*duration), endpoint=False)
signal_i = np.sin(2 * np.pi * shift * samples)
signal_q = np.cos(2 * np.pi * shift * samples)
signal = signal_i + signal_q*1j
return signal * data
def butter_lowpass(cutoff, fs, order=5):
return butter(order, cutoff, fs=fs, btype='low', analog=False)
def butter_lowpass_filter(data, cutoff, fs, order=2):
b, a = butter_lowpass(cutoff, fs, order=order)
y = lfilter(b, a, data)
return y
def freq_demod(samples):
return np.diff(np.unwrap(np.angle(samples)))
FILENAME = "obj_conn.iq"
SAMP_RATE = 1000000
SHIFT1 = 300000
SHIFT2 = -225000
CUTOFF = 125000
signal = np.fromfile(FILENAME, dtype=np.complex64)
signal1 = shift_freq(signal, SAMP_RATE, SHIFT1)
signal2 = shift_freq(signal, SAMP_RATE, SHIFT2)
signal1 = butter_lowpass_filter(signal1, CUTOFF, SAMP_RATE)
signal2 = butter_lowpass_filter(signal2, CUTOFF, SAMP_RATE)
freq1 = freq_demod(signal1)
freq2 = freq_demod(signal2)
plt.plot(freq1)
plt.show()
plt.plot(freq2)
plt.show()
Si l’on regarde le début des deux trames, on constate deux choses qui nous embêtent un peu :
- Il y a beaucoup de bruit, ce qui rend la démodulation plus complexe;
- Sur le premier signal, les données sont modulées avec des rampes de fréquence ascendantes, alors que sur le second elles sont modulées avec des rampes de fréquence descendantes.
Pour le second point, c’est en réalité l’une des particularité de la modulation LoRa : comme on module une donnée avec des rampes de fréquences, celles-ci peuvent être dans un sens ou dans l’autre.
Pour l’anecdote, le protocole LoRaWAN utilise cette disparité : les objets connectés à un réseau LoRaWAN utilisent des rampes montantes, alors que les messages en provenance du réseau et transmis par les passerelles utilisent des rampes descendantes.
On pourrait tenter une démodulation manuelle avec Python, néanmoins il y a déjà des projets open-source basés sur le projet GNURadio qui permettent de démoduler du LoRa.
Pour ce challenge, on peut utiliser le projet gr-lora_sdr de l’EPFL, qui à ma connaissance a les meilleure performances. Le projet de Pieter Robyns, bien que son approche soit facile à comprendre, ne permet pas de démoduler des signaux fortement bruités.
D’un côté, le projet gr-lora_sdr implémente la correction d’erreur à partir de codes de Hamming, ce qui devrait nous permettre de résoudre le problème du bruit. De l’autre, ce projet ne permet de démoduler que des trames modulées avec des rampes de fréquence ascendantes, donc il faut trouver le moyen d’en inverser le sens.
Pour cela, on peut simplement prendre le conjugué complexe du signal et toutes les fréquences se retrouvent inversées. Donc les rampes descendantes deviennent ascendantes et inversement.
Il ne reste donc qu’à trouver le facteur d’étalement (spreading factor) utilisé par chaque signal. Sachant que LoRa définit des facteurs d’étalement qui vont de 7 à 12, on peut donc tous les tester à la main. Au final, il s’avère que le signal du haut utilise un facteur d’étalement de 7 et celui du bas un facteur d’étalement de 9.
Au final, le graphe GNURadio qui permet de résoudre le challenge est le suivant :
On récupère le flag, qui était codé en deux parties FCSC{0083fa85206b09970d550b9e8ba8a705be62027e6e769d6ed5e87b8e93dd5759}
.