時間:2023-06-18|瀏覽:235
最近我寫了一些安全分析文章,可能之后還會發(fā)一些其他的文章。
基礎(chǔ)知識:
1. 跨合約調(diào)用
智能合約之間的調(diào)用本質(zhì)上是外部調(diào)用,可以使用 message.call 或者創(chuàng)建智能合約對象的形式進(jìn)行調(diào)用。
例如,合約1調(diào)用合約2的某個方法:
bytes4 methodId = bytes4(keccak256("increaseAge(string,uint256)")); returnAddr.call(methodId, "jack", 1);
還可以通過已知合約地址的方式獲取合約對象:
Contract1 c = Contract1(AddressOfContract1); c.foo(); // 跨合約調(diào)用
2. 智能合約發(fā)送 ETH
在智能合約中,可以使用代碼向某個地址(可以是人或智能合約)發(fā)送以太幣。常用的兩個方式是:
(1) 調(diào)用 send 函數(shù):
msg.sender.send(100);
(2) 使用 message.call:
msg.sender.call.value(100);
這兩個方式發(fā)送 gas 不同。在調(diào)用 send 方法時,只會發(fā)送 2300gas;而使用 message.call 會發(fā)送全部的 gas。執(zhí)行完之后,剩余的 gas 會退還給發(fā)起調(diào)用的合約。
3. fallback 函數(shù)
智能合約中可以有唯一的一個未命名函數(shù),稱為 fallback 函數(shù)。該函數(shù)不能有實(shí)參,不能返回任何值。如果其他函數(shù)都不能匹配給定的函數(shù)標(biāo)識符,則執(zhí)行 fallback 函數(shù)。
當(dāng)合約接收到以太幣但是不調(diào)用任何函數(shù)時,就會執(zhí)行 fallback 函數(shù)。如果一個合約接收了以太幣但是內(nèi)部沒有 fallback 函數(shù),那么就會拋出異常,然后將以太幣退還給發(fā)送方。
contract Sample { function payable() { // your code here } }
通常,當(dāng)我們單純使用 message.call 或者 send 函數(shù)發(fā)送以太幣給合約時,沒有指明調(diào)用合約的某個方法,這種情況下就會調(diào)用合約的 fallback 函數(shù)。
攻擊事件還原:
我們用一個簡單的模擬代碼來了解整個攻擊過程。
首先是存在漏洞的智能合約代碼 Bank:
用戶可以通過 addToBalance 方法存入一定量的以太幣到這個智能合約,通過 withdrawBalance 方法可以提現(xiàn)以太坊,通過 getUserBalance 可以獲取到賬戶余額。
注意到這里是通過 message.call 的方式來發(fā)送以太幣,所以在調(diào)用 sender 的 fallback 函數(shù)的時候我們就會有充足的 gas 來進(jìn)行循環(huán)調(diào)用。如果是 send 的方式,gas 只有 2300,稍微一操作就會耗盡 gas 拋出異常,這不夠用來進(jìn)行嵌套調(diào)用。這里是不同操作需要的 gas 數(shù)量:
出問題的是 withdrawBalance 方法,特別是修改保存在區(qū)塊鏈的 balances 的代碼是放在發(fā)送以太幣之后。攻擊代碼如下:
這里的 deposit 函數(shù)是往 Bank 合約中發(fā)送 10 wei。withdraw 是通過調(diào)用 Bank 合約的 withdrawBalance 函數(shù)把以太幣提取出來。注意看這里的 fallback 函數(shù),這里循環(huán)調(diào)用了兩次 Bank 合約的 withdrawBalance 方法。
攻擊過程如下:
(1) 假設(shè) Bank 合約中有 100 wei,攻擊者 Attack 合約中有 10 wei。
(2) Attack 合約先調(diào)用 deposit 方法向 Bank 合約發(fā)送 10 wei。
(3) 之后 Attack 合約調(diào)用 withdraw 方法,從而調(diào)用了 Bank 的 withdrawBalance 方法。
(4) Bank 的 withdrawBalance 方法發(fā)送給了 Attack 合約 10 wei。
(5) Attack 合約收到 10 wei 之后,又會觸發(fā)調(diào)用 fallback 函數(shù)。
(6) 這時,fallback 函數(shù)又調(diào)用了兩次 Bank 合約的 withdrawBalance,從而轉(zhuǎn)走了 20 wei。
(7) 之后,Bank 合約才修改 Attack 合約的 balance,將其置為 0。
通過上面的步驟,攻擊者實(shí)際上從 Bank 合約轉(zhuǎn)走了 30 wei。Bank 則損失了 20 wei。如果攻擊者多嵌套調(diào)用幾次 withdrawBalance,完全可以將 Bank 合約中的以太幣全部轉(zhuǎn)走。
復(fù)現(xiàn)過程:
給 Bank 合約 100 wei,給 Attack 合約 10 wei。
(1) 部署 Bank,分配 100 wei。
(2) 部署 Attack 。
(3) 調(diào)用 Attack 合約的 deposit 方法。
(4) 調(diào)用 Attack 合約的 withdraw 方法。
(5) 查看 Attack 合約的余額,變成了 30 wei,即竊取了 20 wei。
DAO 攻擊事件代碼分析:
在 DAO 源碼中,有 withdrawRewardFor 函數(shù):
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)) // vulnerable throw; paidOut[_account] += reward; return true; }
這里調(diào)用了 payOut 函數(shù)進(jìn)行付款:
function payOut(address recipient, uint amount) returns (bool) { if (msg.sender != owner || msg.value > 0 || (payOwnerOnly && _recipient != owner)) throw; if (_recipient.call.value(_amount)){//vulnerable PayOut(_recipient, _amount); return true; } else{ return false; } }
payOut 中直接使用的是 message.call 的方式發(fā)送以太幣,從而導(dǎo)致了嵌套漏洞。
總結(jié):
在編寫智能合約進(jìn)行以太幣發(fā)送的時候,應(yīng)該使用 send 或者 transfer 的方式,而不是使用 message.call 的方式。雖然 send 還是有一些小問題,但以后有時間再分析。DAO 事件直接導(dǎo)致了以太坊硬分叉,分為 ETH 和 ETC。可見,在區(qū)塊鏈領(lǐng)域,安全問題不容忽視,因?yàn)樗男迯?fù)難度和所造成的影響都很高,畢竟是和錢打交道,給個評論。
熱點(diǎn):區(qū)塊鏈