Предугадывая случайные числа в смарт-контрактах Ethereum 0





Сегодня Ethereum широко известен как платформа для проведения ICO. Тем не менее, Ethereum может быть использован не только для работы с ERC20 токенами. Его блокчейн может послужить основой для разработки рулеток, лотерей, карточных игр и их аналогов. Как и любой другой распределенный реестр, блокчейн Ethereum децентрализован, прозрачен и защищен от попыток повлиять на механизмы его работы вне алгоритмов его работы. Ethereum позволяет запускать полные по Тюрингу программы, как правило написанные на Solidity, что, если верить основателям платформы, превращает его во «всемирный суперкомпьютер». Эти свойства особенно полезны в контексте создания компьютерных азартных игр, для которых очень важен вопрос доверия со стороны пользователей.

Блокчейн Ethereum детерминистичен и это его качество усложняет любые попытки написать собственный Генератор псевдослучайных чисел (ГПСЧ) — неотъемлемую часть любого азартного приложения. Мы решили провести исследование смарт-контрактов с целью оценить безопасность написанных на Solidity ГПСЧ и определить распространенные антипаттерны проектирования, ведущие к появлению уязвимостей, позволяющих предсказать будущие состояния.

Наше исследование проходило в несколько этапов:

  1. Отбор 3,649 смарт-контрактов с помощью etherscan.io и GitHub.
  2. Импорт контрактов в опенсорсную поисковую систему Elasticsearch.
  3. Поиск и отбор уникальных реализаций ГПСЧ с помощью веб-интерфейса Kibana, позволяющего производить расширенный поиск и фильтрацию. Были обнаружены 72 уникальные реализации.
  4. Ручная проверка каждого контракта, в рамках которой 43 из них были признаны уязвимыми.

Уязвимые реализации


Анализ выявил 4 категории уязвимых ГПСЧ:

  • ГПСЧ, использующие переменные блока в качестве источника энтропии
  • ГПСЧ, основанные на хэше одного из прошлых блоков
  • ГПСЧ, основанные на хэше одного из прошлых блоков вкупе с сид-фразой, которая считалась приватной
  • ГПСЧ, уязвимые к игре на опережение

Давайте изучим каждую категорию и примеры уязвимого кода.

ГПСЧ, основанные на переменных блока


Существует целый ряд переменных блока, которые могут быть некорректно использованы в качестве источника энтропии:

  • block.coinbase — представляет адрес майнера, добывшего текущий блок
  • block.difficulty — относительная мера сложности добычи блока
  • block.gaslimit — ограничивает максимальное потребление газа для транзакций в блоке
  • block.number — высота текущего блока
  • block.timestamp — время добычи блока

Во-первых, майнеры могут манипулировать каждой из этих переменных, и поскольку они мотивированы поступить именно так, то использовать указанные переменные в качестве источника энтропии нельзя. Но еще более важен тот факт, что переменные очевидно могут быть использованы разными контрактами внутри одного блока. Поэтому если контракт злоумышленника обратится к контракту жертвы с помощью внутреннего сообщения, то одинаковый ГПСЧ в обоих контрактах даст одинаковый результат.

Пример 1 (0x80ddae5251047d6ceb29765f38fed1c0013004b7):

// Победа, если номер блока — четное число 
// (примечание: это очень плохой источник случайности. 
// Пожалуйста, не используйте его с реальными деньгами)
bool won = (block.number % 2) == 0;

Пример 2 (0xa11e4ed59dc94e69612f3111942626ed513cb172):

// Вычисляем некоторое *почти случайное* значение для выбора //победителя из текущей транзакции.
var random = uint(sha3(block.timestamp)) % 2;

Пример 3 (0xcC88937F325d1C6B97da0AFDbb4cA542EFA70870):

address seed1 = contestants[uint(block.coinbase) % totalTickets].addr;
address seed2 = contestants[uint(msg.sender) % totalTickets].addr;
uint seed3 = block.difficulty;
bytes32 randHash = keccak256(seed1, seed2, seed3);
uint winningNumber = uint(randHash) % totalTickets;
address winningAddress = contestants[winningNumber].addr;

ГПСЧ на базе блокхэшей


