Kerentanan Hardware Wallet Coldcard Terungkap: 1.719 BTC Raib dalam Serangan 2026
Ringkasan Pasar AI
Cacat entropi firmware Coldcard yang dilaporkan memungkinkan pemulihan seed melalui brute force secara offline untuk perangkat dengan konfigurasi default, yang mengakibatkan pencurian yang telah terkonfirmasi setidaknya 1.719 BTC di ribuan alamat. Insiden ini signifikan karena membingkai ulang risiko "cold storage" seputar kualitas pembuatan kunci alih-alih kompromi jaringan, yang berpotensi menekan kepercayaan terhadap hardware wallet, mendorong pembaruan firmware mendesak, serta meningkatkan pergerakan dana yang didorong oleh keamanan dalam jangka pendek dan pengawasan di seluruh praktik kustodi BTC.
Level dampak
● Tinggi
Aset terdampak
BTC/USDT-1.73%
Wawasan AI · BTC/USDTWawasan AI
▼ Bearish
Trade sekarang
⚠️ Wawasan yang dihasilkan AI didasarkan pada konten berita dan disediakan untuk tujuan informasi saja. Wawasan ini bukan nasihat investasi dan tidak mencerminkan pandangan BingX. Investasi melibatkan risiko. Harap trade secara bertanggung jawab.
Pada 30 Juli 2026, serangkaian alamat di blockchain tercatat memindahkan dana secara beruntun. Dalam rentang 41 menit, 1.196 alamat single-signature terkuras hingga kosong, dengan sekitar 1.082 BTC hilang. Itu baru gelombang pertama. Hingga awal Agustus, kerugian yang telah terkonfirmasi mencapai setidaknya 1.719 BTC (sekitar US$111 juta), melibatkan lebih dari 5.200 alamat, dan terjadi dalam tiga hingga empat gelombang.
Yang membuat kasus ini membingungkan: banyak dana berasal dari dompet dingin yang tidak tersentuh selama berbulan-bulan bahkan bertahun-tahun. Masalahnya ada pada tingkat private key. Seluruh private key tersebut dibuat menggunakan hardware wallet Coldcard, dan pemiliknya tidak menambahkan entropi tambahan via lemparan dadu maupun mengaktifkan BIP39 passphrase. Dompet yang terdampak memakai konfigurasi default paling sederhana.
Dampak kerentanan berbeda menurut lini firmware. Pada Mk2 dan Mk3 dengan firmware 4.0.1 hingga 4.1.9, entropi efektif hanya sekitar 40 bit—yang paling parah. Pada Mk4, Mk5, dan Q dengan versi firmware tertentu, entropinya sekitar 72 bit, masih berada dalam jangkauan brute force offline.
Coinkite merespons cepat. Pada 30 Juli mereka menerbitkan peringatan, lalu sehari berikutnya merilis firmware perbaikan: Mk2 dan Mk3 ke versi 4.2.0 atau lebih baru, Mk4 dan Mk5 ke 5.6.0 atau lebih baru, serta Q ke 1.5.0Q atau lebih baru. Persediaan perangkat dengan versi rentan dimusnahkan dan pengiriman ditangguhkan.
Serangan ini tidak terkait peretasan jarak jauh atau kompromi rantai pasok. Penyerang tidak pernah menyentuh perangkat. Private key didapat lewat brute force offline dengan menguji setiap seed yang mungkin dalam ruang pencarian. Kerentanan ini sudah lama ada, baru tersorot setelah peneliti eksternal mengembangkan alat brute force. Muan kemudian mereproduksi seluruh rantai serangan. Ulasan ini menelusuri kode firmware (contoh: Mk3 firmware 4.1.9) untuk menunjukkan satu baris logika yang pada akhirnya memungkinkan pengurasan massal ribuan perangkat Coldcard.
Akar masalah kerentanan
Coldcard dirancang untuk menghasilkan seed memakai hardware TRNG (true random number generator) bawaan chip STM32L475. Dua kesalahan firmware membuat proses itu berbelok menjadi PRNG berbasis perangkat lunak dengan keadaan (state) yang nyaris seragam di seluruh perangkat di dunia. PRNG tersebut bernama Yasmarang dan menurunkan entropi efektif Mk3 menjadi sekitar 40 bit.
Kesalahan 1: RNG hardware dinonaktifkan secara aktif
Kekeliruan pertama ada di konfigurasi build: Coldcard mematikan dukungan hardware RNG milik MicroPython pada 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)
Sekilas ini tampak wajar: Coinkite punya ckcc.rng_bytes sendiri yang memanggil TRNG STM32 secara langsung. Masalah muncul dari ekspansi makro berikutnya: my_random_bytes() milik libngu membaca angka acak lewat makro CHIP_TRNG_32(), yang setelah diekspansi memanggil rng_get() dari lapisan port MicroPython STM32 dan memperlakukannya sebagai TRNG hardware. Padahal rng_get() akan turun kualitasnya diam-diam ketika MICROPY_HW_ENABLE_RNG diset 0—yang membawa ke kesalahan kedua.
Kesalahan 2: fallback rng_get() keliru dianggap sebagai TRNG hardware
Penelusuran pada random.c di libngu, mengikuti ekspansi makro, berujung pada cabang else di ports/stm32/rng.c (port STM32 milik 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() berada di ports/stm32/rng.c. Ia menggunakan #if MICROPY_HW_ENABLE_RNG: bila makro nonzero, ia membaca RNG>DR (TRNG hardware). Bila makro bernilai 0, kode yang dikompilasi adalah cabang else yang mengembalikan keluaran PRNG software. Karena konfigurasi Coldcard tepat bernilai 0, setiap Mk3 yang keluar pabrik mengikuti jalur ini.
// 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
Sumber entropinya kecil dan dapat ditebak. UID memberikan sekitar 14 bit. SysTick>VAL saat rng_get() pertama kali dipanggil berada pada rentang 0 hingga 79.999 karena chip berjalan di 80 MHz dengan reload 80.000—setara sekitar 17 bit entropi. RTC_TR dan RTC_SSR diambil segera setelah power-on sebelum aplikasi menginisialisasinya; ruang nilainya sangat kecil. Dalam semua vektor on-chain yang sudah diverifikasi, kedua nilai tersebut adalah 0. Ditambah kontribusi tombol sekitar 5 bit, total entropi nyata untuk pembangkitan seed berada di kisaran 36–37 bit. Klaim Coinkite "sekitar 40 bit" sejalan dengan ruang pencarian yang terlihat dari reverse engineering kode—ruang yang bisa di-brute force oleh klaster GPU dalam beberapa hari.
Bagaimana serangan dijalankan
Pada boot pertama Mk3, konsumsi angka acak mengikuti tiga profil konsumsi yang khas. Tugas utama penyerang adalah memodelkan masing-masing profil tersebut dan merekonstruksi setiap langkah PRNG dengan presisi.
Fase 1: kondisi awal
Segera setelah perangkat menyala, dua hal terjadi.
Power on
└── libngu static Yasmarang (ngu/random.c)
pad=0x0a8ce26f n=69 d=233 dat=0 ← konstanta global
└── rng_get() initial call seeding (ports/stm32/rng.c)
pad=UID^SysTick n=RTC_TR=0 d=RTC_SSR=0 dat=0
Pada titik ini, seluruh "keacakan" dipadatkan menjadi pad 32-bit, plus dua dimensi kecil yang dapat dienumerasi. Ruang pad global Mk3 menjadi sempit dan bisa dihabiskan.
Fase 2: operasi tombol
Saat setup awal, pengguna harus membuat PIN, menerima ketentuan dengan menekan OK, dan menavigasi menu. Tiap penekanan tombol memicu _start_scan() di shared/mempad.py yang memanggil _rand_below() tiga kali melalui shuffle(self.scan_order). Panjang scan_order sama dengan NUM_ROWS (4). shuffle didefinisikan di shared/random.py; randbelow terikat langsung ke ngu.random.uniform (level C) yang setara dengan _rand_below di libngu.
# shared/random.py
import ngu
randbelow = ngu.random.uniform # setara _rand_below di libngu random.c
bytes = ngu.random.bytes # setara my_random_bytes
def shuffle(lst): # Fisher-Yates, sama seperti CPython random.py
for i in reversed(range(1, len(lst))):
j = randbelow(i + 1) # ← mengonsumsi 1 langkah chip + 1 langkah mixer (termasuk retry rejection sampling)
lst[i], lst[j] = lst[j], lst[i]
# shared/mempad.py
# tiap key press > _start_scan() > shuffle(self.scan_order)
# scan_order length = NUM_ROWS = 4
Pola konsumsi tombol ini harus dimodelkan satu per satu. Dari analisis sumber firmware v4.1.9, ditemukan tiga pola pemakaian.
Profil A: setup ritel pertama kali
Pengguna menekan dua kali sebelum menerima ketentuan; settings.save() mencari slot kosong dari 32 slot (kecuali my_pos) sehingga tersisa 31 slot—memicu 30 kali pemanggilan _rand_below. Lalu pengguna memasukkan PIN, bernavigasi di menu, menekan beberapa kali lagi, kemudian sistem mengambil entropi melalui random_bytes(32).
kpad_a × shuffle(4) # sebelum accept_terms (kpad_a = 2)
shuffle(31) # settings.save(): 31 slot → 30 panggilan _rand_below
kpad_b × shuffle(4) # input PIN + navigasi menu (kpad_b dienumerasi [4, 34])
random_bytes(32) # kumpulkan entropi
Profil B: boot pertama setelah flash/wipe, NVRAM kosong
nvstore menjalankan shuffle(32) sekali, lalu mengonsumsi 3.072 langkah sinkron pada 3 slot × 16 blok × 256 byte tanpa umpan balik (hanya memajukan state). Untuk boot dengan NVRAM kosong, ada shuffle(32) tambahan setelah dua langkah tersebut, diikuti konsumsi tombol, lalu my_random_bytes(32).
nvstore shuffle(32) # 31 panggilan _rand_below
nvstore blanking 3×16×256B # 3.072 konsumsi lockstep; output tidak masuk kembali
emptynvramonboarding profile
shuffle(32) tambahan
konsumsi tombol (kpad_b enum [4, 34])
my_random_bytes(32)
Profil C: paper wallet
Paper wallet adalah metode penyimpanan dingin dengan mencetak private key di kertas dan disimpan offline. Menu Paper Wallets pada Coldcard dibuat untuk menghasilkan halaman dompet yang siap cetak. Konsumsi entropinya paling langsung: setelah dompet dibuat, masuk kembali ke menu memerlukan 8–25 kali penekanan tombol, lalu my_random_bytes(32) dipakai langsung sebagai private key, melewati daftar kata BIP39 sepenuhnya.
Fase 3: konstruksi seed dan derivasi alamat
Dari random_bytes(32) hingga alamat Bitcoin akhir, Mk3 mengikuti rantai deterministik berikut.
raw_bytes = random_bytes(32) # gabungkan 4 byte dari 8 iterasi (mpy_step ^ yas_step)
entropy = ngu.hash.sha256s(raw_bytes) # SHA256 tunggal, bukan sha256d
mnemonic = BIP39(entropy, wordlist=english) # mnemonic 24 kata
seed = PBKDF2HMACSHA512(mnemonic, "mnemonic", 2048)
master = HMACSHA512("Bitcoin seed", seed)
child = m/{44,49,84}'/0'/0'/0/0 # alamat penerimaan pertama
address = bech32(hash160(compressed_pubkey)) # default BIP84
Semua tahap tersebut dapat direproduksi. Begitu penyerang menebak pad dengan benar, seluruh rantai bisa dihitung ulang secara offline.
Fase 4: brute force dan "hit" di GPU
Pendekatan penyerang pada praktiknya terdiri dari empat langkah.
1) Menyusun himpunan kandidat pad.
Koordinat wafer X dan Y dari 0 hingga 72 dikombinasikan dengan ruang UID sekitar 5.300 nilai, lalu dikalikan silang dengan nilai SysTick 0–79.999. Hasilnya 424 juta kandidat pad. Angka ini besar, tetapi dapat diselesaikan dalam beberapa hari di GPU.
2) Mengenumerasi jumlah penekanan tombol.
Untuk tiap pad, kpad_b berkisar 4 hingga 34. Karena rtc_tr dan rtc_ssr teramati 0 pada seluruh hit yang diverifikasi, keduanya ditempatkan sebagai loop luar dan loop dalam.
3) Menjalankan seluruh pipeline seed di kernel GPU.
Tahap SHA256, BIP39, PBKDF2 2.048 putaran, BIP32, Hash160, dan Bech32 dieksekusi di GPU. Aliran konstanta dari libngu dapat dipra-hitungan offline; kernel mengambil word berdasarkan indeks untuk mengurangi overhead tiap thread.
4) Mencocokkan hasil.
Pencocokan dilakukan memakai Bloom filter atau array terurut dari nilai hash160 dengan kompleksitas O(log n), menargetkan kumpulan alamat single-signature P2WPKH di jaringan.
Biaya pencarian juga dapat diukur. Menjalankan ruang Profil A pada satu GPU Apple M1—dengan UID 72×72, 80.000 variasi SysTick, dan 31 kemungkinan jumlah key press—menghasilkan sekitar 14,8 miliar kandidat dan memakan waktu sekitar 8,6 hari. Klaster A100 kelas data center dapat menekan pekerjaan yang sama menjadi hanya beberapa jam.
Ruang ~72 bit pada Mk4, Mk5, dan Q besarnya 2^32 kali lipat dibanding Mk3. Kerentanannya berakar dari sumber yang sama dan pendekatan pemodelannya serupa; perbedaannya, brute force bergeser dari satu mesin ke klaster, memperpanjang waktu dari hitungan hari menjadi minggu—biaya yang tetap dinilai layak oleh penyerang. Itu sebabnya perangkat-perangkat tersebut juga muncul di daftar korban.