智能合約編程/Dapp漏洞 --Unexpected Ether

當ether被送到合約後,要麼執行fallback函數或者執行合約裏指定的其餘函數。可是在智能合約中有2種異常狀況。在這兩種異常狀況下,不用執行合約裏的任何代碼就能夠操做合約裏的ether。智能合約編程時,若是認爲只有執行合約代碼才能操做ether的話,就會致使合約收到攻擊:ether能夠被強制性的送到一個指定的合約。html

 

攻擊原理程序員

在程序設計中,有一種通用的技術,即預先設置幾個不變的狀態,在相應的程序執行完後,再來確認狀態沒有發生變化。一個普通的例子就是ERC20 token標準裏的totalSupply變量, 由於沒有函數會去修改totalSupply,開發程序員能夠在transfer()里加上一個檢查:在確信totalSupply不變的前提下,保證程序的正常執行。編程

 

有一個不變的狀態,特別容易被開發程序員用到,可是也特別容易被外部用戶操縱。這個狀態就是當前合約裏的ether數量。一般,做爲剛剛接觸Solidity的程序員,每每會有一個迷思:只有合約裏payable的函數才能發送和接受ether。這個迷思致使一個虛假,其實是不正確的關於合約中ether數量的想定。最明顯的帶有漏洞的用法就是this.balance。像下面所顯示的,對this.balance不當使用會致使很嚴重的漏洞。併發

 

在兩種狀況下,ether會被強制性的送給一個合約,既不會用到payable 修飾符也不會執行任何合約代碼。下面咱們具體討論。app

 

異常狀況1:自毀/自殺(Self Destruct / Suicideide

智能合約能夠實現selfdestruct(address)函數,這個函數會刪除合約地址裏的全部二進制代碼而且把合約裏全部的ether送到一個能夠指定參數的地址。若是指定的地址也是一個智能合約的話,就不會調用合約裏任何的函數(包含fallback) 於是, selfdestruct()函數能被用來強制性的發送ether到任何合約,而不會執行合約裏的任何代碼:合約裏的任何payable 函數都不會執行。這意味着:攻擊者能夠建立一個帶有selfdestruct()的合約,同時發送ether給這個合約,調用selfdestruct(target),而後在強制性的發送ether給一個指定的合約。這篇文章有很詳細的描述:http://swende.se/blog/Ethereum_quirks_and_vulns.html 函數

 

異常狀況2:Pre-sent Etherui

第二種方法是預導入(pre-load)帶有ether的合約。合約地址是肯定的:合約地址是基於建立合約地址的哈希以及建立合約的交易的nonce兩個信息計算而來。this

address = sha3(rlp.encode([account_address,transaction_nonce])) spa

這意味着,任何人均可以在合約被建立前計算合約的地址,併發送ether給那個地址。而後當合約真正被建立是,合約就會有一個非零的ether餘額

 

咱們能夠經過下面一個簡單的合約代碼來看看如何利用上面的知識來找到合約裏的漏洞。

 

EtherGame.sol

contract EtherGame {

   

    uint public payoutMileStone1 = 3 ether;

    uint public mileStone1Reward = 2 ether;

    uint public payoutMileStone2 = 5 ether;

    uint public mileStone2Reward = 3 ether;

    uint public finalMileStone = 10 ether;

    uint public finalReward = 5 ether;

   

    mapping(address => uint) redeemableEther;

    // users pay 0.5 ether. At specific milestones, credit their accounts

    function play() public payable {

        require(msg.value == 0.5 ether); // each play is 0.5 ether

        uint currentBalance = this.balance + msg.value;

        // ensure no players after the game as finished

        require(currentBalance <= finalMileStone);

        // if at a milestone credit the players account

        if (currentBalance == payoutMileStone1) {

            redeemableEther[msg.sender] += mileStone1Reward;

        }

        else if (currentBalance == payoutMileStone2) {

            redeemableEther[msg.sender] += mileStone2Reward;

        }

        else if (currentBalance == finalMileStone ) {

            redeemableEther[msg.sender] += finalReward;

        }

        return;

    }

   

    function claimReward() public {

        // ensure the game is complete

        require(this.balance == finalMileStone);

        // ensure there is a reward to give

        require(redeemableEther[msg.sender] > 0);

        redeemableEther[msg.sender] = 0;

        msg.sender.transfer(redeemableEther[msg.sender]);

    }

 }

 

這個智能合約是一個簡單的遊戲:玩家發送0.5給合約,並爭取成爲第一個完成3個里程碑任務的人。里程碑任務是以ether標價的:完成第一個里程碑任務的第一我的(好比合約總額達到5 ether時的第一人),就會在遊戲結束後拿回一部分的ether。當最終的里程碑任務(好比合約總額達到10ether時)達成時,遊戲結束,相應的玩家拿到獎賞。

 

問題在於合約裏這幾行代碼:

  …

uint currentBalance = this.balance + msg.value;

        // ensure no players after the game as finished

        require(currentBalance <= finalMileStone);

       …

          require(this.balance == finalMileStone);

        …

 

攻擊者能夠經過selfdestruct()來強制性的送小額的ether(好比0.1個ether)給合約,這樣的話,全部未來的玩家都將不能達成里程碑任務。由於全部正規的玩家都只會發送0.5或者0.5倍數的ether。一旦合約接受了上面的0.1ether,this。balance將永遠不會爲0.5的倍數。因此下列的if條件永遠都不會是true

…

// if at a milestone credit the players account

        if (currentBalance == payoutMileStone1) {

            redeemableEther[msg.sender] += mileStone1Reward;

        }

        else if (currentBalance == payoutMileStone2) {

            redeemableEther[msg.sender] += mileStone2Reward;

        }

        else if (currentBalance == finalMileStone ) {

            redeemableEther[msg.sender] += finalReward;

        }

         …

 

更要命的是,若是一個報復心很強的攻擊者,若是錯過了一個里程碑任務的話,他能夠強制性的送10 ether (或者讓合約餘額超過finalMileStone數目的ether),這就將永遠鎖住合約裏的全部獎勵。這是由於claimReward()函數會由於下面的require語句老是回退(revert) (this.balance 老是大於 finalMileStone).

// ensure the game is complete

        require(this.balance == finalMileStone);
相關文章
相關標籤/搜索