
TL;DR
Мы обнаружили критическую уязвимость во внешнем кошельке Loopring, которая позволяет любому получить контроль над каждой учетной записью, созданной с его помощью. Уязвимость возникает из-за того, что внешний кошелек Loopring использует 32-битное целое число для получения закрытого ключа каждого пользователя . Это позволит злоумышленнику воспроизвести все такие закрытые ключи. Мы продемонстрировали уязвимость, восстановив закрытые ключи для более чем дюжины учетных записей. Об этой проблеме сообщили в Loopring, и она быстро принимает меры для ее решения .
Сказка о двух ключах
В Loopring у пользователя есть два типа ключей:
- Ключ Эфириума
- (Цикл) Ключ учетной записи
Ключ учетной записи необходим для его дружественных к SNARK свойств, поскольку гораздо (гораздо) проще доказать подпись, подписанную ключом учетной записи, чем ключом Ethereum. Ключ Аккаунта, дружественный к SNARK, отличается от ключа Ethereum несколькими параметрами (например, эллиптической кривой). Из-за этого его нелегко поддерживать другими кошельками.
Когда пользователь присоединяется к системе, у него, скорее всего, есть ключ Ethereum, но еще нет ключа учетной записи. Таким образом, даже если пользователь выбирает MetaMask в качестве своего кошелька при подключении к Loopring, он будет обслуживать только его транзакции Ethereum (для взаимодействия с контрактом, включая депозиты и снятие средств). Но для отправки заказов пользователь должен будет использовать свой ключ Аккаунта.
Loopring решил выполнять все операции, связанные с ключом учетной записи, в браузере. Таким образом, в рамках процесса регистрации пользователь генерирует свой ключ учетной записи, а сопоставление между двумя типами ключей сохраняется в контракте. С этого момента каждый раз, когда пользователю нужно отправлять заказы, он будет использовать ключ своей учетной записи, а не ключ Ethereum.
Процесс получения ключа учетной записи
Чтобы сгенерировать пару ключей учетной записи (открытый, закрытый¹), пользователь должен ввести пароль. Этот пароль вместе с Ethereum-адресом пользователя используется для получения ключа его Аккаунта:
Это само по себе является плохой практикой: поскольку адреса Ethereum, а также открытые ключи учетной записи можно прочитать из контракта, возможен всесторонний поиск паролей. И, как учит нас история снова и снова (см. Мозговые кошельки²), большинство пользователей не умеют выбирать надежный пароль.
Это не заканчивается здесь. Осматриваем дальше:
Адрес Ethereum и пароль используются для генерации seed . Затем начальное значение используется для получения randomNumber . Это randomNumber позже используется для получения энтропии, которая даст пользователю желаемый закрытый ключ.
Но как из начального числа получается randomNumber ?
hashCode
Давайте посмотрим на хэш -код :
Таким образом, в нашем случае hashCode будет проходить по начальному символу за символом и вычислять из него h.
Но Math.imul выполняет умножение 32-битных целых чисел³! Это означает, что результатом является 32-битное целое число. Другая операция, которую мы видим здесь (для сложения), это | 0. Но побитовые операции в JavaScript приводят к 32-битным целым числам⁴ (все операнды преобразуются в 32-битные целые числа).
Результат: hashCode возвращает 32-битное целое число! Это означает, что независимо от начального значения randomNumber будет 32-битным целым числом.
Вся энтропия для генерации закрытых ключей, всего пространства ключей составляет 32 бита.
Подразумеваемое
Независимо от пароля пользователя, закрытый ключ его учетной записи будет получен из 32-битного пространства ключей. Мы можем вычислить все возможные ключи учетной записи в системе (при условии, что они были созданы с использованием их внешнего интерфейса).
Таким образом, мы можем найти приватные ключи всех пользователей, зарегистрированных через API . На самом деле, всего за короткое время нам действительно удалось воспроизвести закрытые ключи для более чем дюжины учетных записей, чтобы продемонстрировать эту уязвимость.
Управляя этими ключами, злоумышленники могут отправлять заказы от имени пользователей. Это может позволить им фактически украсть средства. Один из способов сделать это — подать достаточно ордеров на неликвидную пару с ценой, далекой от текущей цены. Например, если злоумышленники хотят украсть DAI с аккаунтов, они могут отправить множество заказов с предложением «купить 1 ETH за 10000 DAI». В то же время злоумышленники будут размещать заказы «продать 1 ETH за 10000 DAI». Если пара недостаточно ликвидна, в конечном итоге эти ордера будут выполнены (после того, как книга ордеров будет израсходована), и аккаунт злоумышленников получит эти DAI за небольшое количество ETH.
Выводы
В настоящее время безопасные кошельки Ethereum поставляются с ограниченным набором криптографических стандартов и недостаточно гибки для какой-либо настройки. Система, которая не может использовать эти стандарты, должна создать свой альтернативный кошелек с нуля. Это включает в себя выполнение операций, связанных с безопасностью, в браузере (что нецелесообразно); использование паролей для генерации ключей (что считается плохой практикой); и введение в процесс критических ошибок, таких как описанная выше ошибка хэш-кода (что очень плохо).
Будем надеяться, что в ближайшем будущем кошельки Ethereum станут более гибкими и будут поддерживать более широкий набор криптографических примитивов. Это еще раз отключит процесс создания безопасных кошельков от процесса создания dApp, тем самым предотвратив такие ошибки в будущем.
Благодарность: спасибо Луису Гутманну ( @GuthL ), менеджеру по продукту и исследователю StarkWare, за помощь в эксплуатации уязвимости.
~Авиху Леви ( @avihu28 ), руководитель отдела продуктов, StarkWare
— —
¹ В коде используется «секретный» вместо обычно используемого «частный».
² https://bitcoin.stackexchange.com/questions/8449/how-safe-is-a-brain-wallet
³ https://stackoverflow.com/questions/21052816/why-would-i-use-math-imul
⁴ https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Bitwise_Operators