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

  • Автор темы blacktrader
  • Дата начала
blacktrader

blacktrader

Модератор
14 Ноя 2018
6,623
96
48
Данным постом начинаю цикл статей на тему безопасности смарт-контрактов 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 К счастью, в этом случае разработчик контракта смог использовать несвязанную функцию в контракте в качестве «ручного переопределения» для выпуска застрявших средств. Менее скрупулезный администратор мог бы использовать ту же функцию, чтобы украсть эфир!

источник
 
blacktrader

blacktrader

Модератор
14 Ноя 2018
6,623
96
48
Сканирование Live Ethereum контрактов на ошибку «Unchecked-Send». Часть 2
Продолжение статьи «Сканирование Live Ethereum контрактов на ошибку «Unchecked-Send». Часть 1».


Почти год назад (в то время как Ethereum был в своем «пограничном» выпуске), популярный лотерейный контракт EtherPot [9] также пострадал от той же ошибки. Более ранняя версия BTCRelay также показала эту ошибку [7]. Несмотря на то, что в предыдущем аудите безопасности была обнаружена опасность, сначала было применено неправильное исправление [8].

Обнаружение ошибки «unchecked-send» на живом blockhain

Насколько распространены эти ошибки? Прислушиваются ли к предупреждениям? Применяются ли лучшие практики? Мы отвечаем на эти вопросы эмпирически, анализируя данные block-chain Ethereum, а также репозиторий кода Solidity, найденный на etherscrape.com. Для этого мы разрабатываем простой инструмент анализа программ, который проверяет контракт на block-chain и использует эвристику для проверки того, используется ли один из наиболее эффективных методов защиты. В листинге 2 показана первая техника защиты, как рекомендовано в документации Ethereum, которая должна проверять возвращаемое значение send и выдавать исключение. Чтобы обнаружить использование этого метода, мы используем грубое приближение: мы просто ищем, игнорируется ли возвращаемое значение send или нет.

Листинг 4 иллюстрирует вторую технику защиты, рекомендованную в руководстве UMD, которая непосредственно проверяет, заполнен ли callstack, отправив тестовое сообщение. Чтобы обнаружить эту технику, мы снова используем приблизительную аппроксимацию: мы просто проверяем, отправляется ли сообщение в дополнение к команде send .

Если ни один из этих эвристических индикаторов не присутствует, мы делаем вывод, что ни одна из рекомендаций по лучшей практике не соблюдается. Мы реализуем эти эвристики, используя простое сопоставление шаблонов с компилированным байт-кодом EVM. Более подробно о том, как мы это делаем, содержится в Приложении [12].


Сколько контрактов уязвимо?

Начнем с проверки эвристики в репозитории Etherscrape исходного кода Solidity. По состоянию на 20 марта 2016 года ретрансляция Etherscrape содержала 361 контрактную программу Solidity, 56 из которых содержали инструкцию send. Из этих программ контрактов мы предполагаем, что большинство (не менее 36 из 56) не используют ни один из методов защитного программирования.

Даже если контракт не использует ни одну из защитных технологий, он может или не может иметь реальной уязвимости. Мы вручную проверили контракты Solidity, чтобы подтвердить наличие уязвимости. Для наших целей мы рассматриваем контракт уязвимым, если его состояние может измениться, даже если команда send не cработает (поэтому мы рассмотрим уязвимый код в листинге 5). Мы подтвердили, что уязвимость присутствует в подавляющем большинстве, 32 из 36 из этих контрактов.


Точно так же наша эвристика не гарантирует правильного применения защитного программирования. Возьмем, к примеру, «WeiFund», децентрализованный open-source crowdfunding DApp. Этот контракт имеет две функции: refund () и payout (), которые обманывают нашу эвристику. Ниже приводится выдержка из refund.




function refund(uint _campaignID, uint contributionID) public {
...
receiver.send(donation.amountContributed);
donation.refunded = true;
...
if(c.config != address(0))
WeiFundConfig(c.config).refund(_campaignID, donation.contributor,
donation.amountContributed);
}

