【智能合約系列002-TheDAO悲劇重演,SpankChain重入漏洞分析】

前言

在10月8日,區塊鏈項目方SpankChain在medium上發表了一篇文章,
並表明其受到了攻擊,導致損失了160多個ETH和一些Token,這次攻擊事件,相對來說損失金額是較小的,約4萬美元,不過值得一提的是:這次攻擊事件的起因與2016年鬧得沸沸揚揚的TheDAO事件如出一轍!一共被盜走300多萬ETH,更爲嚴重的是還間接導致了以太坊硬分叉...

自那次事件起,以太坊的智能合約開發者大部分都認識到了重入漏洞這類嚴重問題,而且自那以後也很少有重入漏洞導致資產被竊取的事件,然而相隔兩年,悲劇在SpankChain上重演。

 

什麼是重入漏洞?

瞭解重入漏洞是理解這次攻擊事件必要的知識儲備,所以接下來我們會將該類漏洞進行一個詳細解釋,讓讀者深刻理解,已經瞭解的大佬可直接略過。

在以太坊智能合約中,合約與合約之間是可以互相調用的,在gas足夠的情況下,合約與合約之間甚至可以互相循環調用直至達到gas上限,這本身是合理的,但是若循環中會產生敏感操作例如轉賬,則有可能會導致產生很嚴重的問題。

還是挺抽象對不對,我們直接來用代碼進行解釋,引用大佬的一句話。

「Talk is cheap. Show me the code」

漏洞代碼片段:

 

function withdraw(){
  require(msg.sender.call.value(balances[msg.sender])());
  balances[msg.sender]=0;
}

 

以上是最簡化版的withdraw函數,此函數多數在錢包合約、去中心化交易所等合約中實現,目的是爲了讓用戶能進行“提款”,這裏的提款是指將智能合約體系內的代幣換成通用的以太幣。

漏洞分析:

前面提到過,合約與合約之間是可以互相循環調用的,只要循環所需的gas不超過gas上限即可,使用call來進行轉賬可以使用更多的gas,這是以太坊的機制。
但是如上代碼片段中犯了一個致命的問題:沒有在使用call轉賬之前將用戶的代幣餘額歸零,在循環的過程中,攻擊者的賬戶一直是處於有餘額的狀態。

這會導致什麼問題呢?
在前面的章節中我們提到過,在給智能合約轉賬的時候會觸發智能合約的fallback函數,若收款的智能合約在fallback函數中再次調用對方的withdraw函數的話,那將會產生一個循環調用。

3.png

如圖,漏洞合約會不斷向攻擊者合約轉賬,直至循環結束(有限循環,以太坊的gas上限不允許出現無限循環)後纔將用戶代幣餘額歸零。

用DEMO調試復現:

contract Bank{
    mapping(address => uint256) public balances;
    function wallet() constant returns(uint256 result){
        return this.balance;
    }
    function recharge() payable{
        balances[msg.sender]+=msg.value;
    }
    function withdraw(){
        require(msg.sender.call.value(balances[msg.sender])());
        balances[msg.sender]=0;
    }
}
contract Attacker{
    address public bankAddr;
    uint attackCount = 0;
    constructor(address _bank){
        bankAddr = _bank;
    }
    function attack() payable{
        attackCount = 0;
        Bank bank = Bank(bankAddr);
        bank.recharge.value(msg.value)();
        bank.withdraw();
    }
    function() payable{
    if(msg.sender==bankAddr&&attackCount<5){
        /*
        在合約中調用,這個fallback雖然是由bankAddr觸發的,
        但是代碼是在Attacker中,並且不是通過委託調用,
        所以這個時候withdraw的msg.sender 還是bank自身。
        並且由於是在同一個EVM環境中執行,
        上下文的調用棧並沒有變化,
        此時bank中的balances[msg.sender]並沒有減少
        導致bank可以多次轉賬
        
        */
        attackCount+=1;
        Bank bank = Bank(bankAddr);
        bank.withdraw();
        }
    }
    function wallet() constant returns(uint256 result){
        return this.balance;
    }
}

本文提供了一份復現DEMO,將上面代碼複製至remix IDE中即可對重入漏洞復現並有一個充分的瞭解。
第一步,使用remix賬戶列表中的第一個賬戶扮演受害者來部署漏洞合約以及在漏洞合約中存入一些以太幣。

 

