Содержание статьи
- Уязвимые генераторы
- Генератор, использующий переменные блока
- Генератор, использующий хеш блока
- Генераторы, использующие хеш предыдущего блока с закрытым начальным значением
- Генераторы, подверженные уязвимости с опережением транзакции (front-running)
- Как создать более безопасный ГПСЧ
- Внешние оракулы
- Алгоритм Signidice
- Подход Commit — Reveal
- Заключение
Ethereum позволяет исполнять тьюринг-полные программы, обычно написанные на языке Solidity, поэтому основатели платформы называют ее «мировым суперкомпьютером». Благодаря прозрачности Ethereum весьма удобно использовать в сфере азартных онлайн-игр, где очень важно доверие пользователя.
Однако процессы блокчейна Ethereum предсказуемы, что создает сложности для тех, кто решил написать собственный генератор псевдослучайных чисел — неотъемлемую часть любой азартной игры. Мы решили исследовать умные контракты, чтобы оценить надежность ГПСЧ на Solidity и выявить распространенные ошибки проектирования, которые приводят к уязвимостям и дают тем самым возможность предсказать случайные числа.
INFO
В 2017 году специалисты Positive Technologies реализовали проекты по анализу безопасности и защите от киберпреступников как процедуры ICO, так и внедрения блокчейн-технологий. Результаты исследования оказались безрадостными: уязвимости в смарт-контрактах были выявлены в 71% проектов, в 23% проектов были выявлены недостатки, позволяющие атаковать инвесторов. В каждом проекте ICO в среднем содержалось пять уязвимостей. Однако злоумышленникам достаточно всего одной, чтобы присвоить деньги инвесторов.
Наше исследование включает следующие стадии:
- Собрали 3649 умных контрактов с etherscan.io и GitHub.
- Импортировали контракты в Elasticsearch, поисковик с открытым кодом.
- С помощью веб-интерфейса Kibana с богатыми возможностями по поиску и фильтрации данных обнаружили 72 уникальные реализации ГПСЧ.
- Проанализировав вручную каждый контракт, мы обнаружили, что 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() — хеш будущего блока
Более надежный способ — использовать хеш будущего блока, например следующим образом.
- Игрок делает ставку, казино запоминает block.number транзакции.
- При втором вызове контракта игрок запрашивает у казино выигрышный номер.
- Казино извлекает сохраненный block.number, получает хеш блока по его номеру и затем использует хеш при генерации псевдослучайного числа.
Такой подход работает, только если выполняется одно важное требование. В документации Solidity есть предупреждение об ограниченном числе хешей блоков, которые может хранить EVM.
По соображениям масштабируемости хеши доступны не для всех блоков. Можно получить доступ к хешам только последних 256 блоков, а все остальные значения будут равны нулю.
Поэтому, если второй вызов не был сделан в пределах 256 блоков и не было проверки номера блока, псевдослучайное число будет известно заранее — это 0.
Самый нашумевший случай эксплуатации этой уязвимости — взлом лотереи SmartBillions. Контракт неверно проверял возраст block.number, и это привело к тому, что игрок выиграл 400 ETH: после создания 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;
}
}
Переменная pointer объявлена закрытой, то есть у других контрактов нет доступа к ее значению. После каждой игры выигрышный номер от 1 до 9 присваивался этой переменной и затем использовался как случайное смещение block.number
при получении хеша блока по номеру.
Одно из свойств технологии блокчейн — ее прозрачность, поэтому секретные данные не должны храниться в ней в открытом виде. Несмотря на то что закрытые переменные недоступны другим контрактам, есть возможность извлечь из блокчейна содержимое хранилища контракта. К примеру, в популярном в Ethereum клиенте web3 есть API-метод web3.eth.getStorageAt()
, с помощью которого можно извлечь содержимое постоянного хранилища контракта по индексам записей в нем.
В таком случае не составит труда получить значение закрытой переменной pointer из контракта и использовать ее в качестве аргумента в эксплоите:
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);
}
Генераторы, подверженные уязвимости с опережением транзакции (front-running)
Чтобы отхватить наибольшую награду, майнеры используют транзакции для создания нового блока на основе совокупного газа, полученного с каждой транзакции. Порядок выполнения транзакции в блоке определяется ценой газа. Транзакция с самой высокой ценой газа будет выполнена первой. Таким образом, манипулируя ценой, можно добиться того, что нужная транзакция выполнится раньше остальных в блоке. Это может представлять собой уязвимость в случае, когда выполнение контракта зависит от его позиции в блоке.
Рассмотрим такой пример. У лотереи есть внешний «оракул» для получения псевдослучайных чисел, который используется, чтобы определить победителя среди игроков, делающих ставки в каждом раунде. Числа передаются в незашифрованном виде. Злоумышленник может посмотреть пул неподтвержденных транзакций и дождаться числа от оракула. Как только транзакция предсказателя появляется в пуле, злоумышленник отправляет ставку с большей ценой газа. Эта транзакция отправлена последней в раунде, но она выполняется перед транзакцией оракула благодаря наивысшей цене газа, и таким образом атакующий становится победителем. Подобная техника была продемонстрирована на конкурсе ZeroNights ICO Hacking Contest.
Есть еще один пример контракта, подверженного уязвимости с опережением транзакции, — игра Last is me! Игрок покупает билет, занимает последнее место, и начинается обратный отчет таймера. Если никто не покупает билет после создания определенного количества блоков, контракт отдает выигрышное место тому, кто купил билет последним. Когда раунд заканчивается, злоумышленник следит за пулом неподтвержденных транзакций других участников и выигрывает, отправляя транзакцию с большей ценой газа.
Как создать более безопасный ГПСЧ
Существует несколько подходов к созданию более безопасных ГПСЧ в блокчейне Ethereum:
- внешний оракул;
- алгоритм Signidice;
- подход Commit — Reveal.
Внешние оракулы
Oraclize
Oraclize — сервис для децентрализованных приложений, который служит связующим звеном между блокчейном и внешней средой (интернетом). При помощи Oraclize умные контракты могут запрашивать данные из веб-API (например, курс валюты, прогноз погоды, стоимость акций). Кроме того, Oraclize может использоваться как ГПСЧ. Некоторые из исследованных контрактов использовали Oraclize, чтобы получить случайные числа от random.org.
Главный недостаток этого подхода в том, что сервис централизованный. Можем ли мы быть уверены, что демон Oraclize не изменит результат? Можно ли доверять random.org и всей его инфраструктуре? Несмотря на то что Oraclize использует сервис TLSNotary, верификация производится вне блокчейна (в случае с лотерей — после того, как объявят победителя). Лучше использовать Oraclize как источник «случайных» данных с применением доказательств Ledger proofs, которые могут быть подтверждены внутри блокчейна.
BTC Relay
BTC Relay — мост между блокчейнами Ethereum и Bitcoin. С помощью BTC Relay умные контракты блокчейна Ethereum могут запрашивать хеш будущего блока Bitcoin и использовать его как источник энтропии. Пример проекта, который использует BTC Relay как ГПСЧ, — The Ethereum Lottery.
BTC Relay не подходит для решения проблемы мотивации майнера. Здесь барьер выше, чем при использовании будущих блоков Ethereum, но только из-за более высокой цены биткойна. Так что этот подход снижает, но не устраняет вероятность мошенничества со стороны майнеров.
Алгоритм Signidice
Signidice — алгоритм, основанный на криптографических подписях. Может быть использован как ГПСЧ в умных контрактах — с двумя сторонами (игрок и казино). Алгоритм работает следующим образом.
- Игрок делает ставку, вызывая метод контракта.
- Казино видит ставку, подписывает ее закрытым ключом и отправляет подпись контракту.
- Контракт верифицирует подпись с помощью открытого ключа.
- После этого подпись используется для генерации случайного числа.
В Ethereum есть встроенная функция ecrecover()
для проверки подписей ECDSA в блокчейне. Однако алгоритм ECDSA не может быть использован в Signidice, так как казино может изменять входные параметры (в частности, параметр k) и таким образом влиять на значение конечной подписи. Реализация этой мошеннической схемы была продемонстрирована Алексеем Перцевым.
К счастью, с выходом хардфорка Metropolis появился новый оператор возведения в степень по модулю, что позволило использовать проверку подписи RSA, который, в отличие от ECDSA, не позволяет манипулировать входными данными, чтобы подобрать подпись.
Подход Commit — Reveal
Как видно из названия, данный подход состоит из двух этапов.
- Commit: стороны передают свои данные в умный контракт в зашифрованном виде;
- Reveal: стороны передают начальные значения в открытом виде, контракт подтверждает, что данные верны, начальные значения используются для генерации случайного числа.
Для грамотного применения данного подхода нельзя полагаться ни на одну из сторон. Несмотря на то что игроки не знают начальных значений владельца, владелец может быть одновременно и игроком, поэтому игроки не могут ему доверять.
Randao — более грамотное применение подхода Commit — Reveal. Этот генератор псевдослучайных чисел собирает хешированные начальные значения нескольких сторон, и каждая сторона получает вознаграждение за участие. Стороны не знают начального значения друг друга, поэтому генерируется действительно случайный результат. Однако отказ одной из сторон раскрыть начальное значение приведет к DoS.
Commit — Reveal можно совместить с использованием хеша будущего блока. В таком случае имеется три источника энтропии:
- sha3(seed1) владельца;
- sha3(seed2) игрока;
- хеш будущего блока.
Случайное число генерируется таким образом: sha3(seed1, seed2, blockhash)
. Подход Commit — Reveal решает проблему мотивации майнера: майнер определяет, публиковать ли найденный хеш в блокчейне, но не знает начальные значения владельца и игрока. Подход решает также и проблему мотивации владельца: владелец знает только начальное значение владельца, начальное значение игрока и хеш будущего блока ему неизвестны. Кроме того, подход отлично работает в том случае, если владелец и майнер — одно лицо: он выбирает хеш блока и знает начальное значение владельца, но не игрока.
Заключение
Создание безопасных генераторов псевдослучайных чисел в блокчейне Ethereum по-прежнему непростая задача. Как показало исследование, из-за недостатка готовых решений разработчики чаще используют свои инструменты реализации. Однако легко допустить ошибку при разработке, так как источники энтропии в блокчейне ограничены. При создании ГПСЧ разработчику следует убедиться, что он понимает мотивацию каждой стороны, и тогда приступить к выбору подхода.