Безопасность смарт-контрактов в блокчейне Ethereum

Безопасность смарт контрактов

Количество смарт-контрактов в блокчейне Ethereum только за первую половину 2018 года выросло в два раза по сравнению с 2017-м. Соответственно, растет и множество уязвимостей, векторов атак на децентрализованные приложения. В сегодняшней статье мы попробуем упорядочить уязвимости аналогично OWASP Top 10.

РЕКОМЕНДУЕМ:
Создание блокчейн проекта

Уже обнаружено множество уязвимых контрактов, которые доступны для взаимодействия и по сей день. И конечно, совершались атаки: самыми крупными хищениями стали 30 миллионов долларов из Parity и 53 миллиона долларов из DAO. И лишь в марте 2018 года организация NCC Group представила спецификацию уязвимостей децентрализованных приложений DASP (Decentralized Application Security Project) Top10.

Для начала давайте вспомним, как устроены смарт-контракты в блокчейне Ethereum. В Ethereum существует два типа аккаунтов: внешние (аккаунты пользователей) и аккаунты контрактов, которые принято называть смарт-контрактами. Их различие состоит в том, что аккаунт контракта управляется только с помощью ассоциированного с ним программного кода, который выполняется на EVM (Ethereum Virtual Machine). Каждый смарт-контракт имеет свое хранилище и свою память.

Любое действие в блокчейне Ethereum выполняется с помощью транзакций: отправка ether с одного аккаунта на другой, создание контракта, обращение к функции контракта. Причем инициировать транзакции могут только внешние аккаунты, а контракты могут создавать транзакции только под действием полученных ими транзакций. За каждую транзакцию взимается комиссия, для этого введена специальная единица — gas. Комиссия рассчитывается как произведение цены gas и количества gas.

Пишутся контракты преимущественно на языке Solidity, который компилируется в байт-код и исполняется в EVM на всех узлах сети. На Solidity контракт выглядит как класс со своими методами и переменными. Обращаться к контракту можно, используя его ABI (Application Binary Interface).

А теперь давайте подробно рассмотрим каждый тип уязвимостей в смарт-контрактах и дадим оценку спецификации DASP Top 10.

Reentrancy

Первое место в списке занимает уязвимость типа Reentrancy, также известная как рекурсивный вызов. Проблема кроется в том, что уязвимый контракт совершает вызов к другому контракту, при этом внешний контракт может делать ответный вызов функций уязвимого контракта внутри начального вызова. Рассмотрим простой контракт-кошелек, в котором пользователи могут хранить свой ether:

С первого взгляда найти уязвимость сложно, выглядит все логичным: функция withdrawSomeMoney() проверяет, что на счету аккаунта достаточно средств, затем отправляет их с помощью функции msg.sender.call.value() и, наконец, списывает отправленный ether со счета пользователя. Теперь рассмотрим атакующий контракт:

Контракт Xakep внутри функции withdrawFromVuln() вызывает функцию withdrawSomeMoney() контракта Vuln. Но при отправке токенов функцией msg.sender.call.value() вызывается fallback-функция контракта Xakep. Это функция без названия, которая в данном случае используется для получения контрактом ether, поэтому она отмечена модификатором payable. Внутри нее контракт Xakep снова вызывает функцию withdrawSomeMoney(), причем важно заметить, что с баланса аккаунта в кошельке ether еще не списался, значит, проверка достаточности баланса успешна, и мы снова попадаем в fallback-функцию. Так происходит, пока на контракте Vuln совсем не останется средств. Эксплуатация данной уязвимости привела DAO к потере около 50 миллионов долларов.

Безопасно перевести токены можно при помощи функции transfer(), но если все же необходимо использовать вызов аккаунта, то нужно сначала обновить баланс аккаунта, затем совершать вызов.

Управление доступом (Access Control)

Есть способы стать владельцем чужого контракта или, наоборот, заставить пользователя авторизоваться в необходимом злоумышленнику контракте. Все это уязвимости контроля доступа. Для функций в Solidity существуют спецификаторы видимости: private, publuc, internal, external. Функция без спецификатора автоматически считается public, то есть доступна для вызова отовсюду.

Private-функции и глобальные переменные доступны исключительно внутри своего контракта. К public-функциям и глобальным переменным можно обратиться как изнутри контракта, так и снаружи. Internal-функции и глобальные переменные могут быть доступны изнутри текущего контракта или из контракта-наследника. External-функции могут быть вызваны из других контрактов или через транзакции и не могут быть вызваны изнутри текущего контракта.

