Втрати понад 88 мільйонів доларів США: Аналіз вразливості апаратного гаманця Coldcard та відстеження вкрадених коштів 31 липня було вкрадено 594 біткойни з приблизно 500 апаратних гаманців Coldcard на загальну суму близько 38 мільйонів доларів США. Пізніше компанія Coinkite, яка виробляє гаманці Coldcard, підтвердила наявність безпекової вразливості в процесі генерації ключів, яка стосується кількох поколінь продуктів: Coldcard Mk2, Mk3, Mk4, Q та Mk5.
На даний момент збитки від атаки на цю вразливість перевищили 88 мільйонів доларів США, а атака продовжується. Користувачам Coldcard слід якнайшвидше перевести кошти на інші адреси. Нижче наведено аналіз Beosin щодо цієї вразливості та інформацію про відстеження вкрадених коштів.
I. Аналіз вразливості
Аналіз записів комітів коду прошивки Coldcard показує, що в попередньому коміті 37e4af5451c260c1e7d429fe8972c4cb5e68ee59 команда розробників оновила кілька частин коду, пов’язаних із конфігурацією MK4, зокрема у файлі mpconfigboard.h:
// У нас є власна версія цього коду. #define MICROPY_HW_ENABLE_RNG (0)
На стороні MicroPython STM32 цей макрос керує компіляційними шляхами для за замовчуванням апаратного генератора випадкових чисел (RNG) та універсальної реалізації випадкових чисел. Встановлення його значення на 0 призводить до того, що шлях за замовчуванням апаратного RNG не використовуватиметься як бекенд для rng_get().
Його коментарі вказують, що розробник самостійно реалізує RNG; перевірка його власного rng.h показує, що оголошено лише два об’єкти MicroPython:
MP_DECLARE_CONST_FUN_OBJ_0(pyb_rng_get_obj); MP_DECLARE_CONST_FUN_OBJ_1(pyb_rng_get_bytes_obj);
Відповідна реалізація:
/// \function pyb_rng_get()///// Повертає 30-бітне випадкове число, згенероване апаратно: або помилка!//STATIC mp_obj_t pyb_rng_get(void){ // Отримати та повернути нове випадкове число return mp_obj_new_int(rng_get_or_fault() >> 2);}
/// \function rng_get_bytes()/// Заповнити буфер випадковими бітами; викликаюча сторона повинна надати буфер відповідного розміру.STATIC mp_obj_t pyb_rng_get_bytes(mp_obj_t buffer_io) {
mp_buffer_info_t bufinfo; mp_get_buffer_raise(buffer_io, &bufinfo, MP_BUFFER_WRITE);
mp_uint_t count = bufinfo.len; if(count < 1) { mp_raise_ValueError(NULL); }
// Прочитати 32-бітні слова та розпакувати їх у наданий буфер random_buffer(bufinfo.buf, count);
return mp_const_none;}
MP_DEFINE_CONST_FUN_OBJ_0(pyb_rng_get_obj, pyb_rng_get);MP_DEFINE_CONST_FUN_OBJ_1(pyb_rng_get_bytes_obj, pyb_rng_get_bytes);
rng_get_or_fault() читає вбудований генератор випадкових чисел STM32:
static uint32_t rng_get_or_fault(void){ // Увімкнути периферію RNG, якщо вона ще не увімкнена rng_init();
// Очікувати, поки нове випадкове число буде готове, що займає приблизно 10 мкс uint32_t start = HAL_GetTick();
while (!(RNG->SR & RNG_SR_DRDY)) { if (HAL_GetTick() - start >= RNG_TIMEOUT_MS) { // помилка апаратного забезпечення... нічого не повертати! mp_raise_OSError(MP_EFAULT); } }
// Отримати та повернути нове випадкове число last_value = RNG->DR;
return last_value;}
Це означає, що власний код Coldcard дійсно бажає використовувати апаратний RNG, але він гарантує використання цієї апаратної функції лише під час виклику pyb_rng_get* або його внутрішньої random_buffer().
А при створенні гаманця фактично викликається наступний код, де функція створення гаманця в shared/seed.py має вигляд:
асинхронна функція make_new_wallet(nwords): # Вибрати новий випадковий посів. await ux_dramatic_pause('Генерація...', 3) seed = generate_seed() words = await approve_word_list(seed, nwords) якщо words: await commit_new_words(words)
Цей виклик спочатку потрапляє до модуля Coldcard shared/random.py. У попередніх версіях random.py явно залежав від ngu.random і зберігав:
# random.py - підмножина модуля random, без сумісності, з використанням криптографічно надійного ГВЧ
# для байтів використовуйте ngu.random.byte(len)
# bytes = ngu.random.bytes
Під час ініціалізації гаманця використовується random.bytes, а не pyb.rng(), і власна функція pyb_rng_get_obj не перевизначає random.bytes автоматично.
Оскільки MICROPY_HW_ENABLE_RNG встановлено на 0, під час генерації гаманця не використовувався апаратний RNG, а було викликано pyb_rng_yasmarang з micropython/ports/stm32/rng.c:
#if MICROPY_HW_ENABLE_RNG
uint32_t rng_get(void) { // використовуємо апаратний RNG STM32 ...}
#else
// Для МК без RNG нам все ще потрібно надати функцію rng_get()// Псевдовипадковий генератор не ідеальний, але поки що ми його використовуємо.
// Генератор випадкових чисел Yasmarangstatic uint32_t pyb_rng_yasmarang(void) { static bool seeded = false; static uint32_t pad = 0, n = 0; ...}
uint32_t rng_get(void) { return pyb_rng_yasmarang();}
#endif
pyb_rng_yasmarang є генератором псевдовипадкових чисел, і використання його для генерації посівів апаратного гаманця є дуже небезпечним — зловмисник може отримати ключі методом підбору. Зараз Coinkite явно виключив stm32/rng.c у файлі makefile:
# Не компілювати запасний PRNG MicroPython. Файл rng.c, специфічний для плати,
# надає rng_get(), і цей порожній об’єкт задовольняє список об’єктів з основного коду.
$(BUILD)/rng.o: CFLAGS += -Dpyb_rng_yasmarang=error-do-not-want-this
$(BUILD)/rng.o:
$(ECHO) "SKIP stm32/rng.c"
$(Q)$(CC) $(CFLAGS) -x c -c /dev/null -o $@
Друге: Відстеження вкрадених коштів
На даний момент кошти кількох жертв було переведено та зібрано на кількох адресах, ще не здійснено додаткової очистки. Beosin Trace за допомогою інформації про загрози та аналізу поведінки в ланцюжку виявив наступні адреси збирання:
bc1qq85v2c926eg6pgxhwp6q7lf6cnsz80qs3fcu9r (562 BTC)
bc1qx76cae2706qd5q576feh7xq8rfcsjpf2htfhe3 (398,47 BTC)
Крім того, існують наступні адреси збирання, графік руху коштів яких схожий на попередній, і на даний момент подальших переказів не зафіксовано:
- bc1q8jy96fe5lf8vfugydnte3cguk92gpev7kwtp3q (89,62 BTC)
- bc1q0rvn88w08j75k4h48lf9fvhan7unjp7vjf5q6m (64,9 BTC)
- bc1qtfrwa4j6rmj9rsgspv6a0yjumkg39js2numu75 (45,9 BTC)
- bc1qmd5m5ktv7m5ffujxv4248fxv36myvdx79n8jp6 (30,18 BTC)
Атаки на гаманець Coldcard продовжуються, команда Beosin продовжує моніторити додаткові адреси збору та аналізувати відповідні потоки коштів.
Три. Висновок
Ця серйозна інцидент безпеки з Coldcard апаратним гаманцем виникла через помилку в реалізації генерації випадкових чисел розробницькою командою: у критичному етапі створення посівної фрази гаманця було використано псевдовипадкове генерування. Розробникам слід проводити постійне, всебічне тестування та аудит коду, а користувачам Coldcard слід якомога швидше перевести свої активи та уважно стежити за подальшими безпековими оголошеннями Coinkite.
Beosin — це лідируюча компанія у сфері безпеки блокчейну та відповідності регуляторним вимогам, яка спеціалізується на аудиті безпеки смарт-контрактів до запуску проекту, моніторингу та блокування ризиків безпеки під час роботи проекту, відновленні вкрадених коштів, протидії відмиванню коштів (AML) та слідчим діям щодо віртуальних активів. Beosin надає «однохвилинні» рішення для відповідності блокчейну та послуги безпеки для 200+ постачальників віртуальних активів, 4500+ Web3-проектів та регуляторних та правоохоронних органів у понад 20 країнах та регіонах світу.

