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
Комментарии (12)
Delics
24.11.2018 10:22По-моему, вся проблема контрактов из примеров в том, что у них всего один флаг отправки, хотя отправок две. А когда в одном из примеров они всё-таки ввели два флага, то привязали их к одной проверке.
Очевидное решение:
if (gameHasEnded) { if (!prizePaidOut && winner.send(1000)) { prizePaidOut = True; } if (!loserPaidOut && loser.send(10)) { loserPaidOut = True; } if (!prizePaidOut && !loserPaidOut) { throw; } }
Tsvetik
Кому задачку?
В некоторых смарт-контрактах ставят запрет на вызов функции другим смарт-контрактом. Делают это двумя способами. Один из этих способов не работает.
Какие это способы?
Какой из них не работает?
Почему?
ReklatsMasters
Знаю только, что так можно проверить, что адрес это контракт. Если длина больше 0, то контракт.
Tsvetik
Да, это один из способов и он как раз не работает. Есть второй.
Luxo
а почему не работает extcodesize?
Tsvetik
Создайте контракт с 10 эфира с генератором случайных чисел на основе BLOCKHASH или TIMESTAMP. Защитите платящую функцию с помощью extcodesize, и тогда я покажу. Можете даже исходный код не публиковать. Только адрес
Luxo
Слипнется
ответ: extcodesize возвращает ноль для контракта, который создаётся прямо сейчас
Tsvetik
+1