Back to Blog

~$88M Потеряно: Эксплойты COLDCARD и LULA | Еженедельник BlockSec

Code Auditing
August 5, 2026
10 min read
Key Insights

За прошедшую неделю (2026/07/27 - 2026/08/02) выделены следующие 2 значимых инцидента безопасности, суммарные потери по которым составили около $88 млн.

Дата Инцидент Тип Оценочные потери
2026/07/29 LULA Уязвимость бизнес-логики ~$578K
2026/07/30 COLDCARD Ошибка генерации энтропии ~1 370 BTC (~$88M)*

* Потери COLDCARD варьируются в зависимости от метода подтверждения: ~1 370 BTC (~$88M) — публично верифицируемый on-chain минимум (coldcardwatch.com); сверка по закрытым каналам (Galaxy Research, на основе переписки с 73 пострадавшими) даёт более высокую цифру — около 1 596 BTC и до ~2 055 BTC (~$130M) с учётом предполагаемых, но не подтверждённых списаний.

Причины выбора

  • LULA: Привилегированная функция токена, способная перемещать балансы AMM-пары и принудительно пересинхронизировать резервы, превращается в повторяемый примитив дренажа ликвидности посредством манипуляции ценой.
  • COLDCARD: Ошибка сборки и интеграции в прошивке кошелька незаметно направила генерацию seed-фразы через детерминированный программный запасной вариант, подрывая гарантии энтропии и превратив восстановление seed в офлайн-поиск, переросший в масштабные потери средств.

Лучший аудитор безопасности для Web3

Проверьте дизайн, код и бизнес-логику перед запуском

Главное событие недели: COLDCARD

Мы выбрали COLDCARD в качестве главного события недели, поскольку ошибка энтропии кошелька привела к наибольшим потерям за период. Первопричина — защитный макрос сборки, проверявший наличие конфигурационного макроса, а не его включение, — представляет собой именно тот вид скрытой ошибки интеграции, которую функциональное тестирование не способно обнаружить, а урок применим к любой системе, где безопасность on-chain зависит от случайности off-chain.

COLDCARD, аппаратный Bitcoin-кошелёк, в 2021 году выпустил прошивку, генерировавшую seed-фразы кошелька с использованием детерминированного программного источника вместо предназначенного аппаратного генератора случайных чисел (RNG) [1][2]. Уязвимость не эксплуатировалась в масштабе вплоть до конца июля 2026 года, когда затронутые кошельки были опустошены в on-chain волнах, начавшихся 30 июля: публично подтверждённые потери составляют не менее 1 370 BTC (~$88M, по цене $64 099 на 5 августа) [3], тогда как данные из закрытых источников указывают на цифру около 1 596 BTC [4]. Первопричиной стала ошибка сборки и конфигурации, направившая генерацию seed через программный запасной вариант вместо предназначенного аппаратного RNG. Для затронутых устройств это превратило восстановление seed из криптографически неосуществимой задачи в офлайн-поиск.

Предыстория

COLDCARD — аппаратный Bitcoin-кошелёк. Приватные ключи и адреса кошелька полностью производятся из единственного секретного значения — seed, поэтому безопасность любого кошелька с самостоятельным хранением держится на двух свойствах этого seed: он должен оставаться секретным и быть непредсказуемым при генерации. Аппаратные кошельки существуют главным образом для защиты первого; данный инцидент — сбой второго. Мнемоническая фраза BIP-39 читается человеком, однако лежащее в её основе требование непредсказуемости определяется энтропией случайных байт, использованных при её создании. Если эти байты воспроизводимы, seed тоже можно воспроизвести. Интуитивная аналогия — сейф, комбинация которого выбирается броском кубиков: если кубики нечестные, сейф открыт для любого, кто знает смещение, — сколь бы прочным ни был замок.

Применительно к кошельку «нечестные кубики» означают слабый генератор случайных чисел. Ожидается, что аппаратные кошельки будут черпать энтропию seed из аппаратного истинного генератора случайных чисел (TRNG) на защищённом микроконтроллере, поскольку программный псевдослучайный генератор (PRNG) детерминирован: зная его внутреннее состояние и историю вызовов, можно в точности воспроизвести его вывод. Если выводы генератора предсказуемы, диапазон кандидатов можно перебрать и сопоставить с публичными данными кошелька — адресом, xpub или открытым ключом, — что сводит восстановление seed с криптографически неосуществимой задачи к офлайн-поиску.

