Потери превысили 88 миллионов долларов США: анализ уязвимости аппаратного кошелька Coldcard и отслеживание похищенных средств 31 июля около 500 аппаратных кошельков Coldcard были взломаны, и было похищено 594 биткоина на сумму около 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 RNG:
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)
Этот вызов сначала попадает в модуль shared/random.py Coldcard. В предыдущих версиях 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) { // используем аппаратный генератор случайных чисел 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:
# Не компилировать резервный ГСЧ 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 предоставляет «все-in-one» решения для соблюдения нормативных требований и безопасности блокчейна более чем 200 сервисам виртуальных активов, 4500 проектам Web3 и регуляторным и правоохранительным органам более чем в 20 странах и регионах мира.

