Что такое уязвимость рекурсивного вызова?

Что такое рекурсивная уязвимость вызова?

При создании интеллектуальных контрактов, DAO или DAPP, какие меры я могу предпринять, чтобы убедиться, что я не уязвим?

42 голоса | спросил user36100 17 J0000006Europe/Moscow 2016, 15:48:32

3 ответа


31

Упрощенное пояснение

  1. Нападающий создает контракт с кошельком ( 0xc0ee9db1a9e07ca63e4ff0d5fb6f86bf68d47b89 в атаке 17/06/2016) с функция возврата по умолчанию (или возврат) function () для вызова функции DAO splitDAO(...) несколько раз. Ниже приведена простая функция по умолчанию function ():

    function () {
       // Note that the following statement can only be called recursively
       // a limited number of times to prevent running out of gas or
       // exceeding the call stack
       call TheDAO.splitDAO(...)
    }
    
  2. Злоумышленник создает (или присоединяет) разделенное предложение (# 59 в атаке 17/06/2016) с адресом получателя begin , установленным выше договора кошелька.

  3. Голос злоумышленника Да на предложение о разделении.

  4. После истечения срока действия предложения разделения злоумышленник вызывает функцию DAO splitDAO(...).

    а. Функция splitDAO(...) вызывает функцию function () как часть отправки эфиров получателю.

    б. Функция function () вызывает снова splitDAO(...), которая повторяет цикл из a. выше.

    с. Функция function () должна гарантировать, что ошибка не будет выбрана, так как транзакции будут отменены, если уровень стека вызовов или газ превышен.


Ниже приведены фрагменты исходного кода DAO , участвующие в этом типе атаки:

DAO.splitDAO(...)

Проблема в следующем коде заключается в том, что платеж произведен (оператор withdrawRewardFor(msg.sender);) перед сбросом переменных, которые отслеживают платежи, получаемые получателем ( балансы [msg.sender] = 0; и balances[msg.sender] = 0;).

paidOut[msg.sender] = 0;


function splitDAO( uint _proposalID, address _newCurator ) noEther onlyTokenholders returns (bool _success) { ... withdrawRewardFor(msg.sender); // be nice, and get his rewards totalSupply -= balances[msg.sender]; balances[msg.sender] = 0; paidOut[msg.sender] = 0; return true; }

DAO.withdrawRewardFor(...)


function withdrawRewardFor(address _account) noEther internal returns (bool _success) { if ((balanceOf(_account) * rewardAccount.accumulatedInput()) / totalSupply < paidOut[_account]) throw; uint reward = (balanceOf(_account) * rewardAccount.accumulatedInput()) / totalSupply - paidOut[_account]; if (!rewardAccount.payOut(_account, reward)) throw; paidOut[_account] += reward; return true; }

Оператор ManagedAccount.payOut(...) отправляет эфиры в учетную запись получателя, в этом случае вызывается функция _recipient.call.value(_amount)(), которая позволяет функции function () вызываться рекурсивно.

DAO.splitDAO(...)


См. также:



Дополнительная справочная информация

Вот оригинальная запись в блоге от Peter Vessenes, в которой описана рекурсивная критическая уязвимость в DAO: Больше атак Ethereum: Race-To-Empty - реальная сделка , с предлагаемым исправлением этой проблемы.

Из сообщения:

  

Уязвимость

     

Вот какой код; посмотрите, можете ли вы найти проблему.

@eth
     

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

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;
}
     

Что происходит? Стек вызова выглядит следующим образом:

function () {  
 // To be called by a vulnerable contract with a withdraw function.
 // This will double withdraw.

 vulnerableContract v;
 uint times;
 if (times == 0 && attackModeIsOn) {
   times = 1;
   v.withdraw();

  } else { times = 0; }
}
     

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

     

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

И предлагаемые исправления, с поста:

  

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

     

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

   vulnerableContract.withdraw run 1
     attacker default function run 1
       vulnerableContract.withdraw run 2
         attacker default function run 2

и

  

Подход к восстановлению 2: Мьютекс

     

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

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



И из сообщения пользователя eththrowa в сообщении форума DAO Ошибкаобнаруженный в контракте токена MKR, также влияет на theDAO - позволит пользователям украсть награды от theDAO, вызвав рекурсивно :

  

Эта ошибка:    https://www.reddit.com/r/ethereum/comments/4nmohu /from_the_maker_dao_slack_today_we_discovered_a /57   Также присутствует в коде theDAO - особенно здесь, в функции removeRewardFor DAO.sol:

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;
}
     

