Faille des portefeuilles matériels Coldcard : 1 719 BTC dérobés lors d'une attaque en 2026
Résumé du marché par IA
Une faille d'entropie du firmware Coldcard, signalée, a permis la récupération hors ligne par force brute de seeds pour des appareils configurés par défaut, entraînant un vol confirmé d'au moins 1 719 BTC sur des milliers d'adresses. L'incident est significatif car il requalifie le risque de "cold storage" autour de la qualité de génération des clés plutôt que d'une compromission du réseau, ce qui pourrait mettre sous pression la confiance dans les portefeuilles matériels, susciter des mises à jour urgentes du firmware et accroître, à court terme, les mouvements de fonds motivés par la sécurité ainsi que la vigilance et l'examen des pratiques de conservation (custody) du BTC.
Niveau d'impact
● Élevé
Actifs concernés
BTC/USDT-1.73%
Infos de l'IA · BTC/USDTInfos de l'IA
▼ Baissier
Trader maintenant
⚠️ Les infos générées par l'IA sont basées sur des contenus d'actualité et fournies à titre informatif uniquement. Elles ne constituent pas des conseils en investissement et ne reflètent pas les positions de BingX. Investir comporte des risques. Tradez de manière responsable.
Auteur : Johan & Lisa — Édition : 77
Contexte
Le 30 juillet 2026, une série d'adresses a commencé à transférer ses fonds de façon séquentielle sur la blockchain. En 41 minutes, 1 196 adresses en signature unique ont été vidées, soit environ 1 082 bitcoins envolés. Il ne s'agissait que de la première vague. Début août, les pertes confirmées atteignaient au moins 1 719 BTC (environ 111 millions de dollars), sur plus de 5 200 adresses, l'opération s'étant déroulée en trois à quatre vagues.
Le point le plus déroutant concerne l'état des portefeuilles visés : la plupart des fonds étaient stockés à froid et n'avaient pas bougé depuis des mois, voire des années. Le problème se situe au niveau des clés privées. Toutes les clés compromises ont été générées par des portefeuilles matériels Coldcard, sans ajout d'entropie via des lancers de dés et sans activation d'une passphrase BIP39. Les victimes utilisaient la configuration par défaut, la plus simple.
L'ampleur est analysée selon la lignée de firmware. Sur Mk2 et Mk3 (firmwares 4.0.1 à 4.1.9), l'entropie effective tombe autour de 40 bits, le scénario le plus critique. Sur Mk4, Mk5 et Q, certaines versions de firmware laissent environ 72 bits d'entropie, encore dans une zone exploitable par force brute hors ligne.
Coinkite a réagi rapidement. Le 30 juillet, l'entreprise a publié une alerte, puis, le lendemain, un firmware correctif : Mk2 et Mk3 en 4.2.0 ou plus, Mk4 et Mk5 en 5.6.0 ou plus, et Q en 1.5.0Q ou plus. Les stocks des versions vulnérables ont été détruits et les expéditions suspendues.
Selon les éléments disponibles, l'attaque n'implique ni intrusion à distance ni compromission de la chaîne d'approvisionnement. L'attaquant n'a jamais eu besoin de manipuler les appareils : il a récupéré les clés privées par force brute hors ligne, en testant systématiquement chaque graine possible dans l'espace de recherche.
La faiblesse serait restée invisible pendant des années jusqu'à ce que des chercheurs externes mettent au point un outil de brute force, révélant le problème. Muan a ensuite reproduit l'ensemble de la chaîne d'attaque. L'analyse ci-dessous repart du code firmware, en prenant Mk3 (v4.1.9) comme exemple, pour isoler la ligne de code qui a rendu possible le drainage massif de milliers d'appareils Coldcard.
Origine de la vulnérabilité
Coldcard est conçu pour générer des seeds via le TRNG (true random number generator) matériel intégré au STM32L475. Deux erreurs dans le firmware ont toutefois transformé ce processus en un PRNG logiciel dont l'état est quasi identique entre appareils à l'échelle mondiale. Ce PRNG, Yasmarang, fait chuter l'entropie effective du Mk3 à environ 40 bits.
Erreur n°1 : le RNG matériel a été désactivé
La première erreur se situe dans la configuration de build : Coldcard désactive explicitement la prise en charge du RNG matériel de MicroPython dans mpconfigboard.h.
// stm32/COLDCARD/mpconfigboard.h
// The team has implemented their own version of ckcc.rng_bytes, so disable MicroPython's builtin version
#define MICROPY_HW_ENABLE_RNG (0)
Pris isolément, ce macro peut sembler anodin : Coinkite dispose de son propre ckcc.rng_bytes, qui appelle directement le TRNG du STM32 et offre plus de contrôle que l'implémentation de la couche port MicroPython. Le piège vient de ce que ce macro entraîne, plus bas, un autre comportement : la fonction my_random_bytes() de libngu lit l'aléa via le macro CHIP_TRNG_32(), lequel, après expansion, appelle rng_get() fourni par la couche STM32 de MicroPython, en supposant qu'il s'agit d'un TRNG matériel. Or rng_get() se dégrade silencieusement quand MICROPY_HW_ENABLE_RNG vaut 0, ce qui mène à la seconde erreur.
Erreur n°2 : le fallback de rng_get() a été pris à tort pour un TRNG matériel
En partant de random.c (libngu) et en déroulant les expansions de macros, on arrive sur la branche else de ports/stm32/rng.c (couche port STM32 de MicroPython).
// external/libngu/ngu/random.c
#ifdef MICROPY_PY_STM
// ports/stm32/rng.c
extern uint32_t rng_get(void);
# define CHIP_TRNG_SETUP()
# define CHIP_TRNG_32() rng_get()
#endif
...
void my_random_bytes(uint8_t *dest, uint32_t count) {
uint32_t chip = CHIP_TRNG_32();
// Assumes reading from hardware TRNG, but may actually receive Yasmarang output
if (chip == last) ...
// Reports error if two consecutive words are identical
chip ^= my_yasmarang();
// XORs with a global constant Yasmarang stream (pad=0x0a8ce26f)
...
}
rng_get() provient de ports/stm32/rng.c. La fonction teste #if MICROPY_HW_ENABLE_RNG : si le macro est non nul, elle lit RNG>DR, c'est-à-dire le TRNG matériel. Si le macro vaut zéro, la branche else est compilée et renvoie la sortie d'un PRNG logiciel. La configuration Coldcard étant précisément à 0, chaque Mk3 sorti d'usine emprunte ce chemin.
// external/micropython/ports/stm32/rng.c
#if MICROPY_HW_ENABLE_RNG
uint32_t rng_get(void) {
... read RNG>DR hardware TRNG ...
}
#else
// MICROPY_HW_ENABLE_RNG
// Vulnerability point: Fallback exists, and seed is almost entirely predictable
static uint32_t pyb_rng_yasmarang(void) {
static bool seeded = false;
static uint32_t pad = 0, n = 0, d = 0;
static uint8_t dat = 0;
if (!seeded) {
seeded = true;
rtc_init_finalise();
pad = *(uint32_t *)MP_HAL_UNIQUE_ID_ADDRESS ^ SysTick>VAL;
// pad = UID ^ SysTick
n = RTC>TR;
// n = RTC_TR
d = RTC>SSR;
// d = RTC_SSR
}
pad += dat + d * n;
pad = (pad > 29);
n = pad | 2;
d ^= (pad > 1);
dat ^= (char)pad ^ (d >> 8) ^ 1;
return pad ^ (d > 18) ^ (dat VAL when rng_get() is first called.
}
#endif
SysTick>VAL est la valeur du compteur SysTick au moment du premier appel à rng_get(). Le processeur fonctionne à 80 MHz, avec une valeur de reload de 80 000, ce qui donne des valeurs entre 0 et 79 999, soit environ 17 bits d'entropie. RTC_TR et RTC_SSR correspondent aux registres RTC juste après la mise sous tension, avant initialisation par l'application, avec des espaces de valeurs très réduits. rtc_tr est le RTC>TR encodé en BCD packé, et rtc_ssr est borné par MK3_RTC_SSR_MAX à 0xFF. Sur tous les vecteurs on-chain vérifiés, ces deux valeurs sont à 0.
En agrégeant les composantes, l'UID apporte environ 14 bits, SysTick environ 17 bits, les deux entrées RTC 0 bit, et les pressions de boutons autour de 5 bits, soit 36 à 37 bits de vraie entropie sur l'ensemble de la génération de seed. L'affirmation de Coinkite ("environ 40 bits") colle avec l'espace de recherche mis en évidence par rétro-ingénierie : un espace brute-forçable en quelques jours par un cluster GPU.
Déroulé de l'attaque
Lors du premier démarrage d'un Mk3, la consommation d'aléa suit trois profils typiques. Le travail clé de l'attaquant consiste à modéliser chaque profil et à reconstruire précisément chaque étape du PRNG.
Phase 1 : état initial
Deux événements surviennent dès l'alimentation.
Power on
└── libngu static Yasmarang (ngu/random.c) pad=0x0a8ce26f n=69 d=233 dat=0 ← Global constant
└── rng_get() initial call seeding (ports/stm32/rng.c) pad=UID^SysTick n=RTC_TR=0 d=RTC_SSR=0 dat=0
À ce stade, toute la "randomness" se trouve comprimée dans un pad 32 bits, plus deux petites dimensions énumérables. Le pad global des Mk3 reste confiné à une boîte finie, exploitable.
Phase 2 : opérations via les boutons
Lors de la première configuration, l'utilisateur doit définir un PIN, accepter des conditions en pressant OK et naviguer dans des menus. Chaque appui déclenche _start_scan() dans shared/mempad.py, qui appelle _rand_below() trois fois via shuffle(self.scan_order). La taille de scan_order vaut NUM_ROWS = 4. shuffle est défini dans shared/random.py, et randbelow est relié directement à ngu.random.uniform (libngu), équivalent C de _rand_below.
# shared/random.py
import ngu
randbelow = ngu.random.uniform
# Equivalent to _rand_below in libngu random.c
bytes = ngu.random.bytes
# Equivalent to my_random_bytes
def shuffle(lst):
# Fisher-Yates, same as CPython random.py
for i in reversed(range(1, len(lst))):
j = randbelow(i + 1)
# ← Consumes 1 step chip + 1 step mixer (including rejection sampling retries)
lst[i], lst[j] = lst[j], lst[i]
# shared/mempad.py
# Each key press > _start_scan() > shuffle(self.scan_order)
# scan_order length = NUM_ROWS = 4
Ces motifs de consommation doivent être modélisés séparément. L'analyse du code v4.1.9 met en évidence trois schémas.
Profil A : configuration initiale "grand public"
Deux pressions avant l'acceptation des conditions ; puis settings.save() cherche un slot libre parmi 32 en excluant my_pos, ce qui laisse 31 slots et génère 30 appels à _rand_below. Ensuite, saisie du PIN, navigation, quelques pressions supplémentaires, puis collecte d'entropie via random_bytes(32).
kpad_a × shuffle(4)
# keypress before accept_terms (kpad_a = 2)
shuffle(31)
# settings.save() searches for an empty slot among 32 slots, excluding my_pos, leaving 31 slots = 30 calls to _rand_below
kpad_b × shuffle(4)
# PIN entry + menu navigation (kpad_b enumerates [4, 34])
random_bytes(32)
# gather entropy
Profil B : premier boot après flash/effacement, NVRAM vide
nvstore exécute d'abord un shuffle(32), puis consomme 3 072 étapes en lockstep sur 3 slots × 16 blocs × 256 octets, sans réinjecter la sortie (l'état avance seulement). Pour un démarrage "NVRAM vide", un shuffle(32) additionnel suit, puis la consommation liée aux touches, puis my_random_bytes(32).
nvstore shuffle(32)
# 31 calls to _rand_below
nvstore blanking 3×16×256B
# 3 slots × 16 blocks × 256B = 3072 lockstep consumption, output does not loop back (state progresses only)
emptynvramonboarding profile
# Blank NVRAM onboarding: one additional shuffle(32) after the two steps above
Key press consumption (kpad_b enum [4, 34])
# Each shuffle(4)
my_random_bytes(32)
Profil C : paper wallet
Le paper wallet correspond à une conservation à froid où la clé privée est imprimée et stockée hors ligne, souvent pour un cadeau ou du long terme. Le menu Paper Wallets de Coldcard génère des pages imprimables. La consommation d'entropie y est la plus directe : après création d'un portefeuille, revenir dans le menu nécessite 8 à 25 pressions, puis my_random_bytes(32) sert directement de clé privée, en contournant totalement la liste de mots BIP39.
Phase 3 : construction de la seed et dérivation d'adresse
De random_bytes(32) jusqu'à l'adresse Bitcoin finale, Mk3 suit la chaîne suivante.
raw_bytes = random_bytes(32)
# Concatenate 4 bytes from 8 iterations of (mpy_step ^ yas_step)
entropy = ngu.hash.sha256s(raw_bytes)
# Single SHA256, not sha256d
mnemonic = BIP39(entropy, wordlist=english)
# 24-word mnemonic
seed = PBKDF2HMACSHA512(mnemonic, "mnemonic", 2048)
master = HMACSHA512("Bitcoin seed", seed)
child = m/{44,49,84}'/0'/0'/0/0
# First receiving address
address = bech32(hash160(compressed_pubkey))
# Default BIP84
Chaque étape est déterministe et reproductible. Dès lors que le pad est deviné, l'attaquant peut recalculer toute la chaîne hors ligne.
Phase 4 : force brute GPU et correspondance
La méthode d'attaque se résume à quatre étapes.
1) Construire l'ensemble de pads candidats : prendre les coordonnées de wafer X et Y de 0 à 72, les combiner avec l'espace UID d'environ 5 300 valeurs, puis faire le produit cartésien avec SysTick (0 à 79 999). On obtient 424 millions de pads candidats. Le volume paraît élevé, mais reste traitable en quelques jours sur GPU.
2) Énumérer le nombre de pressions de touches pour chaque pad, avec kpad_b dans l'intervalle 4 à 34. Les hits observés ayant rtc_tr et rtc_ssr à zéro, ces dimensions sont placées dans la boucle externe plutôt qu'interne.
3) Exécuter la chaîne complète de génération dans le kernel GPU : SHA256, BIP39, PBKDF2 (2 048 tours), BIP32, Hash160 et Bech32. Le flux constant de libngu peut être pré-calculé hors ligne ; le kernel récupère les mots par index, évitant à chaque thread de faire avancer indépendamment le PRNG constant.
4) Faire la correspondance via un Bloom filter ou un tableau trié des hash160, avec une complexité O(log n), en ciblant l'ensemble des adresses P2WPKH en signature unique sur le réseau.
Le coût du balayage est quantifiable. Sur l'espace Phase A, un seul GPU Apple M1 (UID 72×72, 80 000 variations SysTick, 31 valeurs de pressions) a traité environ 14,8 milliards de candidats en 8,6 jours. Un cluster A100 de classe datacenter ramènerait la même recherche à quelques heures.
L'espace d'environ 72 bits sur Mk4, Mk5 et Q est 2^32 fois plus grand que celui du Mk3, mais l'origine de la faille est identique et l'approche par modélisation des profils reste la même. Le passage se fait surtout sur la capacité de calcul : d'une machine unique à un cluster, avec un horizon qui passe de quelques jours à plusieurs semaines — un coût que des attaquants sont encore prêts à assumer, ce qui explique la présence de ces modèles dans les listes de victimes.