J'ai participé la semaine dernière à la Toulouse Hacking Convention qui se tenait à Toulouse du 5 au 6 mai, car j'intervenais notamment en tant que confériencer aux côtés d'Axelle Apvrille afin de présenter un sujet commun et ô combien d'actualité: l'impact de l'intelligence artificielle sur les compétitions de type Capture the Flag (CTF) dans le domain de la cybersécurité. Axelle a publié sur son blog un résumé très complet des problématiques que nous avons exposé et des solutions envisagées dans un futur proche, qui vaut vraiment le coup d'être lu. Notre talk s'est bien passé et a permis de longues discussions avec des participants à la conférence sur ce délicat sujet, certains détaillant leur approche des CTFs et d'autres pointant du doigt certaines positions mentionnées dans notre présentation qui les dérangeaient, ce qui ne manquera pas de faire mûrir notre réflexion.
Mais cette conférence a pris un tournant très intéressant durant la matinée de sa seconde journée, un badge électronique nous ayant été remis par le staff lors de notre arrivée, en complément du badge plastifié que nous avions récupéré la veille. Ce badge électronique héberge un challenge propre à la THCon, mais peut aussi être flashé avec d'autres challenges disponibles en ligne. Le badge a été conçu par DVID, comme l'atteste le logo présent sur la sérigraphie du circuit imprimé, et il semblerait qu'il soit interactif. Durant le talk d'ouverture, nos badges se sont mis à clignoter de différentes couleurs tandis que les organisateurs indiquaient qu'ils voyaient que les badges fonctionnaient bien, au vu des nombreuses LED qui s'illuminaient dans la salle. Je n'ai donc pas résisté à la tentation de jeter un œil au micrologiciel, avec dans l'idée de trouver potentiellement un flag voire d'en prendre le contrôle.
Le badge est conçu autour d'un module ESP32-C6-WROOM-1 avec 8 Mio de mémoire Flash. Il possède un écran OLED reposant sur un contrôleur SSD1306, une LED RGB adressable WS2812 reliée à la pin 11 (d'après la sérigraphie), une EEPROM ATC24C04 et un connecteur Shitty Add-On (SAO). Il est alimenté par une batterie mono-cellulaire de 3.7 volts, gérée par un contrôleur de charge TP4056. L'interface série accessible via le connecteur USB-C est quant à elle gérée par un contrôleur CH340C.
La connexion au badge via le port USB-C donne le résultat attendu: le port série de l'ESP32 est bien interfacé (par défaut à 115200 bauds), et un appui sur le bouton RESET produit la réception de la trace suivante:
ESP-ROM:esp32c6-20220919
Build:Sep 19 2022
rst:0x1 (POWERON),boot:0xc (SPI_FAST_FLASH_BOOT)
SPIWP:0xee
mode:DIO, clock div:2
load:0x40875730,len:0x1648
load:0x4086b910,len:0xd60
load:0x4086e610,len:0x31c8
entry 0x4086b91a
I (23) boot: ESP-IDF v6.1-dev-4103-g96194f19a6 2nd stage bootloader
I (24) boot: compile time May 3 2026 20:12:03
I (24) boot: chip revision: v0.2
I (25) boot: efuse block revision: v0.3
I (29) boot.esp32c6: SPI Speed : 80MHz
I (33) boot.esp32c6: SPI Mode : DIO
I (36) boot.esp32c6: SPI Flash Size : 2MB
I (40) boot: Enabling RNG early entropy source...
I (45) boot: Partition Table:
I (47) boot: ## Label Usage Type ST Offset Length
I (54) boot: 0 nvs WiFi data 01 02 00009000 00006000
I (60) boot: 1 phy_init RF data 01 01 0000f000 00001000
I (67) boot: 2 factory factory app 00 00 00010000 00100000
I (73) boot: End of partition table
I (76) esp_image: segment 0: paddr=00010020 vaddr=42078020 size=13818h ( 79896) map
I (99) esp_image: segment 1: paddr=00023840 vaddr=40800000 size=047d8h ( 18392) load
I (103) esp_image: segment 2: paddr=00028020 vaddr=42000020 size=74914h (477460) map
I (194) esp_image: segment 3: paddr=0009c93c vaddr=408047d8 size=0ebe4h ( 60388) load
I (207) esp_image: segment 4: paddr=000ab528 vaddr=408133c0 size=028bch ( 10428) load
I (214) boot: Loaded app from partition at offset 0x10000
I (215) boot: Disabling RNG early entropy source...
I (225) cpu_start: Unicore app
I (233) cpu_start: GPIO 17 and 16 are used as console UART I/O pins
I (233) cpu_start: Pro cpu start user code
I (234) cpu_start: cpu freq: 160000000 Hz
I (235) app_init: Application information:
I (239) app_init: Project name: esp32c6_oled
I (244) app_init: App version: 05e99fa-dirty
I (248) app_init: Compile time: May 3 2026 20:11:43
I (253) app_init: ELF file SHA256: 61148e412...
I (257) app_init: ESP-IDF: v6.1-dev-4103-g96194f19a6
I (263) efuse_init: Min chip rev: v0.0
I (267) efuse_init: Max chip rev: v0.99
I (271) efuse_init: Chip rev: v0.2
I (274) heap_init: Initializing. RAM available for dynamic allocation:
I (281) heap_init: At 408175F0 len 00065020 (404 KiB): RAM
I (286) heap_init: At 4087C610 len 00002F54 (11 KiB): RAM
I (291) heap_init: At 50000000 len 00003FE8 (15 KiB): RTCRAM
I (297) spi_flash: detected chip: generic
I (300) spi_flash: flash io: dio
W (303) spi_flash: Detected size(8192k) larger than the size in the binary image header(2048k). Using the size in the binary image header.
I (316) sleep_gpio: Configure to isolate all GPIO pins in sleep state
I (322) sleep_gpio: Enable automatic switching of GPIO sleep configuration
I (328) coexist: coex firmware version: b00e8cb
I (332) coexist: coexist rom version 5b8dcfa
I (337) main_task: Started on CPU0
I (337) main_task: Calling app_main()
I (2377) BLE_INIT: Using main XTAL as clock source, chip ver: 2
I (2377) BLE_INIT: ble controller commit:[c9fbba6]
I (2377) BLE_INIT: Bluetooth MAC: ac:eb:e6:0e:5b:fe
I (2377) phy_init: phy_version 343,b513b46,Nov 14 2025,16:34:12
I (2447) phy: libbtbb version: c493933, Nov 14 2025, 16:34:25
I (2447) NimBLE: GAP procedure initiated: stop advertising.
I (2457) NimBLE: GAP procedure initiated: discovery;
I (2457) NimBLE: own_addr_type=0 filter_policy=0 passive=1 limited=0 filter_duplicates=0
I (2457) NimBLE: duration=forever
I (2457) NimBLE:
Cette trace est très instructive, car on peut noter que le micrologiciel a été compilé le 3 mai 2026 à 20h11 en utilisant la version 6.1 du SDK ESP-IDF d'Espressif. De plus, il utilise une pile protocolaire Bluetooth Low Energy NimBLE pour découvrir les équipements BLE présents dans les environs.
Je fais alors l'hypothèse que le badge communique avec un autre équipement qui s'annonce en BLE et qui déclenche potentiellement différents comportements. La seule manière d'en avoir le cœur net: extraire le micrologiciel et l'analyser.
L'extraction du micrologiciel d'un module ESP32, si ce dernier n'est pas protégé, est relativement triviale. Le programme esptool permet d'extraire le contenu de la mémoire Flash intégrée au module, ce dernier étant présent dans le SDK mais aussi sur la grande majorité des distributions Linux en tant que paquet officiel. Pour ma part, je l'ai installé au travers du paquet Debian correspondant. La mémoire Flash du module faisant théoriquement 2 Mio (en réalité 8 Mio, mais seuls 2 Mio sont utilisés), l'extraction se passe sans encombre:
$ esptool.py read_flash 0 0x200000 thcon-badge-flash.bin
esptool.py v4.10.0
Found 5 serial ports
Serial port /dev/ttyUSB0
Connecting....
Detecting chip type... ESP32-C6
Chip is ESP32-C6 (QFN40) (revision v0.2)
Features: WiFi 6, BT 5, IEEE802.15.4
Crystal is 40MHz
MAC: ac:eb:e6:ff:fe:0e:5b:fc
BASE MAC: ac:eb:e6:0e:5b:fc
MAC_EXT: ff:fe
Uploading stub...
Running stub...
Stub running...
Configuring flash size...
2097152 (100 %)
2097152 (100 %)
Read 2097152 bytes at 0x00000000 in 186.8 seconds (89.8 kbit/s)...
Hard resetting via RTS pin...
Une rapide recherche dans les chaînes de caractères du dump montre que ce dernier semble être correct (textes affichés sur l'écran du badge):
$ strings thcon-badge-flash.bin| egrep "(Challenge|Baptiste)"
Challenge creator :
Baptiste Rebillard
D'après la trace de démarrage, l'application exécutée par l'ESP32 est placée à l'offset 0x10000 dans la mémoire Flash:
I (45) boot: Partition Table:
I (47) boot: ## Label Usage Type ST Offset Length
I (54) boot: 0 nvs WiFi data 01 02 00009000 00006000
I (60) boot: 1 phy_init RF data 01 01 0000f000 00001000
I (67) boot: 2 factory factory app 00 00 00010000 00100000
I (73) boot: End of partition table
J'ai donc extrait le contenu de la Flash à cette adresse, qui correspond à l'image ESP32 du programme principal, et ai utilisé esptool pour lire cette image et afficher le détail des différents segments mémoire:
$ dd if=thcon-badge-flash.bin of=thcon-badge-app.bin bs=65536 skip=1
$ esptool image_info thcon-badge-app.bin
esptool.py v4.10.0
File size: 2031616 (bytes)
Detected image type: ESP32-C6
Image version: 1
Entry point: 408005a8
5 segments
Segment 1: len 0x13818 load 0x42078020 file_offs 0x00000018 [DROM,IROM]
Segment 2: len 0x047d8 load 0x40800000 file_offs 0x00013838 [DRAM,BYTE_ACCESSIBLE,IRAM]
Segment 3: len 0x74914 load 0x42000020 file_offs 0x00018018 [DROM,IROM]
Segment 4: len 0x0ebe4 load 0x408047d8 file_offs 0x0008c934 [DRAM,BYTE_ACCESSIBLE,IRAM]
Segment 5: len 0x028bc load 0x408133c0 file_offs 0x0009b520 [DRAM,BYTE_ACCESSIBLE,IRAM]
Checksum: ee (valid)
Validation Hash: 32cda12be51dcd11bca6f738f4f0a9a92b797be8c1579ba9873b05eee5d34b5b (valid)
N'ayant pas trouvé de loader adapté pour Ghidra, j'ai ensuite chargé le contenu des différents segments dans un projets Ghidra configuré pour un CPU RISC-V 32-bit LE, qui correspond à l'architecture utilisé par l'ESP32-C6. Le segment 3 contient le code principal de l'application, mais il semble que ce dernier doit être ajusté avec un offset de 8 octets pour que les références croisées soient correctes. Aucune idée si cela est dû au format d'image, toujours est-il que cet ajustement m'a permis assez rapidement d'avoir du code cohérent, avec des références aux variables qui pointent sur les bonnes adresses mémoire.
Le code principal de l'application devient dès lors facilement identifiable à l'adresse 0x4200f684:
void app_main(void)
{
/* Code d'initialisation (supprimé par souci de clarté) */
/* Affichage des infos de boot ("THCon 2026 x DVID", nom de l'auteur) */
oled_print_text(0x1c,1,s_THCON_x_DVID_ram_4207be24);
oled_print_text(10,5,s_Challenge_creator_:_ram_4207be34);
oled_print_text(10,6,s_Baptiste_Rebillard_ram_4207be48);
FUN_ov1__4200f528();
/* Attente de 200ms. */
esp_delay_ms(200);
/* Initialisation du contrôleur de LED RGB WS2812. */
chall_init_ws2812();
/* Création de la tâche FreeRTOS en charge du challenge BLE. */
chall_start_ble_task();
/* Boucle principale affichant en boucle les logos. */
do {
oled_show_thcon_logo();
esp_delay_ms(500);
oled_show_astar_logo();
esp_delay_ms(500);
} while( true );
}
Le programme principal démarre une tâche FreeRTOS qui est en charge de gérer des événements liés au protocole BLE, puis affiche de manière alternée les logos de la THCon et de son sponsor Astar.
La partie intéressante se trouve donc dans cette tâche FreeRTOS:
void chall_start_ble_task(void)
{
_DAT_ram_408172c4 = xTimerCreate(
s_led_tmr_ram_4207bdb8,
200,
0,
0,
chall_rgb_led_update
);
_DAT_ram_408172c0 = xTimerCreate(
s_bcast_tmr_ram_4207bdc0,
50,
0,
&broadcast_timer_id,
FUN_ov1__4200f26a
);
esp_ble_controller_init();
ptr_nimble_gap_event_handler = nimble_on_initialized;
FUN_ov1__4201d6ba(FUN_ov1__4200f096);
return;
}
Cette fonction crée deux timers: le premier va être en charge d'appeler une fonction de rappel pour gérer la LED RGB tandis que le second va gérer des événements liés au BLE qui seront précisés par la suite. Le contrôleur BLE de NimBLE est initialisé, et ce dernier est initialisé . La fonction nimble_on_initialized() est une fonction de rappel qui sera exécutée lorsque le contrôleur sera initialisé. Celle-ci lance une procédure GAP de découverte de périphériques BLE, basée sur la réception de PDU d'annonce avec le contrôleur configuré en simple observateur. Le badge n'envoie aucune information, il va simplement traiter les informations envoyées par les équipements environnant.
La fonction nimble_on_initialized() démarre cette procédure de découverte et spécifie une autre fonction de rappel, celle-ci ayant pour rôle de traiter les annonces reçues par le contrôleur BLE de l'ESP32:
void ble_start_device_discovery(void)
{
undefined4 uStack_18;
undefined2 uStack_14;
undefined1 auStack_11 [17];
uStack_18 = 0x300030;
uStack_14 = 0x200;
FUN_ov1__4200f9d0(0,auStack_11);
nimble_start_discover(
auStack_11[0],
0x7fffffff,
&uStack_18,
ble_handle_device_disc,
0
);
return;
}
La fonction ble_handle_device_disc() effectue une batterie de tests sur chaque annonce reçue:
void ble_handle_device_disc(char *param_1)
{
char cVar1;
char cVar2;
char cVar3;
char cVar4;
int iVar5;
undefined4 uVar6;
undefined4 uVar7;
undefined1 auStack_c4 [156];
char *pcStack_28;
char cStack_24;
/* Vérification du PDU d'annonce reçu. */
if ((((*param_1 == 0x07) &&
(iVar5 = FUN_ov1__4201ba7a(auStack_c4,*(undefined4 *)(param_1 + 0x10),param_1[5]),
iVar5 == 0)) && (cStack_24 == 0x06)) && ((*pcStack_28 == -1 && (pcStack_28[1] == -1)))) {
/* Extraction de l'octet situé à l'offset +2 (ID) */
cVar1 = pcStack_28[2];
/* Vérifie si l'ID a été vu récemment. */
iVar5 = FUN_ov1__4200f106(cVar1);
if (iVar5 != 0) {
/* On extrait trois octets aux offsets +3, +4 et +5. */
cVar2 = pcStack_28[3];
cVar3 = pcStack_28[4];
cVar4 = pcStack_28[5];
/* Affichage dans la console d'un nouvel ID détecté. */
uVar6 = FUN_ram_40811072();
FUN_ram_40810f56(2,s_BLE_CTF_ram_4207bd90,s_W_(%lu)_%s:_>>_new_(ID:%d)_!_rel_ram_4207bdcc,
uVar6,s_BLE_CTF_ram_4207bd90,cVar1);
/* On met à jour la LED RGB avec les valeurs cVar2 (R), cVar3 (G) et cVar4 (B). */
rgb_led_update?(cVar2,cVar3,cVar4);
uVar6 = _DAT_ram_408172c4;
uVar7 = FUN_ram_4081015c();
(*(code *)0x40810456)(uVar6,2,uVar7,0,0);
uVar6 = _DAT_ram_408172c0;
/* On sauvegarde l'id. */
broadcast_id = cVar1;
/* Et les valeurs R,G,B de la LED. */
DAT_ram_408172bd = cVar2;
DAT_ram_408172be = cVar3;
DAT_ram_408172bf = cVar4;
uVar7 = FUN_ram_4081015c();
(*(code *)0x40810456)(uVar6,1,uVar7,0,0);
}
}
return 0;
}
La trame d'annonce semble correspondre à un simple advertising record de 6 octets, dont les deux premiers octets valent 0xFF et les 4 derniers stockent respectivement un ID sur un octet et un triplet RGB. Le format qui correspond le mieux dans la spécification Bluetooth Low Energy est celui du Manufacturer Specific Data, dont les deux premiers octets définissent un identifiant de 16 bits associé à une société (ces valeurs sont normalisées et trouvables dans la liste des Bluetooth Assigned Numbers). Pour le moment ce n'est qu'une hypothèse, mais elle va être rapide à vérifier par la suite.
Un autre bout de code a aussi attiré mon attention:
void chall_relay_id(uint8_t relay_id, uint8_t red, uint8_t green, uint8_t blue)
{
undefined4 uVar1;
undefined1 uStack_d9;
undefined4 uStack_d8;
undefined4 uStack_d4;
undefined2 uStack_d0;
undefined1 uStack_cc;
undefined1 uStack_cb;
undefined1 uStack_ca;
undefined1 uStack_c9;
undefined1 uStack_c8;
undefined1 uStack_c7;
undefined1 auStack_c4 [156];
undefined1 *puStack_28;
undefined1 uStack_24;
/* Construit un advertising record de type Manufacturer Specific Data. */
__call_memset(auStack_c4,0,0xa4);
uStack_cc = 0xff;
uStack_cb = 0xff;
uStack_ca = (undefined1)param_1;
puStack_28 = &uStack_cc;
uStack_24 = 6;
uStack_c9 = red;
uStack_c8 = green;
uStack_c7 = blue;
FUN_ov1__42017038(auStack_c4);
/* Transmet l'advertising record en passant en mode advertising. */
uStack_d8 = 0;
uStack_d4 = 0;
uStack_d0 = 0;
FUN_ov1__4200f9d0(0,&uStack_d9);
FUN_ov1__42016d72(uStack_d9,0,1000,&uStack_d8,0,0);
uVar1 = FUN_ram_40811072();
/* Notification dans la console sur la retransmission réussie d'une commande. */
FUN_ram_40810f56(
3,
s_BLE_CTF_ram_4207bd90,
s_I_(%lu)_%s:_relay_ID:%d_send._ram_4207bd98,
uVar1,
s_BLE_CTF_ram_4207bd90,
param_1
);
return;
}
Cette fonction est appelée régulièrement par un timer, et retransmet une trame reçue précédemment avec les mêmes paramètres (ID, R, G, B).
Suite à cette analyse, le fonctionnement du badge paraît relativement évident: il est à l'écoute d'un périphérique transmettant des données d'annonce particulières, et lorsqu'il reçoit une donnée d'annonce au format attendu ce dernier configure la LED RGB avec les valeurs reçues, puis retransmet cette donnée d'annonce après un temps relativement court. L'ordre reçu est ainsi propagé de proche en proche, au travers d'un réseau maillé rudimentaire (pas de routage, seulement du broadcast). Le champ ID transmis dans la donnée d'annonce permet d'éviter la retransmission en boucle d'un ordre déjà reçu, avec certainement un délai au bout duquel l'ordre est retiré de la liste des ordres déjà traités.
Il n'y a donc pas de flag à trouver dans ce micrologiciel mais une fonctionnalité à exploiter afin de prendre le contrôle à distance de toutes les LEDs RGB des badges actifs, un peu comme ce qu'il se fait lors de certains concerts avec des bracelets lumineux synchronisés par radio.
Le fonctionnement de ce badge déduit de l'analyse du code, j'ai tenté durant la dernière demi-journée de la conférence de capturer ces trames d'activation, mais sans succès. De retour à l'hôtel, je me suis mis en tête d'essayer de transmettre une telle trame, à l'aide du framework WHAD. J'ai utilisé une fonctionnalité en cours de développement pour transmettre ce qui me semblait être un advertising record correspondant aux critères:
from time import sleep
from whad.device import Device
from whad.ble import Advertiser
from whad.ble.profile.advdata import AdvDataFieldList, AdvManufacturerSpecificData
dev = Device.create("hci1")
advertiser = Advertiser(
dev,
adv_data=AdvDataFieldList(
AdvManufacturerSpecificData(0xffff, b'')
)
)
pid = 0
r,g,b = 0,0,0
while True:
if pid%3 == 0:
r,g,b = 0xff,0,0
if pid%3 == 1:
r,g,b = 0,0xff,0
if pid%3 == 2:
r,g,b = 0,0,0xff
pid += 1
advertiser.update(adv_data=AdvDataFieldList(
AdvManufacturerSpecificData(0xffff, bytes([pid & 0xff, r, g, b]))
))
sleep(1)
Ce petit bout de code envoie des données d'annonce structurées suivant le format retrouvé par rétro-ingénierie, en ordonnant au badge d'allumer la LED RGB en rouge, vert, bleu et cela de façon cyclique. La valeur du champ ID est incrémentée constamment pour éviter qu'un nouvel ordre soit ignoré.
Il se trouve que c'était exactement ce qu'attendait le badge, et j'ai pu observer la LED passer d'une couleur à l'autre au rythme des trames émises par mon code:
J'avais pourtant attaqué l'analyse par rétro-ingénierie du micrologiciel du badge dans la matinée, mais suite à notre talk, Axelle et moi-même avons passé beaucoup de temps à discuter avec des participants et c'est seulement en fin d'après-midi que j'ai pu me remettre sur l'analyse du code. J'ai aussi perdu beaucoup de temps à tenter de capturer une trame d'activation officielle, sans succès. C'est dommage, ça aurait été vraiment drôle de prendre le contrôle des LEDs des badges durant un talk !
Cette édition de THCon était vraiment top, ça a été l'occasion de revoir tout plein de gens et d'assister à quelques talks excellents. Encore un grand merci aux organisateurs, et vivement la prochaine édition (avec un badge électronique) !
Un grand merci à DVID et Baptiste Rebillard pour le badge électronique et le challenge BLE.
Note aux francophones: j'ai rédigé ce write-up initialement en anglais, et je n'ai pas eu/pris le temps de le traduire en français. Je sais, je commence mal l'année 2022 (mais lisez ce write-up, l'épreuve était vraiment sympa =)
During the ph0wn CTF that took place virtually on December the 3rd, a strange challenge called Wazabee were available with the following description:
Hint: Do some OSINT on the challenge author to find what this can be about.
Jean Reno has created a network surveillance startup, called Wazabee. He
installed this Android application (download it) on his smartphone and it
looks like it is transmitting something important. But what? Help him,
and you might get to play in Wasabi 2.
Install the application on an Android smartphone (it is not malicious)
that supports Extended advertising and LE 2M. You can easily test your phone's
capabilities with the nRF Connect application. Go in "Device information" and
check the corresponding labels are green.
Your smartphone does not support it? Too bad :=) Get one! Or find another way
to solve the challenge (there are several solutions).
The flag follows the usual format: ph0wn{ .... } where it can any printable
character between the brackets.
We are provided with an Android APK that must be run on a smartphone that supports Extended advertising and LE 2M.
Well, let's have a look at it.
First of all, I don't own a smartphone that supports Extended advertising and LE 2M but I know a little of Bluetooth Low Energy. Extended advertisements and a bitrate of 2 Mbps have been introduced in the version 5 of the Bluetooth core specification, that allows any compatible device to send BLE advertisements on all channels (instead of the only 3 advertising channels 37, 38, and 39 as specified in previous versions) at 2 Mbit per second.
Second, a hint is given in the description, telling us to do a background search on the author of this challenge (Romain Cayre). A quick search revealed that Romain Cayre published a paper written in French, at the French SSTIC conference, called "WazaBee : attaque de réseaux Zigbee par détournement de puces Bluetooth Low Energy" (or "WazaBee: attacking Zigbee networks by subverting Bluetooth Low Energy chips").
This paper presents a way to make a BLE 5 compatible chip send Zigbee frames by abusing the GFSK modulation used in Bluetooth Low Energy PHY. This paper contains the word "Wazabee" in its title (which sounds like "wasabi" in French -- also a French movie starring Jean Reno) and is about Bluetooth Low Energy (and Zigbee). Well, that's becoming interesting !
Once disassembled using Jadx, the interesting code is found in the radio.sploit.phownchallenge.SecondFragment class:
@Override // androidx.fragment.app.Fragment
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
final BluetoothLeAdvertiser advertiser = BluetoothAdapter.getDefaultAdapter()
.getBluetoothLeAdvertiser();
final View v = getView();
if (!adapter.isLe2MPhySupported()) {
Snackbar.make(v, "2M PHY not supported!", -1).show();
} else if (!adapter.isLeExtendedAdvertisingSupported()) {
Snackbar.make(v, "LE Extended Advertising not supported!", -1).show();
} else {
This code checks that the 2 Mbps bitrate and extended advertising are supported, and then builds a payload that is sent in bluetooth low energy advertisements:
AdvertisingSetParameters.Builder parameters = new AdvertisingSetParameters.Builder()
.setLegacyMode(false)
.setInterval(160)
.setTxPowerLevel(1)
.setPrimaryPhy(1)
.setSecondaryPhy(2);
String a = getString(R.string.a);
String b = getString(R.string.b);
String c = getString(R.string.c);
String d = getString(R.string.d);
String e = getString(R.string.e);
String out = "";
int x = 0;
while (out.length() != 64) {
if (x % 4 == 0) {
out = a + out + b;
}
if (x % 4 == 1) {
out = c + out + d;
}
if (x % 4 == 2) {
out = d + out + c;
}
if (x % 4 == 3) {
out = b + out + a;
}
x++;
}
AdvertiseData data = new AdvertiseData.Builder()
.addManufacturerData(
4660,
dewhiten(hexStringToByteArray(out + e), 17, 16)
).build();
The referenced strings are the following:
Let's write some Python code to generate the same advertising data:
# generate out
a = '1b'
b = '03'
c = '3a'
d = 'f7'
e = '70afb33965fc08453b9b03773970af3303f73a1b70afb339fc08c564f73a9b03' \
'03f73a1bb239702f70afb33970afb33908c5647cf73a9b034dc68f5070afb339' \
'c68f504cb239702f514cc60fb239702f4dc68f50f73a9b0303f73a1b70afb339' \
'c464fc08aeb33970b239702ff73a9b03f73a9b0370afb33965fc0845aeb33970' \
'464fc0870afb33970afb33970afb339aeb33970aeb33970514cc60f70afb3390'
'3f73a1bc68f504c70afb3393970af33'
x = 0
out = ''
while len(out)!=64:
if x%4==0:
out = a + out + b
if x%4==1:
out = c + out + d
if x%4==2:
out = d + out + c
if x%4==3:
out = b + out + a
x += 1
# Payload is dewhitened before sending, because it will be whitened
# again when sent by the BLE chip =)
payload = out + e
print('advertising payload:\n%s' % payload)
data = bytearray.fromhex(payload)
This script produces the following output:
advertising payload:
03f73a1b03f73a1b03f73a1b03f73a1b
03f73a1b03f73a1b03f73a1b03f73a1b
70afb33965fc08453b9b03773970af33
03f73a1b70afb339fc08c564f73a9b03
03f73a1bb239702f70afb33970afb339
08c5647cf73a9b034dc68f5070afb339
c68f504cb239702f514cc60fb239702f
4dc68f50f73a9b0303f73a1b70afb339
c464fc08aeb33970b239702ff73a9b03
f73a9b0370afb33965fc0845aeb33970
464fc0870afb33970afb33970afb339a
eb33970aeb33970514cc60f70afb3390
This data is what is sent in BLE advertisements as manufacturer data, but the meaning is still unknown. It's now time to dig into Cayre's paper to understand what is going on !
We won't cover all the details of Cayre's paper (so read it if you are looking for more information) but only the most interesting aspects that may be useful to decode this data.
Cayre found out that Bluetooth Low Energy PHY layer uses a Gaussian Frequency Shift Keying (GFSK) modulation that can be considered (in a way) as an Minimum Shift Keying (MSK) modulation. This MSK modulation when used with a 2 Mbit per second bitrate may be used to generate a RF signal (composed of I/Q samples) that fits the O-QPSK modulation used by Zigbee. In short, a BLE chip can send BLE data at 2 Mbps on a specific BLE channel that would be interpreted by a Zigbee receiver (using an O-QPSK demodulator) as a valid series of bits representing a Zigbee frame.
Cayre provides in his paper a specific table (table 2) that can be used to convert a block of 4 bits of Zigbee data into a series of 31 bits that would be interpreted as a PN sequence used in Zigbee PHY layer. And this is exactly what this Android application does: it sends a series of bits using MSK modulation that will be decoded as PN sequences and then turned into a series of 4-bit blocks by a Zigbee receiver and interpreted as such. For the record, the payload is dewhitened before sending because the BLE chip will whiten the data (and undo this dewhitening) before sending it, thus ensuring our payload is sent as-is.
Therefore, we need to do some bit-level magic with our recovered advertising payload in order to extract the MSK-encoded PN sequences with the help of some Python code:
# reverse lookup table from Cayre's paper (table 2)
msk_to_block = {
'1100000011101111010111001101100':'0000',
'1001110000001110111101011100110':'1000',
'0101100111000000111011110101110':'0100',
'0100110110011100000011101111010':'1100',
'1101110011011001110000001110111':'0010',
'0111010111001101100111000000111':'1010',
'1110111101011100110110011100000':'0110',
'0000111011110101110011011001110':'1110',
'0011111100010000101000110010011':'0001',
'0110001111110001000010100011001':'1001',
'1010011000111111000100001010001':'0101',
'1011001001100011111100010000101':'1101',
'0010001100100110001111110001000':'0011',
'1000101000110010011000111111000':'1011',
'0001000010100011001001100011111':'0111',
'1111000100001010001100100110001':'1111'
}
def msk_to_chip(seq):
assert seq in msk_to_block
return msk_to_block[seq]
def byte_to_bits(b):
return bin(b)[2:].rjust(8,'0')[::-1]
# generate out
a = '1b'
b = '03'
c = '3a'
d = 'f7'
e = '70afb33965fc08453b9b03773970af3303f73a1b70afb339fc08c564f73a9b03' \
'03f73a1bb239702f70afb33970afb33908c5647cf73a9b034dc68f5070afb339' \
'c68f504cb239702f514cc60fb239702f4dc68f50f73a9b0303f73a1b70afb339' \
'c464fc08aeb33970b239702ff73a9b03f73a9b0370afb33965fc0845aeb33970' \
'464fc0870afb33970afb33970afb339aeb33970aeb33970514cc60f70afb3390'
'3f73a1bc68f504c70afb3393970af33'
x = 0
out = ''
while len(out)!=64:
if x%4==0:
out = a + out + b
if x%4==1:
out = c + out + d
if x%4==2:
out = d + out + c
if x%4==3:
out = b + out + a
x += 1
# Payload is dewhitened before sending, because it will be whitened
again when sent by the BLE chip =)
payload = out + e
data = bytearray.fromhex(payload)
# Convert payload into bits
data_bits = ''
for c in data:
data_bits += byte_to_bits(c)
# Split by blocks of 31 bits (MSK-encoded PN sequences)
# and convert to corresponding 4-bit chip
chips = [data_bits[32*i:32*(i+1)] for i in range(int(len(data_bits)/32))]
output = ''.join([msk_to_chip(chip[:31]) for chip in chips])
This small script produces the following output:
00000000000000000000000000000000
11100101001010000000111000010110
00001100111011100111011011011110
10011100101111001101011000001110
00111010110001100110111001011010
00111110111011101010101010111110
0000100111101000
A little bit more of Python to convert this into bytes:
# Group output by 8 bits and rebuild data
nbytes = int(len(output)/8.)
output_bytes = [chr(int(output[8*i:8*(i+1)][::-1],2)) for i in range(nbytes)]
print(''.join(output_bytes))
And this displays the following text (the first null bytes are omitted): §ph0wn{9=kpcvZ|wU}. These bytes may correspond to a Zigbee beacon, but we don't need to decode it as the flag is clearly readable.