Уязвимость аппаратных кошельков Coldcard: в атаке 2026 года похищено 1 719 BTC

Сводка рынка от ИИ
Сообщалось, что уязвимость энтропии в прошивке Coldcard позволяла офлайн перебором восстановить сид-фразы на устройствах с настройками по умолчанию, что привело к подтверждённой краже как минимум 1 719 BTC по тысячам адресов. Инцидент значим тем, что переосмысливает риск "холодного хранения" вокруг качества генерации ключей, а не компрометации сети, потенциально оказывая давление на доверие к аппаратным кошелькам, побуждая к срочным обновлениям прошивки и усиливая в ближайшей перспективе обусловленные безопасностью перемещения средств и контроль за практиками хранения BTC.
Степень влияния
● Высокая
Затронутые активы
BTC/USDT-1.73%
Инсайт ИИ · BTC/USDTИнсайт ИИ
▼ Медвежий
Торговать
⚠️ Инсайты, сгенерированные ИИ, основаны на новостном контенте и предоставляются исключительно в информационных целях. Они не являются инвестиционной рекомендацией и не отражают позицию BingX. Торговля сопряжена с риском. Пожалуйста, торгуйте ответственно.
Авторы: Johan и Lisa. Редактура: 77. 30 июля 2026 года в сети Bitcoin произошла серия последовательных транзакций: за 41 минуту злоумышленник опустошил 1 196 одноподписных адресов, что привело к исчезновению примерно 1 082 BTC. Это была лишь первая волна. К началу августа подтверждённые потери достигли как минимум 1 719 BTC (около $111 млн), затронув более 5 200 адресов; атака проходила в три-четыре волны. Наиболее необычным оказался профиль пострадавших кошельков: многие из них годами не двигали средства и хранили их офлайн, а проблема лежала на уровне приватных ключей. Все скомпрометированные ключи были сгенерированы аппаратными кошельками Coldcard, при этом владельцы не добавляли дополнительную энтропию через броски кубика и не включали BIP39 passphrase. Во всех случаях использовалась самая простая конфигурация по умолчанию. Масштаб риска зависит от линейки и прошивки. Для Mk2 и Mk3 с версиями 4.0.1–4.1.9 эффективная энтропия оценивается примерно в 40 бит — это самый тяжёлый сценарий. Для Mk4, Mk5 и Q на отдельных версиях прошивок энтропия около 72 бит, что всё ещё укладывается в рамки офлайн-брутфорса. Производитель Coinkite отреагировал быстро: 30 июля выпустил предупреждение, а на следующий день — исправленные прошивки. Рекомендованные версии: для Mk2 и Mk3 — 4.2.0 и выше, для Mk4 и Mk5 — 5.6.0 и выше, для Q — 1.5.0Q и выше. Компания также уничтожила остатки уязвимых партий и приостановила отгрузки. Случившееся не связано ни с удалённым взломом, ни с компрометацией цепочки поставок: злоумышленник не получал доступ к устройствам физически. Ключи были получены офлайн путём перебора — систематическим тестированием возможных сидов в пространстве поиска. Уязвимость годами оставалась незаметной, пока внешние исследователи не разработали инструмент для брутфорса. Позднее Muan воспроизвёл всю цепочку атаки. Разбор начинается с кода прошивки (на примере Mk3 v4.1.9) и показывает, как одна строка конфигурации позволила массово "осушить" тысячи устройств. Причина уязвимости Coldcard должен генерировать сиды с использованием встроенного аппаратного TRNG (true random number generator) чипа STM32L475. Но сочетание двух ошибок в прошивке фактически превратило генерацию в программный PRNG с почти одинаковым состоянием на устройствах по всему миру. Этот PRNG называется Yasmarang; для Mk3 он снижает эффективную энтропию примерно до 40 бит. Ошибка 1: аппаратный RNG был отключён В конфигурации сборки Coldcard явно отключает поддержку аппаратного RNG в MicroPython через 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) На первый взгляд это выглядит безобидно: у Coinkite есть собственный ckcc.rng_bytes, который напрямую обращается к TRNG STM32 и даёт больше контроля, чем реализация на уровне порта MicroPython. Проблема скрыта ниже по цепочке: libngu в my_random_bytes() получает случайные данные через макрос CHIP_TRNG_32(), который после развёртки вызывает rng_get() из STM32-порта MicroPython, считая его аппаратным TRNG. При MICROPY_HW_ENABLE_RNG = 0 функция rng_get() незаметно переключается на программный фолбэк, что приводит ко второй ошибке. Ошибка 2: программный фолбэк rng_get() приняли за аппаратный TRNG При разборе libngu random.c и последовательном раскрытии макросов исследователи выходят на ветку else в 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(); // XORs with a global constant Yasmarang stream (pad=0x0a8ce26f) ... } Сам rng_get() устроен так: если MICROPY_HW_ENABLE_RNG ненулевой — он читает RNG>DR (аппаратный TRNG). Если равен нулю — компилируется ветка else и возвращается вывод программного PRNG. В конфигурации Coldcard значение как раз 0, поэтому каждый Mk3 "из коробки" идёт по уязвимому пути. // 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 Семя для Yasmarang формируется из UID микроконтроллера и SysTick>VAL при первом вызове rng_get(). Чип работает на 80 MHz при значении перезагрузки 80 000, что даёт диапазон 0–79 999, то есть около 17 бит энтропии. RTC_TR и RTC_SSR — значения RTC-регистров сразу после включения питания, до инициализации приложением; их пространство значений небольшое. RTC>TR хранится в packed-BCD, а у rtc_ssr максимум MK3_RTC_SSR_MAX = 0xFF. По проверенным on-chain "попаданиям" оба значения были равны 0. В сумме вклад UID оценивается примерно в 14 бит, SysTick — около 17 бит, оба RTC-регистра — 0 бит, нажатия кнопок добавляют порядка 5 бит. Истинный источник энтропии для генерации сида даёт примерно 36–37 бит. Заявление Coinkite о "примерно 40 битах" совпадает с оценкой пространства поиска, полученной при реверс-инжиниринге: такой объём перебирается GPU-кластером за несколько дней. Как проводилась атака При первом запуске Mk3 случайные данные расходуются по трём типовым профилям потребления. Задача атакующего — смоделировать каждый профиль и точно воспроизвести все шаги PRNG. Фаза 1: начальное состояние Сразу после включения питания происходят два события: Power on └── libngu static Yasmarang (ngu/random.c) pad=0x0a8ce26f n=69 d=233 dat=0 ← глобальная константа └── rng_get() initial call seeding (ports/stm32/rng.c) pad=UID^SysTick n=RTC_TR=0 d=RTC_SSR=0 dat=0 На этом этапе вся "случайность" сжата до 32-битного pad плюс двух малых измерений, которые можно перечислить перебором. Глобальный pad Mk3 оказывается заперт в небольшом, исчерпаемом диапазоне. Фаза 2: нажатия кнопок При первичной настройке пользователь обязан задать PIN, подтвердить условия кнопкой OK и перемещаться по меню. Каждое нажатие вызывает _start_scan() в shared/mempad.py, откуда через shuffle(self.scan_order) трижды вызывается _rand_below(). Длина scan_order равна NUM_ROWS, то есть 4. shuffle определён в shared/random.py, а randbelow напрямую связан с ngu.random.uniform — C-реализацией _rand_below. # shared/random.py import ngu randbelow = ngu.random.uniform bytes = ngu.random.bytes def shuffle(lst): # Fisher-Yates for i in reversed(range(1, len(lst))): j = randbelow(i + 1) lst[i], lst[j] = lst[j], lst[i] # shared/mempad.py # Each key press > _start_scan() > shuffle(self.scan_order) # scan_order length = NUM_ROWS = 4 После анализа исходников прошивки v4.1.9 выделены три профиля. Профиль A: стандартная первичная настройка. Пользователь дважды нажимает перед принятием условий; затем settings.save() ищет пустой слот среди 32, исключая my_pos, то есть остаётся 31 вариант и выполняется 30 вызовов _rand_below. Далее ввод PIN и навигация по меню (число нажатий kpad_b перебирается в диапазоне 4–34), после чего запрашивается random_bytes(32). kpad_a × shuffle(4) # нажатия до accept_terms (kpad_a = 2) shuffle(31) # settings.save(): 30 вызовов _rand_below kpad_b × shuffle(4) # ввод PIN + меню (kpad_b ∈ [4, 34]) random_bytes(32) # сбор энтропии Профиль B: первый запуск после прошивки или очистки при пустом NVRAM. nvstore делает один shuffle(32), затем потребляет 3 072 шага синхронно по схеме 3 слота × 16 блоков × 256 байт, не возвращая выход обратно (состояние лишь продвигается). При "пустом" запуске NVRAM добавляется ещё один shuffle(32), затем нажатия кнопок и финальный my_random_bytes(32). nvstore shuffle(32) # 31 вызов _rand_below nvstore blanking 3×16×256B # 3 072 шага потребления emptynvram onboarding: shuffle(32) Key press consumption (kpad_b ∈ [4, 34]) # каждый раз shuffle(4) my_random_bytes(32) Профиль C: paper wallet. В меню Paper Wallets Coldcard генерирует страницы для печати. После создания кошелька повторный вход в меню требует 8–25 нажатий, затем my_random_bytes(32) используется напрямую как приватный ключ, минуя BIP39-словарь. Фаза 3: построение сида и вывод адреса От random_bytes(32) до итогового Bitcoin-адреса Mk3 проходит детерминированный конвейер: raw_bytes = random_bytes(32) # 4 байта из 8 итераций (mpy_step ^ yas_step) entropy = ngu.hash.sha256s(raw_bytes) # один SHA256, не sha256d mnemonic = BIP39(entropy, wordlist=english) # 24 слова seed = PBKDF2HMACSHA512(mnemonic, "mnemonic", 2048) master = HMACSHA512("Bitcoin seed", seed) child = m/{44,49,84}'/0'/0'/0/0 address = bech32(hash160(compressed_pubkey)) # по умолчанию BIP84 Поскольку каждый шаг воспроизводим, достаточно угадать pad — и вся цепочка пересчитывается офлайн. Фаза 4: брутфорс на GPU и поиск совпадений Метод злоумышленника укладывается в четыре шага. 1) Сформировать набор кандидатных pad. Координаты кристалла X и Y берутся в диапазоне 0–72, комбинируются с пространством UID примерно из 5 300 значений и перемножаются с SysTick от 0 до 79 999. Итог — около 424 млн кандидатных pad. Для GPU это задача на несколько дней. 2) Для каждого pad перебрать число нажатий кнопок: kpad_b от 4 до 34. Так как по данным "попаданий" rtc_tr и rtc_ssr равны 0, их оставляют во внешних циклах, а ключевой перебор — по нажатиям. 3) Запустить полный конвейер на GPU: SHA256, BIP39, PBKDF2 (2048 раундов), BIP32, Hash160 и Bech32. Постоянный поток libngu можно заранее предвычислить и доставать по индексу, чтобы не заставлять каждый поток отдельно прокручивать константный PRNG. 4) Сопоставлять результаты через Bloom filter или отсортированный массив hash160 (сложность O(log n)), нацеливаясь на множество одноподписных P2WPKH-адресов. Затраты на поиск оценены количественно. Прогон пространства профиля A на одном Apple M1 GPU (72×72 UID, 80 000 вариантов SysTick и 31 вариант числа нажатий) дал около 14,8 млрд кандидатов и занял примерно 8,6 дня. Кластер уровня дата-центра на A100 сжимает тот же поиск до нескольких часов. Для Mk4, Mk5 и Q пространство около 72 бит примерно в 2^32 раз больше, чем у Mk3. Источник проблемы тот же, а подход с моделированием профилей сохраняется; меняется лишь масштаб вычислений: от "дней" к "неделям" и от одной машины к кластеру. По оценкам исследователей, такие затраты остаются приемлемыми для атакующих, что объясняет присутствие этих моделей среди списков жертв.