Прошивка COLDCARD предоставляла два отдельных RNG-интерфейса. MicroPython поставлял платформенный слой STM32, экспортирующий глобальный символ rng_get(), который ожидала криптобиблиотека кошелька; параллельно COLDCARD поддерживал собственную локальную обёртку аппаратного RNG. Оба интерфейса предназначались для предоставления аппаратной энтропии, и криптобиблиотека кошелька обращается к одному из них через единственный глобальный символ RNG, разрешаемый при сборке прошивки.

Анализ уязвимости

Первопричиной стала ошибка сборки и интеграции, связанная с конфигурационным макросом MICROPY_HW_ENABLE_RNG. Конфигурация производственной платы COLDCARD устанавливала этот макрос в 0, поскольку прошивка предполагала использование собственной локальной обёртки аппаратного RNG, а не реализации аппаратного RNG от MicroPython. Однако путь генерации кошелька был перенесён на ngu.random.bytes(32), и путь STM32 в криптобиблиотеке в конечном счёте зависел от глобального символа rng_get(), разрешаемого MicroPython.

Цепочка проблемы выглядела следующим образом:

generate_seed()
  -> ngu.random.bytes(32)
  -> libngu CHIP_TRNG_32()
  -> rng_get()
  -> Модуль RNG STM32 в MicroPython
  -> Программный запасной вариант Yasmarang, так как MICROPY_HW_ENABLE_RNG == 0

Конфигурация платы отключала ветку аппаратного RNG в MicroPython:

// У нас есть собственная версия этого кода.
#define MICROPY_HW_ENABLE_RNG (0)

Криптобиблиотека по-прежнему считала наличие макроса достаточным доказательством наличия аппаратного RNG [5], поскольку перед вызовом rng_get() проверяла лишь существование макроса:

extern uint32_t rng_get(void);
#define CHIP_TRNG_32() rng_get()

#ifndef MICROPY_HW_ENABLE_RNG
#error "get a HW TRNG plz"
#endif

Эта защита упускает опасный случай: макрос, определённый как 0, всё равно определён, поэтому проверка #ifndef проходит и сборка завершается успешно. Поскольку MicroPython выбирает реализацию RNG по значению макроса, а не по его наличию, MICROPY_HW_ENABLE_RNG == 0 направлял rng_get() в ветку программного запасного варианта вместо локальной обёртки платы COLDCARD [6]:

#if MICROPY_HW_ENABLE_RNG
    // Аппаратный RNG STM32
#else
    // Программный запасной вариант Yasmarang
#endif

Последствия различаются в зависимости от поколения устройства. Для прошивок Mk2/Mk3 v4.0.0–v4.1.9 анализ Block [7] утверждает, что никакая криптографическая энтропия не добавлялась в ngu.random, поэтому генерация кошелька может стать детерминированной, как только станет известно состояние запасного варианта и история вызовов (рекомендация Coinkite [2] ограничивает диапазон Mk2/Mk3 несколько уже: v4.0.1–v4.1.9). Для Mk4/Q/Mk5 материал защищённого элемента хешировался, однако в ngu.random.reseed() передавалось лишь четыре байта, ограничивая надёжный пересев единственным 32-битным словом состояния — значительно меньшим пространством поиска, чем ожидают пользователи от энтропии кошелька. Хеширование конечных 32 случайных байт не может увеличить энтропию результата; оно лишь преобразует уже ограниченное множество кандидатов.

Анализ атаки