Каждый блок к блокчейне Ethereum имеет верификационных хэш. Виртуальная машина Ethereum EVM позволяет получать подобные хэши блоков с помощью функции block.blockhash(). Эта функция ожидает отправку числового аргумента, определяющего номер блока. В ходе исследования мы обнаружили, что результат вызова block.blockhash() часто используется неверно в различных реализациях ГПСЧ.

Существует три основных ошибочных вариации подобных ГПСЧ:

  • block.blockhash(block.number), что позволяет получить хэш текущего блока
  • block.blockhash(block.number - 1), то же с хэшем предыдущего блока
  • block.blockhash() обрабатывает блок, минимум на 256 блоков старше текущего

Давайте изучим каждый из этих случаев.

block.blockhash(block.number)

Переменная состояния block.number позволяет получить высоту текущего блока. Когда майнер берет транзакцию, выполняющую код контракта, block.number содержащего ее будущего блока уже известна, поэтому контракт всегда и гарантированно может узнать ее значение. Тем не менее в момент выполнения транзакции в EVM, блокхэш еще не созданного до конца блока по очевидным причинам неизвестен и потому EVM будет всегда отдавать 0.

Некоторые контракты неверно истолковывают значение выражения block.blockhash(block.number). В случае с этими контактами считалось, что блокхэш текущего блока был известен в момент запуска и был применен в качестве источника энтропии.

Пример 1 (0xa65d59708838581520511d98fb8b5d1f76a96cad):

function deal(address player, uint8 cardNumber) internal returns (uint8) {
 uint b = block.number;
 uint timestamp = block.timestamp;
 return uint8(uint256(keccak256(block.blockhash(b), player, cardNumber, timestamp)) % 52);
}

