Certains bugs se cachent derrière une exploitation astucieuse. Celui-ci est un memcpy sans aucun contrôle de longueur, atteignable avant authentification, depuis n’importe quelle radio BLE à portée.

TL;DR

CVE-2026-47773 (GHSA-77v6-cw9f-9whg). Un contrôle de bornes manquant dans le handler de write request ATT d’ArduinoBLE.

Quand un client écrit dans une caractéristique qui exige le chiffrement (la permission BLEEncryption) alors que le lien n’est pas encore chiffré en AES, ArduinoBLE range l’écriture en attente dans un writeBuffer fixe de 64 octets à l’intérieur de l’objet global ATTClass, pour la rejouer une fois le chiffrement établi. Il copie la valeur fournie par l’attaquant dans ce buffer sans aucun contrôle de longueur.

Un client BLE distant et non authentifié envoie une écriture surdimensionnée et déborde le buffer avec un contenu qu’il contrôle intégralement, corrompant tout ce que le firmware a placé à sa suite dans l’objet global.

  • Faiblesse : CWE-787 (écriture hors limites) plus CWE-131 (calcul incorrect de la taille du buffer)
  • Sévérité : moyenne (aucun score CVSS attribué)
  • Affecté : ArduinoBLE <= 2.0.1. Corrigé : 2.0.2
  • Précondition : l’appareil expose au moins une caractéristique inscriptible portant la permission BLEEncryption

Contexte : le chemin d’écriture ATT

ArduinoBLE est la pile Bluetooth Low Energy pour les cartes Arduino (Nano 33 BLE, Nano RP2040 Connect, et d’autres). En son cœur se trouve un unique objet global ATTClass, ATT, qui analyse les PDU du protocole ATT entrantes. Les écritures vers une caractéristique GATT arrivent sous forme de ATT_OP_WRITE_REQ ou ATT_OP_WRITE_CMD et atterrissent dans ATTClass::writeReqOrCmd().

Une caractéristique peut être marquée comme exigeant le chiffrement avec la permission BLEEncryption. Si un client tente d’écrire dans une telle caractéristique avant que la connexion soit chiffrée en AES, ArduinoBLE ne se contente pas de rejeter la requête. Il répond avec INSUFF_ENC et range l’écriture en attente dans un buffer membre, afin de pouvoir la rejouer une fois le lien devenu chiffré.

C’est dans ce rangement que se niche le bug. Il s’exécute sur le chemin pré-chiffrement, qui est précisément le chemin qu’un attaquant non authentifié atteint en premier.

Le bug : un memcpy sans contrôle de bornes

Le buffer de rangement fait 64 octets fixes, un simple membre de l’objet global :

src/utility/ATT.h
uint8_t writeBuffer[64];
uint8_t writeBufferSize;

À l’intérieur de writeReqOrCmd(), la longueur et le pointeur de la valeur proviennent directement de la PDU entrante, et la branche « garder pour le chiffrement » remplit writeBuffer à la main :

src/utility/ATT.cpp, ATTClass::writeReqOrCmd() (vulnerable, <= 2.0.1)
uint8_t valueLength = dlen - sizeof(handle);   // attacker-controlled ATT PDU length
uint8_t* value      = &data[sizeof(handle)];
// ...
if (holdResponse) {                            // set when BLEEncryption is required
    writeBufferSize = 0;                       //   and the peer is not yet encrypted
    memcpy(writeBuffer, &handle, 2);              writeBufferSize += 2;
    writeBuffer[writeBufferSize++] = _peers[i].addressType;
    memcpy(&writeBuffer[writeBufferSize], _peers[i].address, 6);
    writeBufferSize += 6;
    writeBuffer[writeBufferSize] = valueLength;   writeBufferSize += 1;

    // no check that writeBufferSize + valueLength fits in writeBuffer
    memcpy(&writeBuffer[writeBufferSize], value, valueLength);   // out-of-bounds write
    writeBufferSize += valueLength;
}

Comptons l’en-tête qu’il écrit avant la valeur : handle (2) plus addressType (1) plus address (6) plus valueLength (1) égale 10 octets. Cela laisse 54 octets de marge dans le buffer de 64 octets.

Mais valueLength est un uint8_t dérivé de la longueur de la PDU ATT entrante, et rien dans le handler ne le borne par rapport à writeBuffer. Cette longueur remonte depuis la couche L2CAP : ArduinoBLE réassemble la PDU entrante dans un buffer L2CAP de 255 octets, de sorte qu’un attaquant peut pousser valueLength jusqu’à environ 248 octets. Le handler ne vérifie jamais la longueur de l’écriture par rapport au buffer, et il ne la vérifie même pas par rapport au MTU négocié. Une seule écriture surdimensionnée fait donc courir le memcpy bien au-delà de la fin du writeBuffer de 64 octets et jusque dans tout ce que le firmware a placé à sa suite dans l’objet global ATTClass. Aucune négociation de MTU n’est nécessaire : cela se déclenche au MTU par défaut de 23 octets, qu’ArduinoBLE plafonne sans jamais l’imposer à la longueur de l’écriture de toute façon.