В отличие от эксплойта смарт-контракта, этот инцидент не имеет единственной on-chain транзакции атаки для отслеживания. Это была задача офлайн-восстановления seed с последующими on-chain опустошениями. Предварительным условием являлось то, что затронутые пользователи генерировали свои seed-фразы через уязвимый путь ngu.random.bytes(32), поэтому их seed-материал зависел от воспроизводимого состояния программного запасного варианта, а не от полноценной аппаратной энтропии. Второе, предполагаемое предварительное условие заключается в том, что затронутые кошельки выводились из seed в чистом виде: надёжная уникальная парольная фраза BIP-39 добавляет независимую, предоставляемую пользователем энтропию через PBKDF2, которую уязвимость RNG никогда не затрагивала, выводя такие кошельки за пределы чистого перебора seed; масштаб опустошений свидетельствует о том, что большинство затронутых пользователей не устанавливали такой парольной фразы. Далее восстановление, по всей видимости, проходило в три этапа:

  1. Атакующий ограничивал или перебирал состояния-кандидаты RNG, используя метаданные устройства, время загрузки, предположения о RTC/SysTick и вероятную историю вызовов RNG.
  2. Для каждого состояния-кандидата атакующий выводил seed-фразы-кандидаты и проверял их офлайн на соответствие публичным данным кошелька — адресам, xpub или сгенерированным открытым ключам.
  3. Когда кандидат совпадал с реальным кошельком, атакующий восстанавливал seed, реконструировал приватные ключи и опустошал связанные BTC.

В сети кража проявилась как всплеск опустошений адресов, начавшихся 30 июля. Независимое on-chain эвристическое отслеживание [3] выявляет несколько волн дренажа общим объёмом не менее 1 370 BTC (около $88M, оценивая монеты по цене BTC $64 099 на 5 августа), опустошённых с 4 580 верифицированных адресов, — это верифицированный минимум, а не итог. Тем временем закрытый канал [4], подтверждённый в переписке с 73 пострадавшими, оценивает цифру приблизительно в 1 596 BTC, возрастающую до 2 055 BTC (~$130M) с учётом предполагаемых, но не подтверждённых списаний.

Заключение

Инцидент с COLDCARD представлял собой сбой генерации энтропии: критически важный для безопасности API генерации seed незаметно обращался к детерминированному программному PRNG-запасному варианту вместо предназначенного аппаратного RNG, поскольку защитный макрос сборки проверял наличие конфигурационного макроса, а не его включение. Для затронутых устройств это превратило восстановление seed из криптографически неосуществимой задачи в офлайн-поиск, в результате чего было опустошено более 1 370 BTC [3] в нескольких волнах (по данным закрытых каналов — около 1 596–2 055 BTC [4]).

Ключевой инженерной ошибкой является то, что отправленная прошивка так и не подтвердила, что её наиболее критически важный для безопасности API действительно обращается к предназначенному аппаратному RNG. Три практики позволили бы это обнаружить: защитные макросы сборки для криптографической энтропии должны проверять как наличие макроса, так и его значение; запасные варианты энтропии должны завершаться с ошибкой, а не незаметно подставлять программный PRNG; верификация финального образа прошивки должна охватывать происхождение символов и сквозной поток энтропии, а не только успешность компиляции кода. Скомпрометированный seed нельзя исправить на месте; средства с него следует перевести в кошелёк, созданный на исправленной прошивке, а надёжная уникальная парольная фраза снижает непосредственную уязвимость, не устраняя сам seed [2].

Механизм сбоя заслуживает особого внимания: ошибки такого рода в генерации случайных чисел невидимы при функциональном тестировании, поскольку каждый сгенерированный seed по отдельности является валидным. Дефект заключается не в каком-либо отдельном выводе, а в источнике, который их породил: поскольку этот источник предсказуем и воспроизводим, совокупность seed попадает в небольшой, перечислимый диапазон. Компоненты генерации ключей off-chain заслуживают первоклассного внимания к безопасности.

Источники

Начните работу с Phalcon Explorer

Исследуйте транзакции, чтобы действовать обдуманно

Попробовать бесплатно

Другие инциденты недели

LULA

LULA, токен BEP-20 в сети BNB Chain, 29 июля 2026 года потерял около $578K вследствие уязвимости бизнес-логики в контракте токена. Доступный атакующему путь позволял запустить привилегированную функцию recycle(), позволяя контракту Rental напрямую переводить LULA из пары PancakeSwap V2, а затем вызывать sync(), обновляя резервы пары до манипулированных балансов. Атакующий многократно вызывал recycle(), сводя резерв LULA пары почти к нулю, после чего обменивал небольшое количество LULA обратно почти на весь его USDT [1].

