在現實中發送乙太重入攻擊 - 備份功能如何將消息回調到目前合約?
以下引自solidity docs:
pragma solidity ^0.4.0; // THIS CONTRACT CONTAINS A BUG - DO NOT USE contract Fund { /// Mapping of ether shares of the contract. mapping(address => uint) shares; /// Withdraw your share. function withdraw() { if (msg.sender.send(shares[msg.sender])) shares[msg.sender] = 0; } }
問題是:**乙太幣轉賬總是包括程式碼執行,所以接收方可能是一個回調撤回的合約。**這將讓它獲得多次退款,並基本上取回合約中的所有乙太幣。
為避免重入,您可以使用 Checks-Effects-Interactions 模式,如下所述:
pragma solidity ^0.4.11; contract Fund { /// Mapping of ether shares of the contract. mapping(address => uint) shares; /// Withdraw your share. function withdraw() { var share = shares[msg.sender]; shares[msg.sender] = 0; msg.sender.transfer(share); } }
突出顯示的部分是我有疑問的地方:我確實理解理論上存在重入攻擊,但考慮到備份功能有 2300 氣體限制(連結)這一事實,這不足以使消息回調到目前合約中。那麼在現實中,重入攻擊是如何起作用的呢?有高手能舉個例子嗎?謝謝!
更新
send
在 2016年9 月之前transfer
,預設將所有可用的 gas 傳遞給 CALL op。這在此送出https://github.com/ethereum/solidity/commit/9ca7472089a9f4d8bfec20e9e55c4f7ed2fb502e的 Solidity 編譯器中已更改。// 首先手動提供氣體津貼,因為我們可能會發送零乙太幣。
// 如果我們發送超過零的乙太幣,將被清零。
上面的註釋意味著如果你發送 0 乙太幣,編譯器會為 CALL 操作顯式地設置氣體為 2300。否則,編譯器會為 CALL 操作設置 0 gas,在這種情況下,EVM 將添加 2300 gas 津貼,如Subtleties頁面中所述。
將所有可用氣體添加到外部函式呼叫的程式碼在 ExpressionCompiler::appendExternalFunctionCall() 中:
if (_functionType.gasSet()) m_context << dupInstruction(m_context.baseToCurrentStackOffset(gasStackPos)); else if (m_context.experimentalFeatureActive(ExperimentalFeature::V050)) // Send all gas (requires tangerine whistle EVM) m_context << Instruction::GAS; else { // send all gas except the amount needed to execute "SUB" and "CALL" // @todo this retains too much gas for now, needs to be fine-tuned. u256 gasNeededByCaller = eth::GasCosts::callGas + 10; if (_functionType.valueSet()) gasNeededByCaller += eth::GasCosts::callValueTransferGas; if (!existenceChecked) gasNeededByCaller += eth::GasCosts::callNewAccountGas; // we never know m_context << gasNeededByCaller << Instruction::GAS << Instruction::SUB; }
該文件寫於 2016 年 6 月https://github.com/ethereum/solidity/commit/2df142c49618138ba7f38f32a76022caecc68abb>。這是修復它的拉取請求<https://github.com/ethereum/solidity/pull/3197
上一個答案
這是證明可以進行重入呼叫的事務跟踪:https ://rinkeby.etherscan.io/vmtrace?txhash=0x2e77009bda0fc9c07a01a4589d9b426382521c6e04b84008ffda637a4268824f
在步驟 183 上執行 CALL 操作,花費 700 gas:
步驟 PC 操作 Gas GasCost 深度
$$ 183 $$ 118 致電 1189 1182 3 $$ 184 $$ 0 推1 482 3 4
它只花費 700 gas 的原因是因為呼叫時沒有傳遞任何值。
正如乙太坊 wiki 中的Subtleties頁面所解釋的:
CALL 有一個多部分的 gas 成本:
- 700基地
- 如果值非零,則增加 9000
- 如果目標帳戶尚不存在,則額外增加 25000(注意:零餘額和不存在之間存在差異!)
對應的合約程式碼:
pragma solidity ^0.4.8; contract Fund { bool mutex = false; function withdraw() { if (!mutex) { mutex = true; msg.sender.send(1); mutex = false; } } // deposit some funds for testing function deposit() payable {} } contract Attacker { Fund f; function Attacker(address fund) payable { f = Fund(fund); } function attack() { f.withdraw(); } function () payable { f.withdraw(); } }
請注意,雖然 Solidity 文件中的原始合約容易受到重入攻擊,但它不能被利用來多次發送價值,因為第二次嵌套
send
呼叫會耗盡 2300 氣體津貼。這就是為什麼在我的範例中我必須使用互斥鎖。另請注意,在 DAO hack
call
方法中使用了將所有可用氣體傳遞給嵌套呼叫的方法,這與send
預設情況下僅傳遞 2300 津貼不同。