Использование неподходящих спецификаторов или неиспользование их совсем может привести к неблагоприятным последствиям. Например, вызов функции смены владельца контракта со спецификатором public позволяет любому аккаунту стать владельцем контракта. Также к данному типу уязвимостей относится использование tx.origin для определения аккаунта, вызвавшего контракт. Рассмотрим такой пример:

Контракт Wallet представляет собой кошелек, все средства из которого может перевести на указанный адрес только его владелец, заданный при создании контракта. Обрати внимание на строку проверки владельца в функции withdrawAll. Теперь создадим атакующий контракт.

Используя социальную инженерию, можно заставить владельца контракта Wallet отправить контракту TrueXakep некоторое количество ether. В этом случае исполнится fallback-функция, из которой происходит вызов функции withdrawAll контракта Wallet. А теперь разберемся, какое значение имеет tx.origin. tx.origin — это глобальная переменная в Solidity, которая принимает значение адреса исходного аккаунта, вызвавшего функцию или отправившего транзакцию. В нашем случае tx.origin — это адрес владельца контракта Wallet, так как именно он породил последовательность вызовов. Таким образом, проверка успешно проходит, и весь ether с кошелька переводится злоумышленнику.

РЕКОМЕНДУЕМ:
Создание майнера криптовалюты Electroneum

Чтобы такого не происходило, необходимо использовать переменную msg.sender для определения аккаунта, вызвавшего функцию. msg.sender — это адрес аккаунта, который непосредственно сделал вызов или отправил транзакцию. В данном случае это адрес контракта TrueXakep.

К этой категории уязвимостей также относится неправильное использование delegatecall. Это низкоуровневая функция, которая позволяет исполнять функции стороннего контракта в контексте вызывающего контракта. То есть при исполнении функции стороннего контракта используется хранилище вызывающего контракта, и значения msg.call и msg.value остаются первоначальными. Вторая атака на Parity основана на использовании delegatecall и повторной инициализации контракта-библиотеки, в результате чего пользователь смог стать его владельцем и удалить контракт. Все средства в Parity остались замороженными, так как функция вывода средств вызывалась из удаленного контракта. Сообщение о том, что пользователь случайно удалил контракт, находится на GitHub Parity.

Сообщение об удалении контракта Parity
Сообщение об удалении контракта Parity

Арифметические особенности (Arithmetic Issues)

Уязвимости целочисленного переполнения, как и в других языках, возникают из-за ограниченного размера памяти, выделенного на переменную. Максимальное значение для переменной типа uint (uint256) равно 2256−1, минимальное — 0. Выйти за эти границы не получится: значение либо обнулится (при переполнении вверх), либо станет максимальным (при переполнении вниз).

Мы не будем рассматривать простые примеры, в которых арифметические действия выполняются без проверок. Рассмотрим уязвимость в функции batchTransfer, которую используют многие реальные контракты.

Функция batchTransfer() перечисляет заданное в _value количество ether на адреса из массива _receivers. Обрати внимание на строку вычисления amount, переполнение в которой никак не проверяется. Если задать значение _value равным 2256 / _receivers.length, то в результате переполнения amount примет значение 0. Обе проверки далее пройдут успешно, в том числе и проверка того, что баланс отправителя больше amount. В итоге балансы получателей будут пополнены на величину _value. Если получателей было два, то каждый из них получит по

токенов.

Для исключения подобных уязвимостей рекомендуется использовать библиотеку SafeMath от OpenZeppelin.

Отсутствие проверки возвращаемых значений низкоуровневых вызовов (Unchecked Return Values For Low Level Calls)

Существует три способа отправить ether на сторонний аккаунт: transfer(), send() и call(). Последние две функции при возникновении ошибки возвращают false, но транзакция продолжает выполняться. Функция transfer() в таком случае откатит транзакцию. Также необходимо отслеживать возвращаемое значение низкоуровневых функций callcode(), delegatecall(). Они тоже не откатывают транзакцию, если возвращается результат false. Рассмотрим уязвимость на примере:

Функция из примера отправляет подарки победителям из массива _winners. Поскольку возвращаемое значение функции send не проверяется, может случиться так, что не все победители получат свои подарки. Поэтому рекомендуется для всех переводов средств использовать функцию transfer().

Отказ в обслуживании (Denial of Service)

К этой категории относят множество уязвимостей, которые могут привести к неработоспособности смарт-контракта. Преобразуем функцию give_presents таким образом, чтобы проверялось возвращаемое функцией send значение.

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

