Это история о краже монет из кошелька, виной которой плохая криптография. В ней мы расскажем о нашем исследовании проблем в Libbitcoin Explorer
3.x
(CVE-2023-39910), опишем их связь с уязвимостью Trust Wallet
(CVE-2023-31290) и покажем реальные последствия, которые нам удалось подтвердить. Кроме того, в тексте приведены некоторые ранние исследования проблем в bx
2.x
, о которых нам стало известно на поздних стадиях расследования. Если вам нужна менее техническая информация, перейдите на страницу с кратким обзором и FAQ.
Содержание
- Часть I - Отслеживание проблемы до источника
- Часть II - Контекст и влияние
- Кодовое название "Milk Sad"
- Не первый взлом: слабая энтропия в кошельке Cake
- Даже не второй взлом: Использование Mersenne Twister в кошельке Trust Wallet
- Текущие кражи в разных блокчейнах - некоторые факты
- Поиск кошельков - мотивация и ограничения
- Поиск кошельков - реализация
- Другие подтвержденные жертвы этой кражи
- Основная хронология краж
- Реакция команды Libbitcoin и контекст
- Предполагаемые проблемы с RNG в старых версиях bx
- Часть III - Итоги и перспективы
- Кредиты и благодарности
Часть I - Отслеживание проблемы до источника
Имейте в виду, что в данном тексте опущены или изменены незначительные детали, касающиеся жертв.
Где моя крипта, чувак?
Наша история началась в пятницу, 21 июля 2023 года. При попытке воспользоваться хорошо защищённым криптовалютным кошельком его владелец обнаружил, что все средства, хранившиеся в кошельке, просто исчезли. Это не было случайностью — он стал жертвой довольно изощрённой кражи. Средства были отправлены на адреса злоумышленников 12 июля, тогда как аппаратный кошелёк не использовался в течение нескольких дней. (Подробности ниже)
Создание и использование пострадавшего кошелька было необычайно строгим:
- Сгенерирован на ноутбуке в изолированной сети (air-gapped) на ОС Linux с самокомпилируемым программным обеспечением
- Использована мнемоническая фраза BIP39 из 24 слов
- Мнемоника безопасно введена в аппаратные кошельки Ledger и Trezor
- В наличии хорошая PIN-кодировка и физическая защита аппаратных кошельков
- Мнемоническая сид-фраза никогда не касалась компьютеров вне изолированной сети
- Резервная копия мнемонической фразы была хорошо защищена.
Где крипта моего друга?
Жертва обратилась к своим знакомым с аналогичными протоколами генерации и управления ключами, и нашлась вторая жертва! У второй жертвы также было похищено содержимое криптовалютного кошелька в тот же период времени — биткоин (BTC) обеих жертв был похищен в одну и ту же минуту on-chain. Потерпевшие поняли, что это не случайность. Они стали жертвами некоего взлома.
Пострадавшие обнаружили, что похищены не только биткоины (BTC). Злоумышленники также похитили Ethereum и другие криптовалюты из тех же кошельков. Жертвы поняли, что это могло произойти только в результате утечки закрытых ключей от их основных кошельков. Форсирование аппаратных кошельков с целью авторизации некорректных переводов или взлом отдельных закрытых ключей суб-аккаунтов оказали бы более ограниченное воздействие.
Подобная кража, затронувшая сразу двух человек, несмотря на все меры предосторожности, должна быть очень маловероятной. Ещё хуже то, что эти двое были не единственными пострадавшими. Общедоступные биткойн-транзакции, связанные с кражей, сливали средства из множества различных кошельков, вероятно, примерно с тысячи различных владельцев кошельков только в Биткойне.
Так что же творится? Неужели кто-то нашел уязвимость в аппаратном кошельке, которую можно использовать удалённо, применил её в широких масштабах и ждал несколько месяцев, прежде чем коллективно выполнить on-chain транзакции по сливу? Хуже того, может быть, уязвим один из базовых криптографических примитивов? Может, здесь замешана магия квантовых вычислений? 😱
Напряжение нарастало, и начался поиск источника компрометации.
Наша крипта пропала, но как?!
По итогам общения обе жертвы поняли, что их пострадавшие кошельки были сгенерированы на схожем airgap сетапе, хоть и с разницей в несколько лет. На тот момент проблема казалась трудноопределимой и могла указывать на разные источники. Жертвы решили начать с самого начала — с шагов по созданию кошелька, с первых использованных команд и далее по нарастающей.
Исходным инструментом, участвовавшим в создании обоих кошельков, был Libbitcoin Explorer в версии 3.x и его бинарная команда bx
. Проект Libbitcoin с открытым исходным кодом существует уже очень давно (с 2011 года!), а bx
содержит всё необходимое для создания автономного кошелька в одном автономном бинарном файле.
Несмотря на то, что bx
является специализированным инструментом, о котором большинство пользователей кошельков даже не слышали, он пользуется определённой популярностью и ему посвящён небольшой раздел в приложении книги "Mastering Bitcoin". Другими словами, он представляется вполне адекватным для использования инструментом.
Краткий пример процесса создания кошелька в оболочке Linux:
# generate 256 bits of entropy, turn it into BIP39 mnemonics
bx seed -b 256 | bx mnemonic-new
<output of secret BIP39 mnemonic words>
Приведённая выше команда выдает мнемоническую фразу BIP39 из 24 слов, сопоставимую с теми, что приписаны кошелькам жертв. Этот закрытый ключ является основой всей защиты кошелька.
Может ли быть проблема связана с двоичной командой bx
, которую использовали жертвы? Они убедились, что подсистема /dev/random
Random Number Generator (RNG)(Генератор случайных чисел) в ноутбуках на Linux обладает достаточной энтропией, но, возможно, этого оказалось недостаточно? Может ли это быть серьезная проблема конфигурации системы или вирус?
К этому моменту мы позвали несколько друзей с опытом работы в области информационной безопасности, чтобы они помогли нам разобраться в ситуации и изучить соответствующие пути кода генерации кошельков 🕵️♂️.
По мере того как всё большее количество людей присматривалось к этому кейсу, обрисовались первые признаки серьезной проблемы.
Чем больше глаз, тем больше багов?
Команда решила, что логично будет начать поиск с просмотра исходного кода bx
команды bx seed
.
Выполнение bx seed
вызывает функцию new_seed(size_t bit_length)
в libbitcoin-explorer src/utility.cpp, которая вызывает функцию pseudo_random_fill(data_chunk& out)
в библиотеке libbitcoin-system:
console_result seed::invoke(std::ostream& output, std::ostream& error)
{
const auto bit_length = get_bit_length_option();
// These are soft requirements for security and rationality.
// We use bit vs. byte length input as the more familiar convention.
if (bit_length < minimum_seed_size * byte_bits ||
bit_length % byte_bits != 0)
{
error << BX_SEED_BIT_LENGTH_UNSUPPORTED << std::endl;
return console_result::failure;
}
const auto seed = new_seed(bit_length);
...
}
data_chunk new_seed(size_t bit_length)
{
size_t fill_seed_size = bit_length / byte_bits;
data_chunk seed(fill_seed_size);
pseudo_random_fill(seed);
return seed;
}
Только псевдослучайные? Ну ладно, генератор псевдослучайных чисел (PRNG) не обязательно так уж плох, если он является криптографически защищенным генератором псевдослучайных чисел — (Cryptographically Secure Pseudo Random Number Generator, CSPRNG). Возможно, всё в порядке, но давайте посмотрим внимательнее.
Мы следуем по пути вызова: pseudo_random::fill(data_chunk& out) -> pseudo_random::next() -> pseudo_random::next(uint8_t begin, uint8_t end) -> std::mt19937& pseudo_random::get_twister()
Подождите-ка. mt19937
, twister
— здесь используется PRNG Mersenne Twister (ГПСЧ Вихрь Мерсенна)? 🤔 В этот момент раздаются первые тревожные звоночки. PRNG Мерсенна не является CSPRNG, т.е. криптостойким, поэтому его не должно быть в любом коде, генерирующем секреты. Одно из уязвимых мест вихря Мерсенна заключается в том, что его внутреннее состояние может быть изменено до обратного злоумышленником, знающим несколько сотен результатов, что ставит под угрозу секретность остальных результатов того же потока, которые злоумышленнику неизвестны (если упрощённо).
Однако если ГПСЧ заново сидится перед каждой генерацией кошелька, извлекается только один результат, и этот результат хранится в секрете, будет ли слабая конструкция MT19937 достаточно фатальной, т.е. подверженной удалённой краже, если всё остальное сделано хорошо?
Прочёсывание кодового файла pseudo_random.cpp
продолжилось, и нам не пришлось углубляться в детали pseudo_random::get_twister()
, чтобы понять суть проблемы.
// Use the clock for seeding.
const auto get_clock_seed = []() NOEXCEPT
{
const auto now = high_resolution_clock::now();
return static_cast<uint32_t>(now.time_since_epoch().count());
};
// This is thread safe because the instance is thread static.
if (twister.get() == nullptr)
{
// Seed with high resolution clock.
twister.reset(new std::mt19937(get_clock_seed()));
}
Что за чёрт! Слабый алгоритм ГПСЧ, который сидится всего 32 битами системного времени, используется для генерации долгосрочных закрытых ключей кошельков, в которых хранится криптовалюта? 😧
Наша группа расследователей даже прошлась по нему дважды — не мог же bx
использовать для генерации закрытых ключей ЭТО?
Команда, искавшая уязвимость, не могла поверить, что дело в этом, и поставила простой эксперимент для проверки данной гипотезы. Как и во всех хороших экспериментах, переменные окружающей среды находились под контролем экспериментаторов, а в данном случае именно переменная time
была наиболее значимой для понимания рассматриваемой проблемы. Для проверки нашей теории мы использовали бинарник bx
официальной версии 3.2.0
в сочетании с библиотекой libfaketime
, запустив отдельные исполнения в абсолютно идентичных условиях синхронизации часов:
$ wget https://github.com/libbitcoin/libbitcoin-explorer/releases/download/v3.2.0/bx-linux-x64-qrcode -O ~/.local/bin/bx
$ chmod +x ~/.local/bin/bx
$ sudo apt install libfaketime
$ export LD_PRELOAD=/usr/lib/x86_64-linux-gnu/faketime/libfaketime.so.1 FAKETIME_FMT=%s FAKETIME=0
$ bx seed -b 256 | bx mnemonic-new
milk sad wage cup reward umbrella raven visa give list decorate bulb gold raise twenty fly manual stand float super gentle climb fold park
$ bx seed -b 256 | bx mnemonic-new
milk sad wage cup reward umbrella raven visa give list decorate bulb gold raise twenty fly manual stand float super gentle climb fold park
💥 В одно и то же время получался один и тот же "случайный" кошелёк! Невероятно!
Безопасная и надёжная утилита не выводила бы одну и ту же мнемоническую сид-фразу при таких обстоятельствах. Это было первое неопровержимое доказательство того, что код генерации секрета bx seed
в официальном релизе использует для энтропии кошелька поломанную псевдослучайную функцию, базирующуюся на времени.
Копнув глубже, команда убедилась, что в коде используется стандартный вариант ГПСЧ Mersenne Twister MT19937, который по своей конструкции работает только с 32 битами начального сидинг ввода, а не расширенный вариант MT19937-64 с 64 битами сидинга. Таким образом, этот ГПСЧ может иметь не более 2^32 стартовых позиций в качестве верхней границы, независимо от того, сидится ли он /dev/random
или временем.
Другими словами, при выполнении bx seed -b 256
для запроса 256 бит неугадываемой энтропии в результате выдаётся 32 бита времени высокоточных часов, пропущенных через блендер (точнее: твистер, он же вихрь 🌪️) и расширенных до 256 бит без добавления новой информации. Если бы это были реальные энтропийные данные, то количество возможных вариаций ключа росло бы экспоненциально с размером, поэтому разница между безопасным ожидаемым результатом (256 бит) и реальным результатом (32 бита) имеет астрономические масштабы!
Любой желающий может повторно вычислить и найти первоначально использованную энтропию жертвы после максимум около 4,29 млрд попыток, если он знает определённые характеристики, по которым можно определить успешное обнаружение криптовалютного кошелька. В данном случае это проверка производных адресов кошельков, которые в прошлом были замечены в получении средств на публичном блокчейне. Чтобы представить эту цифру в перспективе: перебор этого ключевого пространства занимает максимум несколько дней на среднем игровом ПК. И, к сожалению, это способен сделать любой человек, обладающий достаточными навыками программирования.
В плане безопасности криптовалютных кошельков это довольно катастрофическая ситуация.
Пятница 21 июля закончилась осознанием того, что всё стало очень сложно. Мало того, что несколько друзей безвозвратно потеряли полный контроль над ключами и средствами из своих кошельков, так ещё и у нашей группы возникла серьезная проблема с раскрытием информации.
Учитывая, что кража была впервые выявлена в США, члены команды хотели избежать сокрытия преступления и получить для жертв соответствующую возможность списания налогов из-за потерь, поэтому уже на раннем этапе мы сообщили о своих результатах в ФБРв формате официального раскрытия информации. Мы также сочли, что это может быть полезно для быстрого ограничения на перемещение этих средств злоумышленниками на основные биржи, если в этом возникнет необходимость.