Ethereum завоевал огромную популярность как платформа для ICO. Но она применима не только для создания токенов стандарта ERC-20. Блокчейн Ethereum можно использовать в онлайн-рулетке, лотереях и карточных играх. Подтвержденные транзакции блокчейна нельзя подделать — технология децентрализована и прозрачна, — но код умных контрактов может быть уязвим. Одна из проблем — уязвимые генераторы псевдослучайных чисел, ГПСЧ. Давай разберем типовые ошибки реализации ГПСЧ в азартных играх на базе Ethereum.
Ethereum позволяет исполнять тьюринг-полные программы, обычно написанные на языке Solidity, поэтому основатели платформы называют ее «мировым суперкомпьютером». Благодаря прозрачности Ethereum весьма удобно использовать в сфере азартных онлайн-игр, где очень важно доверие пользователя.
Однако процессы блокчейна Ethereum предсказуемы, что создает сложности для тех, кто решил написать собственный генератор псевдослучайных чисел — неотъемлемую часть любой азартной игры. Мы решили исследовать умные контракты, чтобы оценить надежность ГПСЧ на Solidity и выявить распространенные ошибки проектирования, которые приводят к уязвимостям и дают тем самым возможность предсказать случайные числа.
В 2017 году специалисты Positive Technologies реализовали проекты по анализу безопасности и защите от киберпреступников как процедуры ICO, так и внедрения блокчейн-технологий. Результаты исследования оказались безрадостными: уязвимости в смарт-контрактах были выявлены в 71% проектов, в 23% проектов были выявлены недостатки, позволяющие атаковать инвесторов. В каждом проекте ICO в среднем содержалось пять уязвимостей. Однако злоумышленникам достаточно всего одной, чтобы присвоить деньги инвесторов.
Наше исследование включает следующие стадии:
Анализ выявил четыре категории уязвимых ГПСЧ:
Рассмотрим каждую категорию с примерами уязвимого кода.
Существует ряд переменных блока, которые могут быть по ошибке использованы как источники энтропии:
block.coinbase
представляет собой адрес майнера, который майнит данный блок;block.difficulty
— относительный показатель того, насколько сложно создать блок;block.gaslimit
— максимальный расход «газа» на все транзакции в блоке;block.number
— высота данного блока;Block.timestamp
— дата майнинга блока.Всеми перечисленными переменными могут манипулировать майнеры, поэтому их нельзя использовать как источник энтропии. Более того, очевидно, что эти переменные одинаковы в пределах одного блока. И если контракт злоумышленника вызывает контракт жертвы внутренним сообщением, один и тот же генератор в обоих контрактах выдаст одинаковое значение.
// 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;
// Compute some *almost random* value for selecting winner from current transaction. var random = uint(sha3(block.timestamp)) % 2;
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.number
позволяет узнать высоту данного блока. Когда майнер добавляет транзакцию в блок, которая выполняет код контракта, известен block.number
будущего блока этой транзакции и контракту доступно его значение. Однако в момент выполнения транзакции в EVM хеш создаваемого блока еще неизвестен по очевидным причинам, и EVM всегда выдает ноль.
Некоторые контракты ошибочно интерпретируют значение выражения block.blockhash(block.number)
. В таких контрактах хеш блока считается известным во время выполнения транзакции и используется как источник энтропии.
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); }
function random(uint64 upper) public returns (uint64 randomNumber) { _seed = uint64(sha3(sha3(block.blockhash(block.number), _seed), now)); return _seed % upper; }
Некоторые контракты используют генераторы, основанные на хеше последнего блока. Понятно, что такой подход тоже уязвим: злоумышленник может создать контракт-эксплоит с таким же значением генератора, чтобы вызвать атакуемый контракт через внутреннее сообщение. «Случайные» числа обоих контрактов совпадут.
//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; }
Более надежный способ — использовать хеш будущего блока, например следующим образом.
1. Игрок делает ставку, казино запоминает block.number транзакции.
2. При втором вызове контракта игрок запрашивает у казино выигрышный номер.
3. Казино извлекает сохраненный block.number, получает хеш блока по его номеру и затем использует хеш при генерации псевдослучайного числа.
Такой подход работает, только если выполняется одно важное требование. В документации Solidity есть предупреждение об ограниченном числе хешей блоков, которые может хранить EVM.
По соображениям масштабируемости хеши доступны не для всех блоков. Можно получить доступ к хешам только последних 256 блоков, а все остальные значения будут равны нулю.
Поэтому, если второй вызов не был сделан в пределах 256 блоков и не было проверки номера блока, псевдослучайное число будет известно заранее — это 0.
Самый нашумевший случай эксплуатации этой уязвимости — взлом лотереи SmartBillions. Контракт неверно проверял возраст block.number, что привело к тому, что игрок выиграл 400 ETH: после создания 256 блоков он запросил предсказуемый выигрышный номер, который отправил в первой транзакции.
Материалы из последних выпусков можно покупать отдельно только через два месяца после публикации. Чтобы продолжить чтение, необходимо купить подписку.
Подписка позволит тебе в течение указанного срока читать ВСЕ платные материалы сайта. Мы принимаем оплату банковскими картами, электронными деньгами и переводами со счетов мобильных операторов. Подробнее о подписке
1 год6790 р. Экономия 1400 рублей! |
1 месяц720 р. 25-30 статей в месяц |
Уже подписан?
Читайте также
Последние новости