Beeper 2 : forger un signal radio OOK pour déclencher une commande cachée

16 mars 2026 dans Security par Gerboise7 minutes

Forger et transmettre un signal OOK à 433 MHz avec un BladeRF pour déclencher une commande cachée sur un vrai device

Introduction

Ceci est le second stage du challenge Pirate Beeper que j’ai créé pour le Ph0wn 2026. Là où Beeper 1 était un exercice de reverse engineering purement statique, Beeper 2 est un challenge radio : l’objectif est de reconstruire et transmettre le bon signal OOK pour déclencher la commande cachée sur un vrai device.

Le Stage 2 s’appuie directement sur le Stage 1 : l’avoir résolu est un prérequis. La commande cachée pico_rum, découverte en reversant le firmware dans Beeper 1, est le point d’entrée de ce challenge. En l’envoyant au device par radio, on déclenche une fonctionnalité secrète et le flag s’affiche à l’écran.

Ce stage peut être abordé de deux façons : avec une puce radio dédiée pilotée par un microcontrôleur (par exemple un breakout CC1101 avec un Arduino), ou avec un SDR capable de transmettre. Cette solution utilise un BladeRF, un SDR full-duplex qui peut à la fois recevoir et émettre.

Matériel du challenge

Les participants reçoivent un seul fichier :

  • doc.pdf : le même document R&D “volé” que dans le Stage 1, décrivant la plateforme matérielle du Pirate Beeper (STM32H573I-DK + CC1101), les caractéristiques radio (OOK à 433,92 MHz, encodage PWM) et le format des messages (ASCII, MSB first, répétition 2x)

Le PDF contient tous les paramètres du protocole nécessaires pour forger le signal. La commande elle-même (pico_rum) a été retrouvée dans Beeper 1. Le script de solution complet est disponible : pico_cmd.py.

Reconstruction du protocole radio

Étape 1 : extraire les paramètres du protocole depuis le PDF

Le document PDF volé décrit les caractéristiques radio du beeper dans sa section Technical Specifications. C’est la source principale pour tous les paramètres du protocole :

  • Modulation : OOK (On-Off Keying) à 433,92 MHz
  • Encodage : PWM (Pulse Width Modulation) avec une période de bit fixe de 1212 µs
  • Module radio : CC1101 en mode série asynchrone (IOCFG0 = 0x0D)
  • Encodage des bits :
Type d’impulsionDuréeSignification
Impulsion courte376 µsBit 1
Impulsion longue780 µsBit 0
Impulsion sync2209 µsDélimiteur de message
  • Format des données : ASCII, MSB first, répété 2 fois (le firmware ne valide une commande que si les deux copies sont identiques)

La section Secret Feature du PDF est redacted. La commande (pico_rum) a été retrouvée dans le Stage 1 en reversant le firmware. Tous les autres paramètres nécessaires à la transmission sont entièrement documentés dans le PDF.

Étape 2 : comprendre l’encodage OOK et PWM

Avant de construire le signal, il est utile de bien comprendre ce que signifient concrètement les paramètres du PDF.

OOK (On-Off Keying) est la forme la plus simple de modulation d’amplitude : la porteuse radio est soit complètement ON, soit complètement OFF. Pas de décalage de fréquence ni de phase, juste la présence ou l’absence du signal.

PWM (Pulse Width Modulation) encode chaque bit sous la forme d’une impulsion (porteuse ON) suivie d’un silence (porteuse OFF), la durée totale impulsion+silence étant toujours fixe. C’est la largeur de l’impulsion seule qui détermine la valeur du bit :

|<---- 1212 µs ---->|
|  pulse  |   gap   |
|▓▓▓▓▓▓▓▓▓|         |

Une impulsion courte encode un bit 1, une impulsion longue encode un bit 0. Le PDF ne donne que les durées d’impulsion ; le silence se déduit par gap = 1212 - durée_impulsion :

BitImpulsion (ON)Silence (OFF)Total
1376 µs1212 - 376 = 836 µs1212 µs
0780 µs1212 - 780 = 432 µs1212 µs

L’impulsion de synchronisation (2209 µs ON + 414 µs OFF) est plus large que n’importe quel bit de données et sert exclusivement de délimiteur de trame. Le récepteur l’utilise pour détecter le début d’un nouveau message, pas comme donnée.

La trame complète est donc : [SYNC] [byte0] [byte1] ... [byteN], répétée 2 fois.

Génération et transmission du signal

Étape 3 : convertir les timings en échantillons IQ pour le BladeRF

Le BladeRF ne travaille pas directement avec des durées d’impulsion. Il transmet un flux continu d’échantillons IQ, où chaque échantillon est une paire (I, Q) représentant le signal en bande de base à un instant donné :

  • I (In-phase) : partie réelle, contrôle l’amplitude de la porteuse
  • Q (Quadrature) : partie imaginaire, contrôle le déphasage

Pour de l’OOK, seule l’amplitude varie et Q reste à 0. Les deux valeurs possibles sont :

ÉtatIQ
RF ON20000
RF OFF00

