Новые атаки в Ethereum: «гонка на опустошение» — реальная угроза

Chriseth на github мимоходом обозначил чрезвычайно опасную атаку на контракты кошельков, которую я не рассматривал ранее. Если бы для разработчиков контрактов Ethereum существовал способ ответственного раскрытия, я бы использовал его, но, по всей видимости, его нет. Мало того, этот код был выпущен и опубликован на github достаточно давно, так что я решил быстро рассказать новости.

Новые атаки в Ethereum: «гонка на опустошение» — реальная угроза

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

ОБНОВЛЕНИЕ: Chriseth уведомил меня, что четыре дня назад основные разработчики получили предупреждения. Он сообщил, что атака не так страшна, как я думал, потому что по умолчанию функция send() дает лишь 21 тыс. единиц газа, чего будет недостаточно для оплаты вложенных вызовов. Однако при использовании функции call.value(amt).() уязвимость сохраняется. Сейчас я тестирую несколько преобразований и скоро опубликую обновление.

ОБНОВЛЕНИЕ 2: в целом эта ошибка повторного входа широко распространена, но не представляет большой угрозы в случае использования простой функции send. Я по-прежнему настоятельно рекомендую проанализировать код и обновить все функции, которые могли быть не рассмотрены, если они будут многократно вызываться во время исполнения. Благодарю Ника Джонсона и Денниса Петерсона за участие в анализе этой проблемы.

Уязвимость

Ниже приведен кусок кода. Попробуйте найти проблему.

function getBalance(address user) constant returns(uint) {
  return userBalances[user];
}
function addToBalance() {
  userBalances[msg.sender] += msg.amount;
}
function withdrawBalance() {
  amountToWithdraw = userBalances[msg.sender];
  if (!(msg.sender.call.value(amountToWithdraw)())) { throw; }
  userBalances[msg.sender] = 0;
}

Проблема в следующем: msg.sender может иметь функцию по умолчанию, которая выглядит следующим образом.

function () {
  // Вызывается уязвимым контрактом с функцией выведения средств.
  // Это будет двойной вывод.
  vulnerableContract v;
  uint times;
  if (times == 0 && attackModeIsOn) {
    times = 1;
    v.withdraw();
   } else { times = 0; }
}

Что происходит? Стек вызовов выглядит так:
    vulnerableContract.withdraw run 1
      attacker default function run 1
        vulnerableContract.withdraw run 2
          attacker default function run 2

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

Когда код завершает работу, устанавливается нулевой баланс пользователя, несмотря на то, сколько раз был вызван контракт.

Так как я написал атакующий код кошелька, он (намеренно) не будет работать в точности. Есть несколько предостережений и вещей, о которых нужно помнить. Однако надеюсь, что это посеет некоторую обеспокоенность в вашем сердце.

Предостережение: функция send непредумышленно неуязвима для атаки гонки на опустошение (Race-To-Empty)

Благодаря разработчикам на языке Solidity, немедленно принявшим участие в решении данной проблемы, по-видимому, нам повезло с наиболее распространенным способом перевода средств. Функция send — это типичный способ передачи денег по контракту. Если вы читаете мой блог, то знаете, что send имеет некоторые проблемы. В данном случае, однако, она обеспечивает некоторое полезное снижение риска, так как по умолчанию эта функция дает лишь 21 000 единиц газа, чего недостаточно для поддержки вложенных вызовов для выведения средств.

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

Подход к снижению риска 1: вызывайте функции в правильном порядке

Рекомендуемый подход, который скоро будет опубликован в обновленных примерах на Solidity, заключается в использовании кода, подобного следующему:

function withdrawBalance() {
  amountToWithdraw = userBalances[msg.sender];
  userBalances[msg.sender] = 0;
  if (amountToWithdraw > 0) {
    if (!(msg.sender.send(amountToWithdraw))) { throw; }
  }
}

Обратите внимание, что теперь userBalances сбрасывается до вызова send. Это решение лишено недостатков; оно работает отлично. Когда кошелек в гонке на опустошение вызывает эту функцию во второй раз, код правильно предупреждает, что баланс пользователя равен 0.

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

Подход к снижению риска 2: взаимные исключения

Рассмотрите теперь этот код.

function withdrawBalance() {
  if ( withdrawMutex[msg.sender] == true) { throw; }
  withdrawMutex[msg.sender] = true;
  amountToWithdraw = userBalances[msg.sender];
  if (amountToWithdraw > 0) {
    if (!(msg.sender.send(amountToWithdraw))) { throw; }
  }
  userBalances[msg.sender] = 0;
  withdrawMutex[msg.sender] = false;
}

Обратите внимание, что здесь мы можем выполнять обнуление userBalances в интуитивной части кода. Это обходится нам в дополнительное количество газа, возможно в районе 10 тыс., так как мы изменяем переменную.

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

Peter Vessenes

Категория: 
Безопасность
10
Средняя: 10 (1 оценка)
0
Ваша оценка: Нет
1238 / 0
Аватар пользователя admin
Публикацию добавил: admin
Дата публикации: ср, 06/15/2016 - 22:31

Что еще почитать:

Добавить комментарий