Предыстория

LULA — токен BEP-20 в сети BNB Chain с механизмом командного вознаграждения на основе аренды. Правомочные адреса накапливают ожидающие командные вознаграждения в контракте Rental и получают их через claimTeamReward(). В процессе получения контракт Rental вызывает функцию recycle() токена для получения LULA с целью распределения вознаграждения. recycle() не может быть вызвана произвольными пользователями; только контракт Rental уполномочен её выполнять.

Точка входа claimTeamReward() содержит проверку «только EOA». Она поддерживает прямые вызовы от аккаунтов с внешним управлением, где msg.sender == tx.origin, а также делегированные вызовы EIP-7702 посредством проверки префикса делегированного кода.

На автоматическом маркет-мейкере (AMM) пара определяет цены свапов из хранимых резервов, которые обновляются через функцию sync() пары, устанавливающую их в текущие балансы токенов пары. Резервы обычно отражают реальную торговлю, поскольку изменяются вместе со свапами и событиями ликвидности, однако баланс токенов пары также может быть изменён прямым переводом, и sync() копирует любой присутствующий баланс — манипулированный или нет — в хранимые резервы.

Анализ уязвимости

Первопричиной являлось то, что LULA.recycle() позволяет контракту Rental напрямую переводить LULA из пары PancakeSwap V2 и затем вызывать sync(), обновляя резервы пары до манипулированных балансов [1].

Поскольку sync() устанавливает резервы в любой остаток LULA в паре, этот привилегированный путь может произвольно снижать резерв LULA пары, не затрагивая сторону USDT. Когда резерв LULA близок к нулю, пара оценивает небольшое количество LULA почти как весь свой USDT.

Анализ атаки

Следующий анализ основан на транзакции 0xa219ab9...411d7c.

  • Шаг 1: Атакующий обеспечил манипуляцию, накопив ~197,05M USDT. Средства поступили из нескольких источников флеш-займов и заимствований, включая Moolah/Lista, Aave V3, Venus, PancakeSwap V3, PancakeSwap Vault, Uniswap V4 PoolManager и Uniswap V3.
  • Шаг 2: Атакующий использовал ~197,05M USDT для крупного свапа USDT -> LULA через роутер PancakeSwap V2. Это резко сократило резерв LULA пары примерно с 8M LULA до 24 022 LULA, при этом сторона USDT возросла до примерно 197,64M USDT.
  • Шаг 3: Атакующий задействовал путь вознаграждения через несколько кошельков EIP-7702. Каждый кошелёк вызывал claimTeamReward() на контракте Rental, который затем запускал LULA.recycle(), сокращая резерв LULA пары с 24 022 LULA до 0,004 LULA, в то время как пара по-прежнему удерживала очень крупную сторону USDT.
  • Шаг 4: Атакующий провёл финальный свап через роутер PancakeSwap V2, отправив в пару всего ~4 749 LULA и получив ~197,64M USDT.
  • Шаг 5: Атакующий погасил все флеш-займы, получив прибыль около $578K.

Заключение

Токен LULA в сети BNB Chain был эксплуатирован примерно на $578K через уязвимость бизнес-логики в контракте токена: доступный атакующему путь позволял запустить привилегированную функцию recycle(), позволяя контракту Rental напрямую переводить LULA из пары PancakeSwap V2 и затем вызывать sync(), пересинхронизируя резервы пары с манипулированными балансами. Атакующий многократно запускал это, искажая цену пары и обменивая небольшое количество LULA обратно почти на весь её USDT.

Контракт токена никогда не должен открывать привилегированный путь, способный перемещать балансы AMM-пары и принудительно пересинхронизировать резервы, поскольку это передаёт контроль над ценой тому, кто способен до него добраться. Токены, интегрирующиеся с AMM-парами, должны обеспечивать привязку резервов пары к подлинным, рыночным изменениям баланса, а любая логика, использующая резервы пула для ценообразования, должна рассматривать их как потенциально манипулированные, а не как авторитетный источник.

Источники

Best Security Auditor for Web3

Validate design, code, and business logic before launch. Aligned with the highest industry security standards.

BlockSec Audit