Vulnerabilitate la Coldcard: atac în 2026 cu pierderi confirmate de 1.719 BTC
Rezumat al pieței generat de AI
Un defect de entropie raportat în firmware-ul Coldcard a permis recuperarea offline prin forță brută a seed-urilor pentru dispozitivele cu configurația implicită, ducând la furt confirmat de cel puțin 1.719 BTC din mii de adrese. Incidentul este semnificativ deoarece reîncadrează riscul "cold storage" în jurul calității generării cheilor, mai degrabă decât al compromiterii rețelei, putând pune presiune asupra încrederii în portofelele hardware, determinând actualizări urgente de firmware și crescând, pe termen scurt, mișcările de fonduri determinate de securitate și nivelul de scrutin asupra practicilor de custodie BTC.
Nivelul impactului
● Ridicat
Active afectate
BTC/USDT-1.20%
Perspectivă AI · BTC/USDTPerspectivă AI
▼ Bearish
Tranzacționează acum
⚠️ Perspectivele generate de AI se bazează pe conținutul știrilor și sunt furnizate exclusiv în scop informativ. Nu constituie consiliere de investiții și nu reprezintă opiniile BingX. Investițiile implică risc. Tranzacționează responsabil.
Autori: Johan & Lisa | Edit: 77
Pe 30 iulie 2026, o serie de adrese de pe blockchain a început să-și transfere fondurile în mod secvențial. În doar 41 de minute au fost golite 1.196 de adrese singlesignature, iar aproximativ 1.082 BTC au dispărut. Acesta a fost doar primul val. Până la începutul lui august, pierderile confirmate ajunseseră la cel puțin 1.719 BTC (circa 111 milioane de dolari), atacul vizând peste 5.200 de adrese și desfășurându-se în trei sau patru valuri.
Elementul care a derutat inițial ancheta a fost "starea" portofelelor: majoritatea erau cold wallets, neatinse de luni sau chiar ani. Problema nu a fost una de acces la dispozitiv, ci de chei private. Toate cheile compromise au fost generate cu portofele hardware Coldcard, în configurația implicită: fără entropie suplimentară prin zaruri și fără BIP39 passphrase.
Impactul diferă în funcție de linia de firmware. Pentru Mk2 și Mk3, pe firmware 4.0.1–4.1.9, entropia efectivă este de aproximativ 40 de biți, scenariul cel mai grav. Pentru Mk4, Mk5 și Q, pe anumite versiuni, entropia este în jur de 72 de biți, suficient de mică pentru atacuri offline de tip brute force.
Coinkite a reacționat rapid. Pe 30 iulie a publicat un avertisment, iar a doua zi a lansat firmware-uri corective: Mk2 și Mk3 trebuie actualizate la 4.2.0 sau mai nou, Mk4 și Mk5 la 5.6.0 sau mai nou, iar Q la 1.5.0Q sau mai nou. Compania a anunțat și distrugerea stocurilor afectate și suspendarea livrărilor.
Atacul nu are legătură cu intruziuni la distanță sau cu compromiterea lanțului de aprovizionare. Agresorul nu a pus mâna pe niciun dispozitiv; a obținut cheile private prin brute force offline, testând sistematic fiecare seed posibil din spațiul de căutare. Vulnerabilitatea a existat ani la rând, până când cercetători externi au dezvoltat un instrument de brute force care a scos problema la lumină. Muan a reprodus ulterior întregul lanț al atacului.
Analiza pornește din codul de firmware (exemplu: Mk3, versiunea 4.1.9) și identifică linia de cod care a permis drenarea în masă a mii de dispozitive Coldcard.
Cauza rădăcină
Coldcard este proiectat să genereze seed-uri folosind TRNG-ul (true random number generator) hardware al cipului STM32L475. Două erori de firmware au combinat însă efectele astfel încât seed-ul a ajuns să depindă de un PRNG software cu o stare aproape identică între dispozitive la nivel global. PRNG-ul folosit este Yasmarang, ceea ce reduce entropia efectivă pe Mk3 la circa 40 de biți.
Eroarea 1: RNG-ul hardware a fost dezactivat explicit
În configurația de build, Coldcard dezactivează suportul RNG hardware din MicroPython în 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)
La prima vedere pare benign: Coinkite are ckcc.rng_bytes, care apelează direct TRNG-ul STM32 și oferă mai mult control decât implementarea din layer-ul MicroPython. Capcana apare mai jos, după expandarea macro-urilor: my_random_bytes() din libngu citește aleatoriu prin CHIP_TRNG_32(), care, după expandare, cheamă rng_get() din portarea STM32 a MicroPython, tratând-o ca pe un TRNG hardware. Când MICROPY_HW_ENABLE_RNG este 0, rng_get() degradează silențios către PRNG software, ceea ce duce la a doua eroare.
Eroarea 2: fallback-ul din rng_get() a fost tratat ca TRNG hardware
Urmărind fluxul în libngu random.c și expandările de macro-uri, se ajunge în ramura else din ports/stm32/rng.c (MicroPython STM32):
// 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) ...
chip ^= my_yasmarang();
...
}
rng_get() compilează două comportamente în funcție de MICROPY_HW_ENABLE_RNG. Dacă macro-ul e nenul, citește din RNG>DR (TRNG hardware). Dacă e 0, se compilează fallback-ul care returnează ieșirea PRNG-ului software. Configurația Coldcard este exact 0, astfel încât fiecare Mk3 livrat din fabrică urmează această ramură:
// external/micropython/ports/stm32/rng.c
#if MICROPY_HW_ENABLE_RNG
uint32_t rng_get(void) { ... read RNG>DR hardware TRNG ... }
#else
// 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 ...
}
#endif
Analiza surselor de entropie arată că pad este derivat din UID ^ SysTick>VAL la primul apel. Cipul rulează la 80 MHz cu reload 80.000, ceea ce produce valori între 0 și 79.999, aproximativ 17 biți de entropie. RTC_TR și RTC_SSR sunt citite imediat după pornire, înainte ca aplicația să le inițializeze; spațiul lor de valori este mic. În toate vectorii on-chain verificați, ambele au fost 0.
Rezultatul: UID contribuie cu aproximativ 14 biți, SysTick cu circa 17 biți, ambele câmpuri RTC cu 0 biți, iar apăsările de butoane adaugă aproximativ 5 biți. Entropia reală pentru generarea seed-ului ajunge la 36–37 biți. Declarația Coinkite de "aproximativ 40 de biți" se potrivește cu spațiul de căutare rezultat din reverse engineering, un spațiu ce poate fi epuizat cu un cluster GPU în câteva zile.
Cum s-a desfășurat atacul
La prima pornire a Mk3, de la cod la cheia privată, numerele "aleatoare" sunt consumate după trei profile tipice. Sarcina atacatorului a fost să modeleze aceste profile și să reconstruiască exact fiecare pas al PRNG-ului.
Faza 1: starea inițială
Imediat după alimentare se întâmplă două lucruri:
Power on
└── libngu static Yasmarang (ngu/random.c) pad=0x0a8ce26f n=69 d=233 dat=0 ← constantă globală
└── rng_get() initial call seeding (ports/stm32/rng.c) pad=UID^SysTick n=RTC_TR=0 d=RTC_SSR=0 dat=0
În acest punct, "aleatoriul" este comprimat într-un pad pe 32 de biți, plus două dimensiuni mici care pot fi enumerate. Spațiul pad-urilor posibile pe Mk3 devine practic finit și abordabil.
Faza 2: apăsările de butoane
La configurarea inițială, utilizatorul trebuie să seteze un PIN, să confirme termenii prin OK și să navigheze meniul. Fiecare apăsare declanșează _start_scan() în shared/mempad.py, care cheamă _rand_below() de trei ori prin shuffle(self.scan_order). scan_order are lungimea NUM_ROWS = 4. shuffle este în shared/random.py, iar randbelow este legat direct de ngu.random.uniform din libngu (implementare C a _rand_below):
# shared/random.py
import ngu
randbelow = ngu.random.uniform
bytes = ngu.random.bytes
def shuffle(lst):
for i in reversed(range(1, len(lst))):
j = randbelow(i + 1)
lst[i], lst[j] = lst[j], lst[i]
# shared/mempad.py
# fiecare apăsare > _start_scan() > shuffle(self.scan_order)
# scan_order length = 4
După analiza sursei v4.1.9, au fost confirmate trei tipare.
Profilul A (setup retail): utilizatorul apasă de două ori înainte de acceptarea termenilor; settings.save() caută un slot liber dintre 32 (excluzând my_pos), rămân 31 sloturi, ceea ce duce la 30 apeluri _rand_below; apoi urmează PIN-ul, navigare suplimentară și, la final, random_bytes(32).
kpad_a × shuffle(4) # înainte de accept_terms (kpad_a = 2)
shuffle(31) # settings.save(): 30 apeluri _rand_below
kpad_b × shuffle(4) # PIN + meniu (kpad_b enumerat în [4, 34])
random_bytes(32) # colectare entropie
Profilul B (prima pornire după flash/wipe cu NVRAM gol): nvstore face întâi un shuffle(32), apoi consumă 3.072 pași în sincron (3 sloturi × 16 blocuri × 256 bytes), fără feedback, doar avansând starea. Pentru un boot cu NVRAM gol, se mai face un shuffle(32), apoi consumul de taste și, la final, my_random_bytes(32).
nvstore shuffle(32) # 31 apeluri _rand_below
nvstore blanking 3×16×256B # 3.072 consumări, doar avans de stare
emptynvram onboarding: shuffle(32)
Key press consumption (kpad_b în [4, 34])
my_random_bytes(32)
Profilul C (paper wallet): după ce există deja un wallet, reintrarea în meniu implică 8–25 apăsări, după care my_random_bytes(32) este folosit direct ca cheie privată, fără a trece prin lista de cuvinte BIP39.
Faza 3: construcția seed-ului și derivarea adresei
De la random_bytes(32) la adresa finală Bitcoin, Mk3 urmează acest lanț determinist:
raw_bytes = random_bytes(32)
entropy = ngu.hash.sha256s(raw_bytes) # SHA256 simplu
mnemonic = BIP39(entropy, wordlist=english) # 24 cuvinte
seed = PBKDF2HMACSHA512(mnemonic, "mnemonic", 2048)
master = HMACSHA512("Bitcoin seed", seed)
child = m/{44,49,84}'/0'/0'/0/0
address = bech32(hash160(compressed_pubkey)) # implicit BIP84
Fiind funcții complet reproductibile, odată ce pad-ul este ghicit corect, atacatorul poate recalcula offline întregul lanț.
Faza 4: brute force pe GPU și identificarea adreselor
Metoda atacatorului se rezumă la patru pași:
1) Construirea setului de pad-uri candidate: se iau coordonatele de wafer X și Y între 0 și 72, combinate cu un spațiu UID de aproximativ 5.300 valori, apoi produs cartezian cu SysTick între 0 și 79.999, rezultând circa 424 milioane pad-uri candidate. Volumul este mare, dar abordabil în câteva zile pe GPU.
2) Enumerarea numărului de apăsări pentru fiecare pad, cu kpad_b în intervalul 4–34. Cum rtc_tr și rtc_ssr au avut hit-uri zero, pot fi puse în bucle externe, cu cost minim.
3) Rularea întregului pipeline în kernel-ul GPU: SHA256, BIP39, PBKDF2 (2.048 runde), BIP32, Hash160 și Bech32. Fluxul constant din libngu poate fi precomputat offline; kernel-ul indexează cuvintele, evitând avansarea independentă a PRNG-ului în fiecare thread.
4) Potrivirea cu un Bloom filter sau un tablou sortat de valori hash160 (complexitate O(log n)), țintind setul de adrese singlesignature P2WPKH din rețea.
Costul poate fi estimat. Rularea spațiului din Faza A pe un singur GPU Apple M1—cu un UID 72×72, 80.000 variații SysTick și 31 valori pentru numărul de apăsări—în total circa 14,8 miliarde de candidați, a durat aproximativ 8,6 zile. Un cluster A100 la nivel de data center poate reduce aceeași căutare la câteva ore.
Spațiul de ~72 biți pentru Mk4, Mk5 și Q este de 2^32 ori mai mare decât cel de Mk3, însă vulnerabilitățile au aceeași origine, iar abordarea de modelare a profilelor rămâne identică. Diferența este doar de resurse: de la o singură mașină la un cluster, ceea ce extinde durata de la zile la săptămâni—un cost pe care atacatorii par dispuși să îl suporte, motiv pentru care aceste modele apar și ele pe listele de victime.