Les octets de la valeur sont entièrement contrôlés par l’attaquant. Ce n’est pas un écrasement aléatoire, c’est un débordement avec un contenu choisi, émis par une radio non authentifiée.

Qui peut le déclencher

Tout ce dont l’attaquant a besoin se trouve sur le chemin pré-chiffrement, donc aucun pairing, bonding ou clé n’est impliqué :

  • L’appareil cible expose au moins une caractéristique inscriptible portant la permission BLEEncryption.
  • L’attaquant se connecte en BLE, sans pairing ni bonding, et envoie une requête d’écriture vers le handle de cette caractéristique portant une valeur plus longue que les 54 octets de marge, fragmentée sur L2CAP si nécessaire.
  • Aucun échange de MTU n’est requis. Le handler ne vérifie jamais la longueur de l’écriture par rapport au MTU, et ArduinoBLE réassemble de lui-même la PDU surdimensionnée dans son buffer L2CAP de 255 octets.

Le chemin de rangement INSUFF_ENC se déclenche, la valeur surdimensionnée est copiée au-delà du buffer, et l’objet global est corrompu. Aucune interaction utilisateur, aucune authentification.

Ce que cela permet

Une note d’honnêteté d’abord : nous n’avons pas construit de preuve de concept pour ce bug. Ce qui suit est une analyse raisonnée de la primitive, ancrée dans le code source ci-dessus, et non une exploitation démontrée. La primitive elle-même est suffisamment claire pour raisonner dessus : un client distant et non authentifié, sans interaction utilisateur, obtient une écriture hors limites à contenu contrôlé qui démarre à un offset fixe (10 octets) à l’intérieur du writeBuffer de 64 octets et se poursuit dans le reste de l’objet global ATTClass.

Non démontré. Nous ne l’avons pas exécuté sur du matériel. Les paliers ci-dessous distinguent ce que la primitive fournit de manière fiable de ce qui nécessiterait un exploit spécifique à la cible que nous n’avons pas écrit.

Palier 1, déni de service distant (attendu). Déborder un objet global actif corrompt l’état de la pile BLE elle-même pendant son exécution. Le résultat fiable et portable est un crash ou une radio bloquée : une seule écriture surdimensionnée met le périphérique hors ligne jusqu’au redémarrage. Cela ne nécessite rien de plus que le débordement lui-même et ne dépend pas de l’agencement mémoire exact.

Palier 2, corruption d’état ciblée (conditionnel). Parce que le contenu du débordement est choisi par l’attaquant, les octets juste après writeBuffer sont un levier, et non pas seulement des dégâts collatéraux. Les voisins immédiats sont holdBufferSize puis writeBufferSize ; au-delà se trouvent le reste de l’objet ATTClass (la table de connexions _peers, la comptabilité ATT, des pointeurs) et tout ce que le linker a placé après le global. Écraser ces éléments pourrait désynchroniser la machine à états ATT, corrompre les entrées de pairs stockées, ou altérer l’écriture en attente qui sera précisément rejouée une fois le lien chiffré. Tout cela dépend de l’agencement de l’objet dans un build donné.

Palier 3, détournement du flot d’exécution (spéculatif, spécifique à la cible). Si un pointeur de fonction, ou une structure que la pile déréférence ensuite, se trouve à portée du débordement sur une image firmware spécifique, une écriture à contenu choisi pourrait en principe rediriger l’exécution avec les privilèges de la tâche BLE. Qu’une telle cible soit réellement à portée dépend entièrement de la carte, du build ArduinoBLE et de l’agencement du linker. Nous n’en avons identifié aucune et ne formulons aucune affirmation générale. C’est le plafond à investiguer appareil par appareil, pas un résultat.

L’impact portable et défendable est un déni de service distant, avant authentification. Tout ce qui va au-delà est spécifique à la cible et nécessiterait un exploit propre à chaque firmware. Arduino a évalué le problème comme moyen, et aucun score CVSS n’a été attribué.

Le correctif

La version 2.0.2 ajoute l’unique contrôle qui manquait, juste avant la copie :

the fix (ArduinoBLE 2.0.2)
if (writeBufferSize + valueLength > sizeof(writeBuffer)) {
    sendError(connectionHandle, op, handle, ATT_ECODE_INSUFF_RESOURCES);
    return;
}
memcpy(&writeBuffer[writeBufferSize], value, valueLength);

Si l’écriture en attente ne rentre pas, elle est rejetée avec INSUFF_RESOURCES au lieu d’être copiée. Quatre lignes. Si vous embarquez ArduinoBLE, mettez à jour vers 2.0.2.

Versions affectées

VersionStatut
<= 2.0.1Vulnérable
2.0.2 et ultérieuresCorrigé

Commit du correctif 1460e1a (PR #431), publié dans ArduinoBLE 2.0.2.

Chronologie

Divulgation coordonnée avec Arduino. L’advisory et la version corrigée (2.0.2) ont été publiés le 2026-06-03.

Références

  • GHSA-77v6-cw9f-9whg, advisory Arduino pour CVE-2026-47773
  • ArduinoBLE PR #431, le correctif
  • src/utility/ATT.cpp, ATTClass::writeReqOrCmd(), le handler vulnérable
  • src/utility/ATT.h, le membre writeBuffer de 64 octets