Attacco del 2026 sfrutta una falla dei wallet Coldcard: sottratti 1.719 BTC

Riepilogo di mercato AI
Un'anomalia di entropia nel firmware Coldcard, come riportato, ha consentito il recupero tramite brute force offline dei seed per i dispositivi configurati con le impostazioni predefinite, portando a furti confermati di almeno 1.719 BTC su migliaia di indirizzi. L'incidente è significativo perché ridefinisce il rischio dello "storage a freddo" in termini di qualità della generazione delle chiavi piuttosto che di compromissione della rete, potenzialmente mettendo sotto pressione la fiducia nei wallet hardware, sollecitando aggiornamenti urgenti del firmware e aumentando, nel breve termine, i movimenti di fondi guidati dalla sicurezza e l'attenzione sulle pratiche di custodia di BTC.
Livello dell'impatto
● Elevato
Asset interessati
BTC/USDT-1.62%
Approfondimenti AI · BTC/USDTApprofondimenti AI
▼ Ribassista
Fai subito trading
⚠️ Le analisi generate dall'AI si basano sui contenuti delle notizie e sono forniti esclusivamente a scopo informativo. Non costituiscono consulenza in materia di investimenti né rappresentano le opinioni di BingX. Investire comporta rischi. Fai trading in modo responsabile.
Autori: Johan & Lisa Revisione: 77 Contesto Il 30 luglio 2026 una serie di indirizzi sulla blockchain ha iniziato a spostare i fondi in modo sequenziale. In 41 minuti sono stati svuotati 1.196 indirizzi singlesignature, con la sparizione di circa 1.082 bitcoin. È stato solo il primo fronte: entro i primi di agosto le perdite confermate hanno raggiunto almeno 1.719 BTC (circa 111 milioni di dollari), coinvolgendo oltre 5.200 indirizzi, in un'operazione articolata in tre o quattro ondate. L'elemento più anomalo riguarda lo stato dei wallet colpiti: molti fondi risultavano custoditi in cold wallet inattivi da mesi o anni. Il problema era a livello di chiavi private. In tutti i casi, le chiavi erano state generate da hardware wallet Coldcard e i proprietari non avevano aggiunto entropia extra con i dadi né attivato una passphrase BIP39. I dispositivi compromessi avevano usato la configurazione predefinita più semplice. L'impatto varia in base alla linea firmware. Per Mk2 e Mk3 con firmware dalla versione 4.0.1 alla 4.1.9, l'entropia effettiva scende a circa 40 bit, lo scenario più grave. Per Mk4, Mk5 e Q su specifiche versioni firmware, l'entropia stimata è intorno a 72 bit, ancora compatibile con attacchi offline di bruteforce. Coinkite ha reagito rapidamente: il 30 luglio ha diffuso un avviso e il giorno successivo ha rilasciato firmware corretti. Mk2 e Mk3 devono aggiornare alla 4.2.0 o superiore, Mk4 e Mk5 alla 5.6.0 o superiore, Q alla 1.5.0Q o superiore. L'azienda ha anche distrutto l'inventario delle versioni vulnerabili e sospeso le spedizioni. L'attacco non è legato a intrusioni da remoto né a manomissioni della supply chain. L'attaccante non ha mai avuto accesso ai dispositivi: ha ricavato le chiavi private tramite bruteforce offline, testando sistematicamente i seed possibili nello spazio di ricerca. La falla è rimasta latente per anni, fino a quando ricercatori esterni hanno sviluppato un tool di bruteforce che l'ha resa evidente. Muan ha poi riprodotto l'intera catena d'attacco. L'analisi seguente parte dal codice firmware (esempio: Mk3 firmware 4.1.9) e ricostruisce la singola riga che ha reso possibile lo svuotamento di massa di migliaia di dispositivi Coldcard. Radice della vulnerabilità Coldcard è progettato per generare seed usando il TRNG (true random number generator) hardware integrato nel chip STM32L475. Due errori firmware, combinati, hanno trasformato quel processo in un PRNG software con stato quasi identico su dispositivi in tutto il mondo. Il PRNG in questione è Yasmarang e, su Mk3, riduce l'entropia effettiva a circa 40 bit. Errore 1: disattivazione deliberata dell'RNG hardware La prima anomalia è nella configurazione di build: Coldcard disabilita esplicitamente il supporto all'RNG hardware di MicroPython in 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) A prima vista può sembrare innocuo: Coinkite usa ckcc.rng_bytes, che richiama direttamente il TRNG STM32 e offre più controllo rispetto al layer di porting MicroPython. Il punto critico emerge dall'espansione successiva: my_random_bytes() di libngu legge numeri casuali via la macro CHIP_TRNG_32(), che dopo l'espansione finisce per chiamare rng_get() del port STM32 di MicroPython, trattandolo come TRNG hardware. Quando MICROPY_HW_ENABLE_RNG è impostato a 0, rng_get() degrada in modo silenzioso, innescando il secondo errore. Errore 2: il fallback di rng_get() scambiato per TRNG hardware Partendo da libngu random.c e seguendo le macro, si arriva al ramo else in ports/stm32/rng.c del port STM32 di 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() è implementato in ports/stm32/rng.c. Se #if MICROPY_HW_ENABLE_RNG è nonzero, legge RNG>DR, cioè il TRNG hardware. Se vale zero, compila l'else e restituisce l'output di un PRNG software. La configurazione Coldcard è esattamente 0, quindi ogni Mk3 uscito di fabbrica segue questa strada. // 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 } La conseguenza è che, al primo rng_get(), il seed si basa su MP_HAL_UNIQUE_ID_ADDRESS, su SysTick>VAL e su registri RTC. L'UID aggiunge circa 14 bit; SysTick, con clock a 80 MHz e reload a 80.000, produce valori fra 0 e 79.999, circa 17 bit. RTC_TR e RTC_SSR sono letti subito dopo il power-on, prima dell'inizializzazione applicativa: dimensioni con spazio di valori molto ridotto. rtc_tr è RTC>TR in formato BCD packed, rtc_ssr arriva al massimo a MK3_RTC_SSR_MAX (0xFF). In tutti i vettori on-chain verificati, entrambi risultano pari a 0. Sommando: UID ~14 bit, SysTick ~17 bit, i due RTC ~0 bit, pressioni dei pulsanti ~5 bit. L'entropia reale della generazione seed si colloca a 36–37 bit. La stima Coinkite di "circa 40 bit" coincide con lo spazio di ricerca ricostruito via reverse engineering, uno spazio attaccabile in pochi giorni con un cluster GPU. Processo d'attacco Al primo avvio di Mk3, dal codice alla chiave privata, i numeri casuali vengono consumati secondo tre profili tipici. Il compito dell'attaccante è modellare ciascun profilo e ricostruire con precisione ogni passo del PRNG. Fase 1: stato iniziale All'accensione avvengono subito due inizializzazioni: Power on └── libngu static Yasmarang (ngu/random.c) pad=0x0a8ce26f n=69 d=233 dat=0 ← costante globale └── rng_get() prima chiamata, seeding (ports/stm32/rng.c) pad=UID^SysTick n=RTC_TR=0 d=RTC_SSR=0 dat=0 La casualità si riduce a un pad a 32 bit più alcune dimensioni piccole enumerabili. Il pad globale Mk3 finisce in un insieme limitato e quindi esauribile. Fase 2: operazioni sui tasti Durante la prima configurazione l'utente deve impostare un PIN, accettare i termini premendo OK e navigare i menu. Ogni pressione tasto attiva _start_scan() in shared/mempad.py, che chiama _rand_below() tre volte via shuffle(self.scan_order). scan_order ha lunghezza NUM_ROWS=4. shuffle è in shared/random.py; randbelow è collegato a ngu.random.uniform (cioè _rand_below in C dentro libngu random.c). # shared/random.py import ngu randbelow = ngu.random.uniform bytes = ngu.random.bytes def shuffle(lst): # Fisher-Yates, come CPython random.py for i in reversed(range(1, len(lst))): j = randbelow(i + 1) # consuma 1 step chip + 1 step mixer (con eventuali retry) lst[i], lst[j] = lst[j], lst[i] # shared/mempad.py # Ogni pressione tasto > _start_scan() > shuffle(self.scan_order) # lunghezza scan_order = NUM_ROWS = 4 Questi pattern di consumo devono essere modellati separatamente. Dall'analisi del sorgente v4.1.9 emergono tre profili. Profilo A: prima configurazione retail Pressione due volte prima di accettare i termini; settings.save() cerca uno slot libero fra 32 slot escludendo my_pos, restano 31 slot, con 30 chiamate a _rand_below; poi inserimento PIN, navigazione menu, altre pressioni, infine random_bytes(32). kpad_a × shuffle(4) # pressioni prima di accept_terms (kpad_a = 2) shuffle(31) # settings.save(): 31 slot = 30 chiamate a _rand_below kpad_b × shuffle(4) # PIN + navigazione (kpad_b enumera [4, 34]) random_bytes(32) # raccolta entropia Profilo B: primo avvio dopo flash o wipe con NVRAM vuota nvstore esegue uno shuffle(32), poi consuma 3072 step in lockstep su 3 slot × 16 blocchi × 256 byte senza feedback, avanzando solo lo stato. Su boot con NVRAM vuota, dopo questi passaggi c'è un ulteriore shuffle(32), poi consumo tasti, infine my_random_bytes(32). nvstore shuffle(32) # 31 chiamate a _rand_below nvstore blanking 3×16×256B # 3072 consumi lockstep, output non rientra (solo avanzamento stato) emptynvramonboarding profile # su NVRAM vuota: uno shuffle(32) addizionale Key press consumption (kpad_b [4,34]) my_random_bytes(32) Profilo C: paper wallet Il paper wallet è una modalità di cold storage in cui la chiave privata viene stampata su carta e conservata offline, spesso per regali o custodia di lungo periodo. Nel menu Paper Wallets di Coldcard, dopo la creazione di un wallet, rientrare nel menu richiede 8–25 pressioni tasto; poi my_random_bytes(32) viene usato direttamente come chiave privata, bypassando del tutto la wordlist BIP39. Fase 3: costruzione del seed e derivazione dell'indirizzo Da random_bytes(32) fino all'indirizzo Bitcoin finale, Mk3 segue questa pipeline: raw_bytes = random_bytes(32) # concatena 4 byte da 8 iterazioni di (mpy_step ^ yas_step) entropy = ngu.hash.sha256s(raw_bytes) # singolo SHA256, non sha256d mnemonic = BIP39(entropy, wordlist=english) # mnemonica da 24 parole seed = PBKDF2HMACSHA512(mnemonic, "mnemonic", 2048) master = HMACSHA512("Bitcoin seed", seed) child = m/{44,49,84}'/0'/0'/0/0 # primo indirizzo di ricezione address = bech32(hash160(compressed_pubkey)) # BIP84 di default Ogni passaggio è deterministico e riproducibile, comune ai tre profili. Se l'attaccante indovina correttamente pad, può ricalcolare offline l'intera catena. Fase 4: bruteforce e "hit" su GPU L'approccio dell'attaccante si articola in quattro step: 1) Costruzione dei pad candidati: si combinano le coordinate wafer X e Y (0–72) con uno spazio UID di circa 5.300 valori e si fa il prodotto cartesiano con SysTick da 0 a 79.999, ottenendo 424 milioni di pad candidati. È un numero gestibile in pochi giorni su GPU. 2) Enumerazione delle pressioni tasto per ogni pad, con kpad_b fra 4 e 34. Poiché rtc_tr e rtc_ssr mostrano hit a zero, vengono posizionati nel loop più esterno e valutati come dimensioni interne. 3) Esecuzione dell'intera pipeline seed nel kernel GPU: SHA256, BIP39, PBKDF2 (2048 round), BIP32, Hash160, Bech32. Lo stream costante di libngu può essere precomputato offline; il kernel recupera le word per indice riducendo l'overhead di avanzare il PRNG costante per thread. 4) Matching tramite Bloom filter o array ordinato di valori hash160 con complessità O(log n), puntando all'insieme degli indirizzi P2WPKH singlesignature presenti in rete. Il costo della ricerca è misurabile. L'esplorazione dello spazio della Fase A su una singola GPU Apple M1 (72x72 UID, 80.000 varianti SysTick e 31 conteggi di pressioni tasto), per circa 14,8 miliardi di candidati, ha richiesto circa 8,6 giorni. Un cluster A100 di classe data center può comprimere la stessa ricerca a poche ore. Lo spazio ~72 bit su Mk4, Mk5 e Q è 2^32 volte più ampio di quello Mk3. L'origine della vulnerabilità resta la stessa e l'approccio di profiling non cambia; varia solo la scala: da una singola macchina a un cluster, con tempi che passano da giorni a settimane, un costo che alcuni attaccanti restano disposti a sostenere. È per questo che questi modelli compaiono con frequenza simile nelle liste delle vittime.