4.png

如圖,筆者在Bank合約也就是漏洞合約中充值了500wei個以太幣。
第二步,使用remix賬戶列表中的第2個賬戶來扮演攻擊者部署一份攻擊者合約。
部署攻擊者合約需要填入Bank合約的地址,按如下按鈕即可複製地址,然後點擊Deploy按鈕來部署:

5.png

 

第三步,部署完之後,在value中填入要向Bank合約充值的數量,然後點擊attack函數,即可竊取相當於充值數額5倍的以太幣,這是由於爲了防止超出gas上限,攻擊者合約的fallback函數中限制了循環調用次數爲5次。

6.png

這裏我們充值了10wei的以太幣用於攻擊Bank合約,執行attack函數後我們在看看Attacker合約的餘額,以此推斷是否利用成功。

7.png

查看wallet,可以看到合約的錢包餘額爲60,正好多出了充值數的5倍。

SpankChain重入漏洞分析

有了上面的基礎知識接下來就好理解多了,我們先進行一個總結:重入漏洞產生的原因是未在進行轉賬操作之前進行狀態變更操作而在之後進行狀態變更操作,這是最根本的原因,換句話說,在進行一個類似於"提現"的操作中,轉賬只能爲最後一個要進行的步驟。

接下來我們要列出幾個重要的線索:

SpankChain支付通道合約: https://etherscan.io/address/0xf91546835f756da0c10cfa0cda95b15577b84aa7#code
攻擊者地址: https://etherscan.io/address/0xcf267ea3f1ebae3c29fea0a3253f94f3122c2199
攻擊者惡意合約地址: https://etherscan.io/address/0xc5918a927c4fb83fe99e30d6f66707f4b396900e
攻擊者惡意合約發起的攻擊交易: https://bloxy.info/zh/tx/0x21e9d20b57f6ae60dac23466c8395d47f42dc24628e5a31f224567a2b4effa88#

有上面的線索就很好分析了,我們先來看發起攻擊的那筆交易:

8.png

可以看到攻擊者先轉了5個ETH到他自己部署的惡意合約,然後再通過惡意合約將5ETH轉入SpankChain的支付通道合約,最後支付通道合約轉出了32次5個ETH到其惡意合約,惡意合約再將總金額32*5=160 ETH轉到了攻擊者賬戶中。

我們再來看看攻擊者具體的操作:

圖片.png

可以看到攻擊者先調用了支付通道合約的createChannel函數並轉入了5個ETH,然後循環調用了支付通道合約的LCOpenTimeou函數,並一直獲取ETH,每調用一次獲取5 ETH,一共調用了32次。

我們再來看看這兩個函數的具體代碼,先來看createChannel函數:

 

image.png

爲方便讀者理解,我們將該函數的每一行都進行了註釋,簡單來說該函數是用於創建一個“安全支付通道”,其原理是先將要轉的資金存到支付通道合約中,在規定的時間內收款方纔可以收款,超出規定時間發起方可以將轉賬撤回,支付通道合約相當於一箇中轉擔保的角色。

再來看看LCOpenTimeou函數:

9.png

該函數相當於提款函數,不過在這個支付通道內的意義爲轉賬超時撤回,就是說通道已經超出其開放時間了,發起方有權將轉賬撤回,具體漏洞點看紅框中的代碼以及註釋,簡單來說就是在發起轉賬之後才進行狀態變更操作,從而引發了重入漏洞。

更新,經過PeckShield團隊友情提醒,上述描述存在一處錯誤,向大家道歉!正確的結論如下:

儘管是在進行轉賬之後更新的狀態,但是上面的代碼要形成重入也又一定難度,看第一個紅框中的代碼,因爲該函數裏進行ETH轉賬不是使用的call.value,而是使用的transfer,使用transfer只能消耗2300GAS,無法構成重入,這也是SpankChain與TheDAO不同的點。

再看第二個紅框,其中調用了token的transfer函數,而token是攻擊者可控的,調用token合約的transfer函數不會有2300 GAS限制!於是攻擊者可以在自己部署的惡意token合約的transfer函數中調用支付通道合約的LCOpenTimeou函數,形成重入循環...

 

解決方案

最根本的解決方案還是在轉賬之前就把所有應該變更的狀態提前更新,而不是在轉賬之後再進行更新,希望這次事件能讓TheDAO慘案不再重演。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章