Уязвимости смарт-контрактов Etherium. Примеры кода +1


Данным постом начинаю цикл статей на тему безопасности смарт-контрактов Ethereum. Считаю эту тему весьма актуальной, так-как количество разработчиков лавинообразно растет, а уберечь от «граблей» — некому. Пока — переводы…

1. Сканирование Live Ethereum контрактов на ошибку «Unchecked-Send»


Оригинал — Scanning Live Ethereum Contracts for the «Unchecked-Send...»


Авторы: Zikai Alex Wen и Andrew Miller

Программирование смарт-контрактов в Ethereum, как известно, подвержено ошибкам [1] . Недавно мы увидели, что несколько
высококлассных смарт-контрактов, таких как King of the Ether и The DAO-1.0, содержали уязвимости, вызванные ошибками программирования.

Начиная с марта 2015 года программисты смарт-контрактов были предупреждены о конкретных опасностях программирования, которые могут возникнуть, когда контракты отправляют сообщения друг другу [6]

В нескольких руководствах по программированию содержится рекомендация, как избежать распространенных ошибок (в официальных документах Ethereum [3] и в независимом руководстве от UMD [2] ). Хотя эти опасности достаточно понятны, чтобы избегать их, последствия такой ошибки являются ужасными: деньги могут быть заблокированы, потеряны или украдены.

Насколько распространены ошибки, возникающие в результате этих опасностей? Есть ли еще уязвимые, но живые контракты на block-chain Ethereum? В этой статье мы отвечаем на этот вопрос, анализируя контракты на живом block-chain Ethereum с помощью нового инструмента анализа, который мы разработали.


Что такое ошибка «unchecked-send»?


Для отправки контрактом эфира на другой адрес, самым простым способом является использование ключевого слова send. Это действует как метод, определенный для каждого объекта. Например, следующий фрагмент кода может быть найден в смарт-контракте, который реализует настольную игру.


 /*** Listing 1 ***/ 
if (gameHasEnded && !( prizePaidOut ) ) {
  winner.send(1000); // отправить выигрыш победителю
  prizePaidOut = True;
}

Проблема здесь в том, что метод send  может выполниться с ошибкой. Если он не сработает, то победитель не получит деньги, однако переменная prizePaidOut будет установлена в True.

Существуют два разных случая, когда функция winner.send() может выйти из строя. Мы разберем различие между ними позже. Первый случай заключается в том, что адрес winner — это контракт (а не учетная запись пользователя), а код этого контракта генерирует исключение (например, если он использует слишком много «газа»). Если это так, то, возможно, в этом случае это «ошибка победителя». Второй случай менее очевиден. Виртуальная машина Ethereum имеет ограниченный ресурс, называемый «callstack» (глубина стека вызовов), и этот ресурс может быть использован другим кодом контракта, который был выполнен ранее в транзакции. Если callstack  уже израсходован к моменту выполнения команды send , выполнение команды потерпит неудачу, независимо от того, как определен winner. Приз победителя будет уничтожен не по его вине! 



Как можно избежать этой ошибки?

В документации Ethereum содержится краткое предупреждение об этой потенциальной опасности [3] :"Есть некоторая опасность при использовании send — передача завершается с ошибкой, если глубина стека вызовов составляет 1024 (это всегда может быть вызвано вызывающим), и также терпит неудачу, если у получателя заканчивается «газ». Поэтому, чтобы обеспечить безопасную передачу эфира, всегда проверяйте возвращаемое значение send или даже лучше: используйте шаблон, в котором получатель изымает деньги."

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


  /*** Listing 2 ***/
if (gameHasEnded && !( prizePaidOut ) ) {
  if (winner.send(1000))
    prizePaidOut = True;
  else throw;
}

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


/*** Listing 3 ***/
if (gameHasEnded && !( prizePaidOut ) ) {
  if (winner.send(1000) && loser.send(10))
    prizePaidOut = True;
  else throw; 
}

Однако это ошибка, поскольку она вводит дополнительную уязвимость. В то время как этот код защищает winner от атаки callstack, он также делает winner и loser уязвимыми друг для друга. В этом случае мы хотим предотвратить атаку callstack, но продолжаем выполнение, если команда send по какой-либо причине не сработает.

Поэтому даже лучшая передовая практика (рекомендованная в нашем «Руководстве программиста для Ethereum и Serpent», хотя она одинаково применима к Solidity), заключается в проверке наличия ресурса callstack. Мы можем определить макрос callStackIsEmpty (), который вернет ошибку, если и только если callstack пустой.


/*** Listing 4 ***/
if (gameHasEnded && !( prizePaidOut ) ) {
  if (callStackIsEmpty()) throw;
    winner.send(1000)
    loser.send(10)
    prizePaidOut = True;  
    }

Еще лучше рекомендация из документации Ethereum — «Использовать шаблон, в котором получатель забирает деньги», является немного загадочной, но имеет объяснение. Предложение состоит в том, чтобы реорганизовать ваш код, чтобы эффект неудачи send был изолирован, и влиял только на одного получателя за раз. Ниже приведен пример этого подхода. Однако этот совет также является анти-шаблоном. Он принимает на себя ответственность за проверку callstack самим получателям, что делает вероятными попадание в одну и ту же ловушку.


/*** Listing 5 ***/
if (gameHasEnded && !( prizePaidOut ) ) {
  accounts[winner] += 1000
  accounts[loser] += 10
  prizePaidOut = True;
  }
 ...
function withdraw(amount) {
if (accounts[msg.sender] >= amount) {
  msg.sender.send(amount);
  accounts[msg.sender] -= amount;
  }
}

Многие высокоразвитые интеллектуальные контракты уязвимы. Лотерея «Король Эфира Трона» — наиболее известный случай этой ошибки [4] . Эта ошибка не была замечена, пока сумму 200 эфиров (стоимостью более 2000 долларов США по сегодняшней цене) не смог получить законный победитель лотереи. Соответствующий код в King of the Ether похож на код в листинге 2 К счастью, в этом случае разработчик контракта смог использовать несвязанную функцию в контракте в качестве «ручного переопределения» для выпуска застрявших средств. Менее скрупулезный администратор мог бы использовать ту же функцию, чтобы украсть эфир!


Продолжение Сканирование Live Ethereum контрактов на ошибку «Unchecked-Send». Часть 2




К сожалению, не доступен сервер mySQL