La valeur 2000 est choisie proche du maximum du DAC (2047) avec une petite marge. Le BladeRF joue ces échantillons à un sample rate fixe. On choisit 2 Msps (2 000 000 échantillons/s), ce qui donne une résolution de 0,5 µs par échantillon, suffisamment fine pour représenter fidèlement notre impulsion la plus courte (376 µs = 752 échantillons).

Chaque valeur de timing du PDF se convertit directement en nombre d’échantillons en multipliant par 2 :

ÉlémentÉchantillons ONÉchantillons OFF
Bit 1376 × 2 = 752836 × 2 = 1672
Bit 0780 × 2 = 1560432 × 2 = 864
Sync2209 × 2 = 4418414 × 2 = 828

Étape 4 : construire le message complet pour pico_rum

Avec les compteurs d’échantillons établis, la forme d’onde complète est assemblée comme une liste de paires (I, Q) :

  1. Préambule de silence (10 000 × 0,0, ~5 ms) pour laisser le SDR se stabiliser avant la première impulsion sync
  2. Pour chacune des 4 répétitions :
    • Impulsion sync : 4418 × 2000,0 puis 828 × 0,0
    • Pour chaque octet de pico_rum en ASCII, pour chaque bit MSB first :
      • Bit 1 : 752 × 2000,0 puis 1672 × 0,0
      • Bit 0 : 1560 × 2000,0 puis 864 × 0,0
    • Silence inter-message : 20 000 × 0,0 (~10 ms), pour que le récepteur puisse finaliser le message en cours avant la prochaine impulsion sync

Le firmware exige 2 copies consécutives identiques pour valider une commande. On envoie 4 répétitions parce que les une ou deux premières peuvent être partiellement perdues pendant que le récepteur se synchronise sur le signal. Les copies supplémentaires garantissent qu’au moins 2 messages propres passent.

Étape 5 : générer le fichier CSV

Le CLI du BladeRF n’accepte pas de flux d’échantillons en direct, il lit les échantillons IQ depuis un fichier pré-calculé. Le format est un CSV simple où chaque ligne est un échantillon (I, Q) : soit 2000, 0 (RF ON), soit 0, 0 (RF OFF). Le signal complet de l’étape 4 est simplement écrit ligne par ligne dans ce fichier.

Le script pico_cmd.py et le CSV pré-généré pico_cmd.csv constituent la solution présentée ici. D’autres approches sont valides (par exemple piloter un CC1101 directement depuis un microcontrôleur), mais c’est celle qu’on a utilisée. pico_cmd.py gère tout le pipeline :

def generate_message(data, reps=4):
    """Construit la liste complète d'échantillons : préambule + reps × (sync + bytes + silence)."""

def write_csv(filename, samples):
    """Écrit chaque paire (I, Q) sur sa propre ligne."""

Le fichier pico_cmd.csv résultant a la structure suivante :

# Lignes 1–10 000      préambule de silence (5 ms × 2 Msps = 10 000 échantillons)
0, 0
...
# Lignes 10 001–14 418  SYNC ON  (2209 µs × 2 = 4418 échantillons)
2000, 0
...
# Lignes 14 419–15 246  SYNC OFF (414 µs × 2 = 828 échantillons)
0, 0
...
# Lignes 15 247–…       bits de données pour 'p','i','c','o','_','r','u','m' (MSB first)
#   'p' = 0x70 = 0b01110000
#   bit7=0 : 1560 × "2000,0"  +  864 × "0,0"
2000, 0
...
0, 0
...
#   bit6=1 : 752 × "2000,0"  +  1672 × "0,0"
2000, 0
...

Chacune des 4 répétitions se termine par un bloc de silence de 20 000 échantillons (10 ms).

Étape 6 : transmettre avec bladeRF-cli

Le CSV étant prêt, on appelle le CLI du BladeRF pour transmettre avec les paramètres du PDF : 433,92 MHz, 2 Msps :

def transmit(filename, repeats=10):
    """Appelle bladeRF-cli avec la bonne fréquence / sample rate / gain."""

Pour lancer tout le pipeline en une commande :

python pico_cmd.py rum

Ou pour transmettre manuellement un CSV déjà généré :

bladeRF-cli \
  -e "set frequency tx1 433920000" \
  -e "set samplerate tx1 2000000" \
  -e "set bandwidth tx1 1500000" \
  -e "set gain tx1 60" \
  -e "tx config file=pico_cmd.csv format=csv channel=1 repeat=2" \
  -e "tx start" \
  -e "tx wait"

Flag

Une fois que le beeper reçoit pico_rum, il affiche le flag à l’écran :

ph0wn{rum_is_pirate_drink}

Conclusion

Ce challenge était aussi noté easy/medium au Ph0wn. Les paramètres du protocole sont tous documentés dans le PDF, il n’y a rien à deviner. La difficulté réside dans la traduction de ces timings en un signal concret : comprendre comment l’encodage OOK et PWM fonctionnent ensemble, convertir les durées d’impulsion en échantillons IQ à un sample rate donné, et assembler la trame complète avec sync, bits de données et répétitions. C’est un exercice pratique pour passer d’une spécification de protocole à une transmission radio fonctionnelle.