Ethereum завоевал огромную популярность как платформа для ICO. Но она применима не только для создания токенов стандарта ERC-20. Блокчейн Ethereum можно использовать в онлайн-рулетке, лотереях и карточных играх. Подтвержденные транзакции блокчейна нельзя подделать — технология децентрализована и прозрачна, — но код умных контрактов может быть уязвим. Одна из проблем — уязвимые генераторы псевдослучайных чисел, ГПСЧ. Давай разберем типовые ошибки реализации ГПСЧ в азартных играх на базе Ethereum.

Ethereum позволяет исполнять тьюринг-полные программы, обычно написанные на языке Solidity, поэтому основатели платформы называют ее «мировым суперкомпьютером». Благодаря прозрачности Ethereum весьма удобно использовать в сфере азартных онлайн-игр, где очень важно доверие пользователя.

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

INFO

В 2017 году специалисты Positive Technologies реализовали проекты по анализу безопасности и защите от киберпреступников как процедуры ICO, так и внедрения блокчейн-технологий. Результаты исследования оказались безрадостными: уязвимости в смарт-контрактах были выявлены в 71% проектов, в 23% проектов были выявлены недостатки, позволяющие атаковать инвесторов. В каждом проекте ICO в среднем содержалось пять уязвимостей. Однако злоумышленникам достаточно всего одной, чтобы присвоить деньги инвесторов.

Наше исследование включает следующие стадии:

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

Уязвимые генераторы

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

  • генераторы, использующие переменные блока как источник энтропии;
  • генераторы, использующие хеш предыдущего блока;
  • генераторы, использующие хеш предыдущего блока в сочетании с якобы секретным начальным значением;
  • генераторы, подверженные уязвимости с опережением транзакции (front-running).

Рассмотрим каждую категорию с примерами уязвимого кода.

 

Генератор, использующий переменные блока

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

  • block.coinbase представляет собой адрес майнера, который майнит данный блок;
  • block.difficulty — относительный показатель того, насколько сложно создать блок;
  • block.gaslimit — максимальный расход «газа» на все транзакции в блоке;
  • block.number — высота данного блока;
  • Block.timestamp — дата майнинга блока.

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

Пример 1 (0x80ddae5251047d6ceb29765f38fed1c0013004b7):
// Won if block number is even
// (note: this is a terrible source of randomness, please don’t use this with real money)
bool won = (block.number % 2) == 0;
Пример 2 (0xa11e4ed59dc94e69612f3111942626ed513cb172):
// Compute some *almost random* value for selecting winner from current transaction. 
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 всегда выдает ноль.

Некоторые контракты ошибочно интерпретируют значение выражения 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):
//Generate random number between 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.blockhash() — хеш будущего блока

Более надежный способ — использовать хеш будущего блока, например следующим образом.

  1. Игрок делает ставку, казино запоминает block.number транзакции.
  2. При втором вызове контракта игрок запрашивает у казино выигрышный номер.
  3. Казино извлекает сохраненный block.number, получает хеш блока по его номеру и затем использует хеш при генерации псевдослучайного числа.

Такой подход работает, только если выполняется одно важное требование. В документации Solidity есть предупреждение об ограниченном числе хешей блоков, которые может хранить EVM.

По соображениям масштабируемости хеши доступны не для всех блоков. Можно получить доступ к хешам только последних 256 блоков, а все остальные значения будут равны нулю.

Поэтому, если второй вызов не был сделан в пределах 256 блоков и не было проверки номера блока, псевдослучайное число будет известно заранее — это 0.

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

Продолжение доступно только подписчикам

Вариант 1. Оформи подписку на «Хакер», чтобы читать все материалы на сайте

Подписка позволит тебе в течение указанного срока читать ВСЕ платные материалы сайта. Мы принимаем оплату банковскими картами, электронными деньгами и переводами со счетов мобильных операторов. Подробнее о подписке

Вариант 2. Купи один материал

Заинтересовала информация, но нет возможности оплатить подписку? Тогда этот вариант для тебя! Обрати внимание: этот способ покупки доступен только для материалов, опубликованных более двух месяцев назад.


3 комментария

  1. Skybad

    13.04.2018 at 21:25

    а если брать параметры погода по геолокации на текущщий момент как переменные для расчета случайностей?

  2. le

    05.05.2018 at 12:41

    Skybad, кто будет источником данных о погоде? Можно ли ему доверять? Не является ли он одной из играющих сторон?

  3. ageyev

    14.06.2018 at 13:44

    Тут мы разбирали вариант в котором используется хеш блока в качестве источника рандомности: https://habr.com/post/345904/

Оставить мнение

Check Also

Разработчик малвари собрал ботнет из 18 000 уязвимых роутеров за один день

ИБ-специалисты обнаружили появление нового IoT-ботнета, который скомпрометировал более 18 …