А теперь представим, что количество победителей не ограничено и выигрыш выплачивается всем одновременно. Злоумышленник может зарегистрировать много аккаунтов, которые станут победителями. Для выполнения вызовов в цикле может потребоваться большое количество gas, которое превысит лимит gas в блоке, и такая транзакция не будет выполнена. Для гарантированной выплаты выигрышей всем победителям следует разделить большой цикл на несколько, каждый из которых будет выполняться в отдельной транзакции.

Проблема псевдослучайных чисел (Bad Randomness)

Блокчейн Ethereum основан на детерминированном алгоритме, поэтому получение случайных чисел — задача трудная. Источниками псевдослучайных чисел НЕ могут выступать:

  • переменные контракта, даже отмеченные спецификатором private;
  • переменные блока;
  • хеш предыдущего и даже будущего блока.

Во всех этих вариантах псевдослучайные числа могут быть предугаданы. Более надежные способы генерации псевдослучайных чисел — использование внешних источников, алгоритм Signidice, подход Commit — Reveal.

Опережение (Front-Running)

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

Контракт представляет собой конкурс на поиск числа, хеш от которого меньше заданного значения. Допустим, ты нашел подходящее решение и отправил транзакцию с ответом. Нечестный игрок может подсмотреть ваш ответ в пуле транзакций и отправить такой же. При отправке транзакции нужно указать цену gas, которую вы готовы заплатить, чтобы ваша транзакция выполнилась. Чем выше цена gas, тем больше вероятность транзакции быть добавленной в блок раньше. Таким образом, злоумышленник, выставив цену gas выше, сможет выиграть конкурс.

РЕКОМЕНДУЕМ:
Создание биткойн-майнера на Java

Частично решит проблему добавление в контракт ограничения на цену gas, но майнеры все равно смогут манипулировать порядком транзакций. Более правильным, но сложным решением будет схема Commit — Reveal, как и в предыдущем пункте. В данном случае пользователям предлагается сначала отправлять хеш от найденного числа. После добавления этой транзакции в блок пользователь отправляет найденное число. Результат проверяется, награждается победитель.

Зависимость от времени (Time manipulation)

В Solidity переменная now соответствует глобальной переменной block.timestamp, которую задает майнер, поэтому использовать ее в качестве источника энтропии нельзя.

Контракт представляет собой гибрид аукциона и лотереи: участники отправляют ставки, выигрывает тот, время ставки которого удовлетворяет определенному условию. Для гарантированного выигрыша майнер может установить подходящее значение block.timestamp и смайнить блок со своей выигрышной транзакцией.

Манипулирование длиной адреса (Short Address Attack)

Для вызова функций контрактов используется ABI (Application Binary Interafce) — бинарный интерфейс приложения, согласно которому вызов функции представляется в виде последовательности байтов. Первые четыре байта — это начало хеша keccak256 от подписи функции (ее название и типы входных данных), далее идут значения входных параметров.

Адрес в Ethereum состоит из 20 байт. Для осуществления атаки злоумышленнику нужно сгенерировать адрес, который заканчивается на нулевой байт, например abcdabcdabcdabcdabcdabcdabcdabcdabcdab00. В обычном случае вызов функции на перевод 1 ether будет выглядеть так (вертикальной чертой разделены составляющие последовательности):

Но если указать адрес abcdabcdabcdabcdabcdabcdabcdabcdabcdab без последнего байта, то недостающий байт адреса будет взят из следующего аргумента (amount), а в конец EVM допишет нулевой байт:

Получается, вместо 1 ether жертва отправит 0x100=256 ether. Защититься от такой атаки поможет только тщательная проверка параметров перед загрузкой их в блокчейн.

Ненайденное (Unknown unknowns)

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

Заключение

Рассмотренная классификация, с одной стороны, объединяет в категорию множество разных уязвимостей, с другой — не учитывает некоторые из них совсем. Например, уязвимости контроля доступа следовало бы разделить на отдельные типы, особенно связанные с использованием delegatecall для работы с контрактами-библиотеками. В эту группу можно добавить уязвимости прокси-контрактов, которые перенаправляют вызов на контракты-библиотеки. Также в классификации не указаны проблема неинициализированных переменных в хранилище и возможность отправки ether смарт-контракту, который этого не ожидает.

РЕКОМЕНДУЕМ:
Как запустить свое ICO

Понравилась статья? Поделиться с друзьями:
Добавить комментарий