Уязвимости смарт-контрактов и способы их эксплуатации

11 октября 12:38

Ethereum – платформа, основанная на Blockchain, для создания распределенных онлайн-приложений, работающих на базе смарт-контрактов. Реализация представляет собой единую децентрализованную виртуальную машину. Solidity — объектно-ориентированный, предметно-ориентированный язык программирования самовыполняющихся контрактов для платформы Ethereum. Смарт-контракт представляет собой небольшую программу, написанную на Solidity для автоматизации разного рода задач: сбор средств для стартапа, распределение активов или работу других онлайн-приложений (финансовых приложений, онлайн-казино, систем голосования и т.д.).  Как и любые приложения, смарт-контракты могут содержать в себе уязвимости, позволяющие похитить средства.

Reentrancy

Данная уязвимость стала широко известна после краха проекта TheDAO в 2016. В ходе эксплуатации уязвимости злоумышленникам удалось вывести ~3.5 млн. ETH (порядка 53 млн. долларов на то время). Суть уязвимости заключается в том, что функция вывода vulnWithdraw содержит функцию msg.sender, уязвимую для повторного вызова.

contract Reentrancy {

function vulnWithdraw(uint _amount) {

require(balances[msg.sender] >= _amount);

msg.sender.call.value(_amount)();

balances[msg.sender] -= _amount; } }

Когда средства посылаются на адрес, находящийся по значению msg.sender, при помощи функции низкого уровня call(), контракт становится уязвим для повторного вызова и злоумышленник получает в 2 раза больше средств, чем положено. Действия повторяются, пока на атакуемом контракте совсем не останется средств.

Эксплойт:

import “Reentrancy.sol”

contract exploitReentrancy {

Reentrancy public reentrancy;

function withdrawFromReentrancy() {

reentrancy.withdrawSomeMoney(1000); }

function () payable { 

reentrancy.withdrawSomeMoney(1000); } }

Как защититься:

  • Не использовать функцию низкого уровня call. Если использование call неизбежно, то необходимо производить внутреннюю работу, например, проверку баланса
  • Для отправки средств лучше использовать функции send и trance, так как они лишены уязвимости Reentrancy

Denial of Service

Отказ в обслуживании – множество уязвимостей, приводящих к неработоспособности смарт-контракта.

contract DoS {

function give_presents(address[] _winners) private {

uint len = _winners.length;

for (uint i=0; i<n; i++) {

require(_winners[i].send(10)); } } }

Если рассмотреть данный контракт с точки зрения того, что победителей может быть неограниченное количество (или просто очень много) и выигрыш выплачивается одновременно, то для выполнения всего цикла n-раз может понадобиться очень много gas, и если это значение превысит количество ресурсов gas в блоке, то транзакция не будет выполнена. Следовательно, весь выигрыш останется в сети в замороженном состоянии.

Как защититься:

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

Access Control

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

function initContract() public {

owner = msg.sender; }

Например, в вышеописанной функции не производится проверка того что владелец контракта уже вызван.

Как защититься:

  • Внимательно относиться к объявлению спецификаторов видимости

Arithmetic Issues

Уязвимости целочисленного переполнения, как и в других языках, возникают из‐за ограниченного размера памяти, выделенного на переменную.

function withdraw(uint _amount) {

require(balances[msg.sender] — _amount > 0);

msg.sender.transfer(_amount);

balances[msg.sender] -= _amount; }

Вышеописанная функция из-за отсутствия проверки целостности нижней границы позволяет вывести неограниченное количество токенов.

Второй пример описывает ситуацию, когда результатом арифметической операции над двумя целыми беззнаковыми числами является беззнаковое число. То есть в нашем случае условие под if никогда не будет true и, следовательно, посты не будут удаляться.

function votes(uint postId, uint upvote, uint downvotes) {

if (upvote — downvote < 0) {

deletePost(postId) } }

Как защититься:

  • Использовать библиотеку SafeMath от OpenZeppelin

Bad Randomness

Ethereum спроектирован на детерминированном алгоритме, поэтому получить случайность прямо внутри затруднительно.

function play() public payable {

require(msg.value >= 1 ether);

if (block.blockhash(blockNumber) % 2 == 0) {

msg.sender.transfer(this.balance); }  }

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

Как защититься:

  • Не использовать в качестве энтропии внутренние переменные контракта (даже private), хеш предыдущих или будущих блоков, переменные блока

 

 

Литература

  1. Документация “Decentralized Application Security Project (or DASP) Top 10 of 2018”, [Электронный ресурс, режим доступа: http://www.dasp.co ]
  2. Доклад компании Positive Technologies “Initial Coin Offering. Угрозы информационной безопасности”, 2018, 8 с. [Электронный ресурс, режим доступа: https://www.ptsecurity.com/upload/corporate/ru-ru/analytics/ICO-Threats-rus.pdf ]
  3. Официальная документация по языку Solidity от Ethereum Foundation [Электронный ресурс, режим доступа: https://solidity.readthedocs.io/en ]
  4. К. Даннен, Введение в Ethereum и Solidity, 2018, 90 c., «Самиздат»