В этом коде сообщение отправляется по адресу WeiFundConfig (c.config), чтобы вызвать метод refund но только при определенных условиях. Если c.config — это нулевое значение, то контракт действительно уязвим для атаки callstack. При проверке * ни одна из программ Solidity, которые прошли нашу эвристическую проверку, фактически не применяла рекомендуемую наилучшую практику тестирования callstack напрямую. *

Затем мы обратим наше внимание на составленные контракты на живом block-chain Ethereum. Мы посмотрели снимок от 20 марта 2016 года (временная метка: 1184243). Этот моментальный снимок содержит в общей сложности 13645 цепочки блоков, которые, по-видимому, генерируются компилятором Solidity, из которых только 1618 (11,8%) включали команду send.

Из них подавляющее большинство, похоже, не использует ни одного из методов защитного программирования.


Как насчет проблемы рекурсивной гонки в TheDAO? Самый захватывающий смарт-контракт в эти дни, TheDAO [11], страдает от совершенно отдельной ошибки, которая заключается в том, что она не «безопасна для повторного использования» [13] . Это еще один (связанный, но отчетливый) вид небезопасного программирования, который также ожидался в предыдущих проверках безопасности [6] , но, по-прежнему, вероятно, многие контракты сегодня небезопасны. Будущая работа заключалась в том, чтобы сделать инструмент, который также может обнаружить такую ошибку.


Куда это все пошло не так?

Мы не ожидаем, что программирование на смарт-контрактах будет совершенно простым, по крайней мере, пока. Однако удивительно, что эта конкретная форма ошибки настолько распространена, несмотря на что она была так давно описана при развитии экосистемы Эфириума.

В докладе в 2015 году [6] была приведена эта рекомендация разработчикам Ethereum:"

В настоящее время примеры программирования, представленные в документации, недостаточны для распространения передовых методов написания безопасных контрактов и решения проблемы с газовым механизмом. Вводные учебники на C ++ часто пропускают
проверку ошибок для удобства чтения, что и привело к многочисленным ошибкам безопасности. Примеры Ethereum должны преподавать лучшие привычки. Рекомендация: предоставить еще больше примеров тщательного программирования защитных контрактов."

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


Мы считаем, что этого замечания недостаточно для документирования проблемы. Оно предлагает только неполное смягчение и описывает только один вариант опасности, потенциально вводящий в заблуждение читателя о его степени.


  • Обновление:
    неадекватность документации Solidity также была подробно проиллюстрирована Питером Весенесом. [16]
Кроме того, предупреждение, похоже, часто не учитывается. Поэтому мы считаем, что необходимо предпринять дополнительные превентивные меры.



Как может помочь Etherscrape?

Мы полагаем, что использование инструментов статического анализа, даже грубых, таких как описанное в этом сообщении, может помочь улучшить качество интеллектуальных контрактов.На Etherscrape мы интегрируем инструменты анализа, подобные этому, в наш общедоступный веб-сервис, и мы добавим ссылку на страницу инструмента, когда она будет готова. Это упростит просмотр кода интеллектуального контракта, выделив места, где могут возникнуть ошибки. Мы предполагаем, что пользователи такого смарт-контракта (например, потенциальные инвесторы в TheDAO или его предложениях) могут легко использовать такие инструменты, как проверку здравомыслия, прежде чем депонировать свои деньги. Даже нетехнические инвесторы могут привлекать разработчиков к ответственности за объяснение того, как они реагировали на проблемы, отмеченные в коде.

Etherscrape также помогает, анализируя публичный block-chain и контролируя распространенность этой ошибки, что может помочь при принятии решения о том, например, сколько средств выделять на исследования и разработку инструментов статического анализа. Кроме того, компиляторы, такие как solc, могут интегрировать такие анализы, предоставляя предупреждение программисту, когда ошибка кажется вероятной.


Рекомендуемая литература

источник