Contract-Development

什麼是遞歸呼叫漏洞?

  • November 2, 2017

究竟什麼是遞歸呼叫漏洞?

在創建智能合約、DAO 或 DAPP 時,我可以採取哪些措施來確保我不會受到攻擊?

更簡單的解釋

  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(...)
}
  1. 攻擊者創建(或加入)一個拆分提案(2016 年 6 月 17 日攻擊中的#59),收件人地址開始設置為上面創建的錢包合約。
  2. 攻擊者對拆分提案投了贊成票。
  3. 拆分提案到期後,攻擊者呼叫 The DAO 的splitDAO(...)函式。

一種。該splitDAO(...)函式呼叫錢包合約的預設值function ()作為將乙太幣發送給接收者的一部分。

灣。錢包合約預設再次function ()呼叫 DAO splitDAO(...),從**a 開始重複循環。**多於。

C。錢包合約的預設設置function ()必須確保不會拋出錯誤,因為如果超過呼叫堆棧或氣體,交易將回滾。

以下是涉及此類攻擊的The DAO 原始碼片段:

DAO.splitDAO(...):

以下程式碼中的問題是withdrawRewardFor(msg.sender);在重置跟踪接收者有權接收的付款的變數之前進行付款(語句)(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(...)函式能夠被遞歸呼叫。

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

也可以看看:


更多背景資訊

以下是 Peter Vessenes 的原始部落格文章,其中描述了 DAO 中的遞歸呼叫漏洞:更多乙太坊攻擊:Race-To-Empty 是真正的交易,並建議對這個問題進行補救。

從文章:

漏洞

這是一些程式碼;看看你能不能找到問題。

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 () {  
 // 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; }
}

發生什麼了?呼叫堆棧如下所示:

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

每次,合約都會檢查使用者的可提款餘額並將其發送出去。因此,使用者將從合約中獲得兩倍的餘額。

當程式碼解析時,無論合約被呼叫多少次,使用者的餘額都將設置為 0。

以及建議的補救措施,來自文章:

補救方法 1:正確訂購

在即將發布的升級的solidity範例中推薦的方法是使用如下程式碼:

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

補救方法 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;
}

從使用者eththrowa在 The DAO 論壇文章中的文章中,在 MKR 代幣合約中發現的錯誤也影響了 theDAO - 將允許使用者通過遞歸呼叫從 theDAO 竊取獎勵

這個錯誤: https : //www.reddit.com/r/ethereum/comments/4nmohu/from_the_maker_dao_slack_today_we_discovered_a/57 也存在於 DAO 程式碼中 - 特別是在withdrawRewardFor 函式 DAO.sol 中:

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

在 managedAccount.sol 中

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

這將允許使用者通過遞歸呼叫合約多次消耗他的權利。奇怪的是,slockit 團隊在提案部分發現了這個錯誤:

// we are setting this here before the CALL() value transfer to
// assure that in the case of a malicious recipient contract trying
// to call executeProposal() recursively money can't be transferred
// multiple times out of the DAO
p.proposalPassed = true;

但在獎勵部分錯過了它。顯然,DAO 中還沒有任何獎勵,所以這不是今天可以花錢的問題。


:在創建智能合約、DAO 或 DAPP 時,我可以採取哪些措施來確保我不會受到攻擊?

測試,審核,測試,審核,…… 與任何軟體系統一樣,漏洞可能會潛入許多潛在領域。它的價值越高,攻擊者對它的興趣就越大。

來自乙太坊部落格CRITICAL UPDATE Re: DAO Vulnerability

合約作者應注意 (1) 非常小心遞歸呼叫錯誤,並聽取乙太坊合約程式社區的建議,這些建議可能會在下週發布,以減少此類錯誤,以及 (2) 避免創建包含以下內容的合約價值超過 1000 萬美元,但子代幣合約和其他系統除外,其價值本身由乙太坊平台之外的社會共識定義,如果出現錯誤,可以通過社區共識輕鬆“硬分叉” (例如 MKR),至少在社區獲得更多的錯誤緩解經驗和/或開發更好的工具之前。

reddit 執行緒我們可以請不要在沒有正式正確性證明的情況下再將 100m 放入契約中嗎?建議一些正式的正確性證明(但仍然可能存在錯誤)。

在接下來的幾週內會有更多的建議 - 我會更新這個答案。

一些資源:

引用自:https://ethereum.stackexchange.com/questions/6176