什麼是遞歸呼叫漏洞?
究竟什麼是遞歸呼叫漏洞?
在創建智能合約、DAO 或 DAPP 時,我可以採取哪些措施來確保我不會受到攻擊?
更簡單的解釋
- 攻擊者創建一個錢包合約(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(...) }
- 攻擊者創建(或加入)一個拆分提案(2016 年 6 月 17 日攻擊中的#59),收件人地址開始設置為上面創建的錢包合約。
- 攻擊者對拆分提案投了贊成票。
- 拆分提案到期後,攻擊者呼叫 The DAO 的
splitDAO(...)
函式。一種。該
splitDAO(...)
函式呼叫錢包合約的預設值function ()
作為將乙太幣發送給接收者的一部分。灣。錢包合約預設再次
function ()
呼叫 DAOsplitDAO(...)
,從**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; } }
也可以看看:
- 追逐 DAO 攻擊者的喚醒- “在執行外部呼叫後,你不能對合約的狀態做出任何假設”,並且“不要在你的合約中使用 Solidity 的呼叫構造呼叫外部合約程式碼,只要你能避免它。如果你不能,堅持下去,並理解你在那時失去了關於契約程序流程的所有保證”。
- DAO 漏洞分析
splitDAO(...)
- 此連結提供了對攻擊的良好分析,包括攻擊者通過在其代幣餘額設置為 0 之前轉移其 DAO 代幣然後將其轉回以重複攻擊而設法重複呼叫的方法。(感謝2016 年 6 月 17 日 The DAO 攻擊中使用的第二個漏洞是什麼?@eth
)。- TheDAO 大劫案常見問題解答
- 解構 theDAO 攻擊:簡短的程式碼之旅
- 哪些賬戶參與了對 The DAO 的遞歸呼叫漏洞攻擊?
- 哪個拆分提案被用於對 The DAO 進行遞歸呼叫漏洞攻擊?
- 當沒有贊成票時,如何通過提案 #59 進行遞歸呼叫漏洞攻擊?
- 有什麼方法可以確定 DAO 攻擊者部署攻擊需要多長時間?
- 我可以通過向合約發送正常交易來執行合約功能嗎?
- 回退函式可以完成多少計算?
更多背景資訊
以下是 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 放入契約中嗎?建議一些正式的正確性證明(但仍然可能存在錯誤)。
在接下來的幾週內會有更多的建議 - 我會更新這個答案。
一些資源:
- 思考智能合約安全
- 智能合約安全
- 掃描實時乙太坊合約中的“未檢查發送”錯誤(來自2016 年 6 月 17 日“DAO”攻擊如何發生
@Roland Kofler
的回答? )。- 關於 DAO 黑客的思考(也來自2016 年 6 月 17 日“DAO”攻擊是如何發生的?
@Roland Kofler
)。