и здесь в файле managedAccount.sol

if (!rewardAccount.payOut(_account, reward))
   throw;
paidOut[_account] += reward;
return true;
     

Это позволит пользователю многократно истощать свое право, рекурсивно вызывая контракт. Как ни странно, команда slockit заметила эту ошибку здесь, в разделе предложений:

function payOut(address _recipient, uint _amount) returns (bool) {
        if (msg.sender != owner || msg.value > 0 || (payOwnerOnly && _recipient != owner))
            throw;
        if (_recipient.call.value(_amount)()) {
            PayOut(_recipient, _amount);
            return true;
        } else {
            return false;
        }
    }
     

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



Q : При создании смарт-контрактов, DAO или DAPP, какие меры я могу предпринять, чтобы убедиться, что я не уязвим?

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

Из блога Ethereum КРИТИЧЕСКОЕ ОБНОВЛЕНИЕ Re: DAO Уязвимость :

  

Авторы контрактов должны позаботиться о том, чтобы (1) быть очень осторожными в отношении ошибок рекурсивного вызова и прислушиваться к советам сообщества разработчиков программного обеспечения Ethereum, которые, вероятно, будут опубликованы на следующей неделе для смягчения таких ошибок и (2) избежать создания контракты, которые имеют стоимость более чем на $ 10 млн., за исключением контрактов подлоканов и других систем, стоимость которых сама определяется социальным консенсусом вне платформы Ethereum и которая может быть легко «сорвана» через общинный консенсус если появляется ошибка (например, MKR), по крайней мере до тех пор, пока сообщество не приобретет больше опыта в области устранения ошибок и /или улучшит инструменты.

Reddit thread Можем ли мы никогда больше не ставить 100 миллионов контрактов без формального доказательства корректности? предлагают некоторые официальные доказательства корректности (но все равно могут быть ошибки).

В ближайшие несколько недель появится больше советов - я обновлю этот ответ.

Некоторые ресурсы:

ответил The Officious BokkyPooBah 17 J0000006Europe/Moscow 2016, 16:16:51
14

Если ваш код выглядит так в псевдокоде:

function do:
   if (pool has mymoney = true)
     split(mymoney) 
     pool has mymoney = false

Повторяя вызов этой функции, у вас есть своеобразное состояние гонки, когда вам разрешено тратить свои деньги дважды, три, ... ad infitum .

Исправление простое, обратное две операции:

function do:
   if (pool has mymoney = true)
     pool= pool - mymoney // 2
     split(mymoney) //1

См. это фиксацию, например, исправления

ответил Roland Kofler 17 J0000006Europe/Moscow 2016, 16:17:09
5

«Уязвимость рекурсивного вызова» - это неопределенный термин, который следует избегать , потому что он неточен и может означать 2 вещи.

Повторная атака

Вероятно, вы имеете в виду «уязвимость при повторном доступе» или «повторную атаку», что и объясняет ответ Роланда. Примечание: не все реентерабельные атаки должны быть рекурсивными (в том смысле, что вредоносному коду не нужно повторно вводить один и тот же путь: он может повторно заключить контракт через любую внешнюю функцию ).

http://forum.ethereum.org/discussion/1317/reentrant-contracts

https://github.com/LeastAuthority/ethereum-analyses /blob/master/GasEcon.md

Атака глубины вызова (больше не возможна с помощью EIP 150)

В Ethereum также возможна «атака глубины вызова» (одним из способов ее выполнения является рекурсивный вызов).

Как глубина стека атака делает send () молча неудачным?

Атака при вызове

ответил eth 20 J0000006Europe/Moscow 2016, 21:09:14

Похожие вопросы

Популярные теги

security × 330linux × 316macos × 2827 × 268performance × 244command-line × 241sql-server × 235joomla-3.x × 222java × 189c++ × 186windows × 180cisco × 168bash × 158c# × 142gmail × 139arduino-uno × 139javascript × 134ssh × 133seo × 132mysql × 132