Пример 2 (https://github.com/axiomzen/eth-random/issues/3):

function random(uint64 upper) public returns (uint64 randomNumber) {
 _seed = uint64(sha3(sha3(block.blockhash(block.number), _seed), now));
 return _seed % upper;
}

block.blockhash(block.number-1)

Другая группа контрактов пользуется другой вариацией ГПСЧ на основе блокхэшей, полагаясь на хэш последнего сформированного блока. Излишне говорить, что этот подход также содержит в себе изъян: злоумышленник может специально создать контракт с таким же кодом ГПСЧ, работа которого будет направлена на вызов целевого контракта с помощью внутреннего сообщения. «Случайные» числа двух контрактов будут совпадать.

Пример 1 (0xF767fCA8e65d03fE16D4e38810f5E5376c3372A8):

//Генерация случайного числа между 0 и max
uint256 constant private FACTOR =  1157920892373161954235709850086879078532699846656405640394575840079131296399;
function rand(uint max) constant private returns (uint256 result){
 uint256 factor = FACTOR * 100 / max;
 uint256 lastBlockNumber = block.number - 1;
 uint256 hashVal = uint256(block.blockhash(lastBlockNumber));
 return uint256((uint256(hashVal) / factor)) % max;
}

Хэш будущего блока


Лучше показывает себя подход с применением хэша некоторого будущего блока. Сценарий реализации следующий:

  • Игрок делает ставку, заведение сохраняет block.number транзакции.
  • Своим вторым обращением к контракту, игрок запрашивает заведение объявить номер победителя.
  • Заведение получает сохраненный block.number из хранилища, вычисляет хэш этого блока и пользуется им для последующей генерации псевдослучайного числа.

Это подход работает только если соблюдается одно важное требование. Документация Solidity предупреждает о количестве блокхэшей, которое EVM способно сохранить:



И поэтому если второй вызов приходится на блок, отстающий от первого более чем на 256 блоков, и валидация блокхэшей не проводится, псевдослучайное число будет известно наперед. Это будет ноль вместо случайного хэша.

Самый яркий пример эксплуатации этой уязвимости — хак лотереи SmartBillions, у контракта которой не было должной валидации возраста block.number. Некий неизвестный игрок подождал 256 блоков и угадал уже не случайное, известное число.

Блокхэш с приватным сидом


Для увеличения энтропии некоторые из проанализированных нами контрактов применяли дополнительный, якобы скрытый от посторонних глаз, сид. Один из таких случаем — лотерея Slotthereum. Далее соответствующий код:

bytes32 _a = block.blockhash(block.number - pointer);
for (uint i = 31; i >= 1; i--) {
 if ((uint8(_a[i]) >= 48) && (uint8(_a[i]) <= 57)) {
   return uint8(_a[i]) - 48;
 }
}

Переменная-указатель была объявлена как приватная, что означает, что другие контракты не могут обращаться к ее значению. После каждой игры этой переменной присваивался выигрышный номер от 1 до 9, который который прибавлялся к block.number. В итоге хэш вычислялся с учетом этого отклонения.

Блокчейн — система прозрачная по своей природе, и потому ей не следует пользоваться для хранения секретных данных в открытом виде. Несмотря на то, что приватные переменные защищены от других контрактов, доступ к содержимому хранилища контракта возможно получить извне блокчейна. К примеру, у популярного Ethereum-клиента web3 имеется API-метод web3.eth.getStorageAt(), позволяющий получать содержимое хранилища при обращении к конкретным индексам.

Знание этого факта превращает извлечение значения приватной переменной-указателя из хранилища контракта тривиальной задачей. После чего уязвимостью можно воспользоваться:

function attack(address a, uint8 n) payable {
 Slotthereum target = Slotthereum(a);
 pointer = n;
 uint8 win = getNumber(getBlockHash(pointer));
 target.placeBet.value(msg.value)(win, win);
}

Игра на опережение


Для получения максимальной награды майнеры выбирают транзакции, которые войдут в новоиспеченный блок, так, чтобы сумма используемого ими газа была как можно больше. Порядок выполнения транзакций в блоке определяется количеством отдаваемого вместе с ней газа. Транзакция, обеспеченная наибольшим количеством газа будет выполнена первой. Поэтому, манипулируя количеством газа, можно добиться выполнения нужной транзакции раньше остальных транзакций текущего блока. Это делает возможным так называемую игру на опережение — проблему безопасности, когда поток выполнения контракта зависит от его положения в блоке.

Рассмотрим пример. Лотерея пользуется внешним оракулом для получения псевдослучайных чисел, которые, в свою очередь, применяются для определения победителя из числа игроков, сделавших свои ставки в каждом раунде. Эти числа отправляются в незашифрованном виде. Мошенник может наблюдать за пулом ожидающих подтверждения транзакций и дождаться отправки оракулом числа. Как только транзакция оракула появляется в пуле, мошенник отправляет ставку с самым высоким количеством газа. Его транзакция была создана уже в конце, но наибольшему количеству газа, она фактически будет выполнена до транзакции оракула, и приз уйдет мошеннику. Подобная задача была представлена на ZeroNights ICO Hacking Contest.

Другой пример контракта, уязвимого к игре на опережение — игра под названием Last is me! Каждый раз когда игрок покупает билет, он «садится на место». Далее игра ждет новых «садящихся» и если на протяжении некоторого определенного количества блоков никто не покупает билет, последний игрок, «севший на место» берет джекпот. Ближе к концу раунда, мошенник может понаблюдать за тем, есть ли в пуле транзакции других участников и отправить свою, сопроводив ее самым большим количеством газа.

Навстречу более безопасным ГПСЧ


Существует несколько подходов к реализации безопасных ГПСЧ на блокчейне Ethereum.

  • Внешние оракулы
  • Signidice
  • Подход «коммит-раскрытие»

Внешние оракулы: Oraclize


Oraclize — сервис для распределенных приложений, предоставляющий мост между блокчейном и внешней средой (Интернетом). С помощью Oraclize смарт-контракты могут запрашивать данные из веб API, например, данные о курсах обмена, прогноз погоды и цены на акции. Один из самых полезных кейсов его применения — возможность его использования в качестве ГПСЧ. Некоторые из проанализированных нами контрактов использовали Oraclize для получения случайных чисел от random.org с помощью URL-коннектора. Эта схема изображена на рисунке 1.



Рисунок 1. Схема работы Oraclize

Главный недостаток этого подхода заключается в том, что сервис централизован. Можем ли мы быть уверены, что демон Oraclize не подменит результаты? Можем ли мы доверять random.org и всей лежащей в его основе инфраструктуре? Несмотря на тот факт, что Oraclize предоставляет подтверждение результатов, заверенное с помощью с помощью TLSNotary, пользоваться этим способом подтверждения можно только вне блокчейна и только после того, как победитель уже был выбран. Более правильным будет вариант, когда Oraclize используется в качестве источника «случайных» данных с помощью подтверждения на основе аппаратного кошелька Ledger.

Внешние оракулы: BTCRelay


BTCRelay — мост между блокчейнами Ethereum и Биткоин. С помощью этого сервиса смарт-контракты на блокчейне Ethereum могут запрашивать будущие хэши будущих блоков Биткоина и пользоваться ими в качестве источника энтропии. Один из проектов, пользующийся BTCRelay в качестве ГПСЧ — это The Ethereum Lottery.

Подход BTCRelay не безопасен поскольку не учитывает мотивацию майнеров вмешаться в процесс. Да, он требует гораздо более ощутимых усилий по сравнению с обращением к хэшам Ethereum, однако при этом он фактически просто пользуется тем фактом, что цена комиссий в сети Биткоин выше их цены в сети Ethereum. Таким образом риск мошеннического поведения со стороны майнеров снижается, но не устраняется совсем.

Signidice


Signidice — алгоритм, использующий криптографические подписи, применимый в качестве ГПСЧ в смарт-контрактах, в которых есть только две стороны: игрок и заведение. Алгоритм работает следующим образом:

  • Игрок делает ставку, вызывая смарт-контракт.
  • Заведение видит ставку, подписывает ее приватным ключом и отправляет подпись в смарт-контракт.
  • Смарт-контракт подтверждает подпись с помощью известного публичного ключа.
  • Эта подпись потом используется для генерации случайного числа.

У Ethereum есть встроенная функция ecrecover() для подтверждения ECDSA подписей внутри блокчейна. Тем не менее, ECDSA не может быть использована в Signidice, поскольку заведение имеет возможность манипулировать параметрами ввода (особенно параметром k) и таким образом влиять на создание подписи. Проверка концепции подобного способа мошенничества уже была представлена Алексеем Перцевым.

К счастью, с в выпущенном относительно недавно хард-форке Metropolis был представлен оператор возведения в степень по модулю. Это позволяет пользоваться RSA-подписями, которые, в отличие от ECDSA не позволяют манипулировать входными параметрами для поиска удобной подписи.

Подход «коммит-раскрытие»


Как видно из названия этого способа, он состоит из двух этапов:

  • Этап «коммита», в рамках которого стороны отправляют свои криптографически защищенные секретные данные в смарт-контракт.
  • Этап «раскрытия», когда стороны объявляют свои сиды открытым текстом, смарт-контракт проверяет, что они корректны, и сиды используются для генерации случайного числа.

Правильная реализация коммита-раскрытия не должна полагаться на какую-либо одну из сторон. Несмотря на то, что игроки не знают изначальный сид, отправленный владельцем, и их шансы равны, владелец также может выступить в роли игрока, и в силу этого они не могут ему доверять.

Хороший пример реализации этого метода — Randao. Этот ГПСЧ собирает хэшированные сиды у множества сторон и каждая из сторон получает вознаграждение за участие. Никто не знает сиды других участников и поэтому результат получается совершенно случайным. Тем не менее если хотя бы одна из сторон откажется раскрывать свой сид, то отказ в сервисе получат все участники.

Коммит-раскрытие можно сочетать с вычислением хэшей будущих блоков. В этом случае существует три источника энтропии:

  1. sha3(seed1) владельца
  2. sha3(seed2) игрока
  3. хэш будущего блока

Случайное число в таком случае генерируется так: sha3(seed1, seed2, blockhash). Таким образом, подход коммит-раскрытие решает проблему мотивированных к мошенничеству майнеров: они по-прежнему могут принимать решения по поводу блокхешей, но не знают сиды игрока и владельца. Он также устраняет мошенническую мотивацию владельца: он знает только свой сид, сид игрока и будущего блока неизвестны. Кроме того, этот подход позволяет не беспокоит о ситуациях когда владелец и майнер — одно и то же лицо. В таком случае этому лицу неизвестен по крайней мере сид игрока.

Заключение


Безопасная реализация ГПСЧ в блокчейне Ethereum остается непростой задачей. Как видно из нашего исследования, в силу отсутствия готовых решений, разработчики склонны пользоваться собственными реализациями. Однако из-за того, ограниченность блокчейна в плане источников энтропии приводит к ошибкам в подобных реализациях. Проектируя ГПСЧ, разработчикам следует прежде всего четко прояснить для себя стимулы каждой стороны и только после этого выбирать сообразный подход.

image

Вы можете помочь и перевести немного средств на развитие сайта



Комментарии (0):