智能合約遊戲之殤——Dice2win安全分析

作者:LoRexxar'@知道創宇404區塊鏈安全研究團隊
時間:2018年10月18日
系列文章:
《智能合約遊戲之殤——類 Fomo3D 攻擊分析》
《智能合約遊戲之殤——God.Game 事件分析》
Dice2win是目前以太坊上很火爆的區塊鏈博彩遊戲,其最大的特點就是理論上的公平性保證,每天有超過1000以太幣被人們投入到這個遊戲中。

Dice2win官網

Dice2win合約代碼

dice2win的遊戲非常簡單,就是一個賭概率的問題。

就相當於猜硬幣的正面和反面,只要你猜對了,就可以贏得相應概率的收穫。

這就是一個最簡單的依賴公平性的遊戲合約,只要“莊家”可以保證絕對的公正,那麼這個遊戲就成立。

2018年9月21日,我在 《以太坊合約審計 CheckList 之“以太坊智能合約編碼設計問題”影響分析報告》 中提到了以太坊智能合約中存在一個弱隨機數問題,裏面提到dice2win的合約中實現了一個很好的隨機數生成方案 hash-commit-reveal

2018年10月12日,Zhiniang Peng from Qihoo 360 Core Security發表了 《Not a fair game, Dice2win 公平性分析》 ,裏面提到了關於Dice2win的3個安全問題。

在閱讀文章的時候,我重新審視了Dice2win的合約代碼,發現在上次的閱讀中對Dice2win的執行流程有所誤解,而且Dice2win也在後面的代碼中迭代更新了Merkle proof功能,這裏我們就重點聊聊這幾個問題。

Dice2win安全性分析

選擇中止攻擊

讓我們來回顧一下dice2win的代碼

function placeBet(uint betMask, uint modulo, uint commitLastBlock, uint commit, bytes32 r, bytes32 s) external payable {
        // Check that the bet is in 'clean' state.
        Bet storage bet = bets[commit];
        require (bet.gambler == address(0), "Bet should be in a 'clean' state.");

        // Validate input data ranges.
        uint amount = msg.value;
        require (modulo > 1 && modulo <= MAX_MODULO, "Modulo should be within range.");
        require (amount >= MIN_BET && amount <= MAX_AMOUNT, "Amount should be within range.");
        require (betMask > 0 && betMask < MAX_BET_MASK, "Mask should be within range.");

        // Check that commit is valid - it has not expired and its signature is valid.
        require (block.number <= commitLastBlock, "Commit has expired.");
        bytes32 signatureHash = keccak256(abi.encodePacked(uint40(commitLastBlock), commit));
        require (secretSigner == ecrecover(signatureHash, 27, r, s), "ECDSA signature is not valid.");

        uint rollUnder;
        uint mask;

        if (modulo <= MAX_MASK_MODULO) {
            // Small modulo games specify bet outcomes via bit mask.
            // rollUnder is a number of 1 bits in this mask (population count).
            // This magic looking formula is an efficient way to compute population
            // count on EVM for numbers below 2**40. For detailed proof consult
            // the dice2.win whitepaper.
            rollUnder = ((betMask * POPCNT_MULT) & POPCNT_MASK) % POPCNT_MODULO;
            mask = betMask;
        } else {
            // Larger modulos specify the right edge of half-open interval of
            // winning bet outcomes.
            require (betMask > 0 && betMask <= modulo, "High modulo range, betMask larger than modulo.");
            rollUnder = betMask;
        }

        // Winning amount and jackpot increase.
        uint possibleWinAmount;
        uint jackpotFee;

        (possibleWinAmount, jackpotFee) = getDiceWinAmount(amount, modulo, rollUnder);

        // Enforce max profit limit.
        require (possibleWinAmount <= amount + maxProfit, "maxProfit limit violation.");

        // Lock funds.
        lockedInBets += uint128(possibleWinAmount);
        jackpotSize += uint128(jackpotFee);

        // Check whether contract has enough funds to process this bet.
        require (jackpotSize + lockedInBets <= address(this).balance, "Cannot afford to lose this bet.");

        // Record commit in logs.
        emit Commit(commit);

        // Store bet parameters on blockchain.
        bet.amount = amount;
        bet.modulo = uint8(modulo);
        bet.rollUnder = uint8(rollUnder);
        bet.placeBlockNumber = uint40(block.number);
        bet.mask = uint40(mask);
        bet.gambler = msg.sender;
    }

    // This is the method used to settle 99% of bets. To process a bet with a specific
    // "commit", settleBet should supply a "reveal" number that would Keccak256-hash to
    // "commit". "blockHash" is the block hash of placeBet block as seen by croupier; it
    // is additionally asserted to prevent changing the bet outcomes on Ethereum reorgs.
    function settleBet(uint reveal, bytes32 blockHash) external onlyCroupier {
        uint commit = uint(keccak256(abi.encodePacked(reveal)));

        Bet storage bet = bets[commit];
        uint placeBlockNumber = bet.placeBlockNumber;

        // Check that bet has not expired yet (see comment to BET_EXPIRATION_BLOCKS).
        require (block.number > placeBlockNumber, "settleBet in the same block as placeBet, or before.");
        require (block.number <= placeBlockNumber + BET_EXPIRATION_BLOCKS, "Blockhash can't be queried by EVM.");
        require (blockhash(placeBlockNumber) == blockHash);

        // Settle bet using reveal and blockHash as entropy sources.
        settleBetCommon(bet, reveal, blockHash);
    }

主要函數爲placeBet和settleBet,其中placeBet函數主要爲建立賭博,而settleBet爲開獎。最重要的一點就是,這裏完全遵守 hash-commit-reveal 方案實現,隨機數生成過程在服務端,整個過程如下。

  1. 用戶選擇好自己的下注方式,確認好後點擊下注按鈕。
  2. 服務端生成隨機數reveal,生成本次賭博的隨機數hash信息,有效最大blockNumber,並將這些數據進行簽名,並將commit和信息簽名傳給用戶。
  3. 用戶將獲取到的隨機數hash以及lastBlockNumber等信息和下注信息打包,通過Metamask執行placebet函數交易。
  4. 服務端在一段時間之後,將帶有隨機數和服務端執行settlebet開獎

在原文中提到,莊家(服務端)接收到用戶猜測的數字,可以選擇是否中獎,選擇部分對自己不利的中止,以使莊家獲得更大的利潤。

這的確是這類型合約最容易出現的問題,莊家依賴這種方式放大莊家獲勝的概率。

上面的流程如下

而上面提到的選擇中止攻擊就是上面圖的右邊可能會出現的問題

整個流程最大的問題,就在於placebet和settlebet有強制的執行先後順序,否則其中的一項block.number將取不到正確的數字,也正是應爲如此,當用戶下注,placebet函數執行時,用戶的下注信息就可以被服務端獲得了,此時服務端有隨機數、打包placebet的block.number、下注信息,服務端可以提前計算用戶是否中獎,也就可以選擇是否中止這次交易。

選擇開獎攻擊

在原文中,提到了一個很有趣的攻擊方式,在瞭解這種攻擊方式之前,首先我們需要對區塊鏈共識算法有所瞭解。

比特幣區塊鏈採用Proof of Work(PoW)的機制,這是一個叫做工作量證明的機制,提案者需要經過大量的計算才能找到滿足條件的hash,當尋找到滿足條件的hash反過來也證明了提案者付出的工作量。但這種情況下,可能會有多個提案者,那麼就有可能出現鏈的分叉。區塊鏈對這種結果的做法是,會選取最長的一條鏈作爲最終結果。

當你計算出來的塊被拋棄時,也就意味着你付出的成本白費了。所以礦工會選擇更容易被保留的鏈繼續計算下去。這也就意味着如果有人破壞,需要付出大量的經濟成本。

借用一張原文中的圖

在鏈上,計算出的b2、c5、b5、b6打包的交易都會回退,交易失敗,該塊不被認可。

回到Dice2win合約上,Dice2win是一個不希望可逆的交易過程,對於賭博來說,單向不可逆是一個很重要的原則。所以Dice2win新添加了MerikleProof方法來解決這個問題。

MerikleProofi方法核心在於,無論是否分叉,該分塊是否會被廢棄,Dice2win都認可這次交易。當服務端接收到一個下注交易(placebet)時,立刻對該區塊開獎。

MerikleProofi 的commit

上面這種方法的原理和以太坊的區塊結構有關,具體可以看 《Not a fair game, Dice2win 公平性分析》 一文中的分析,但這種方法一定程度的確解決了開獎速度的問題,甚至還減少了上面提到的選擇中止攻擊的難度。

但卻出現了新的問題,當placebet交易被打包到分叉的多個區塊中,服務端可以通過選擇獲利更多的那個區塊接受,這樣可以最大化獲得的利益。但這種攻擊方式效果有效,主要有幾個原因:

  1. Dice2win需要有一定算力的礦池才能主動影響鏈上的區塊打包,但大部分算力仍然掌握在公開的礦池手中。所以這種攻擊方式不適用於主動攻擊。
  2. 被動的遇到分叉情況並不會太多,尤其是遇到了打包了placebet的區塊,該區塊的hash只是多了選擇,仍然是不可控的,大概率多種情況結果都是一致的。

從這種角度來看,這種攻擊方式有效率有限,對大部分玩家影響較小。

任意開獎攻擊(Merkle proof驗證繞過)

在上面的分析中,我們詳細分析了我們Merkle proof的好處以及問題所在。但如果Merkle proof機制從根本上被繞過,那麼是不是就有更大的問題了。

Dice2win在之前已經出現了這類攻擊 https://etherscan.io/tx/0xd3b1069b63c1393b160c65481bd48c77f1d6f2b9f4bde0fe74627e42a4fc8f81

攻擊者成功構造攻擊合約,通過合約調用placeBet來下賭注,並僞造Merkle proof並調用settleBetUncleMerkleProof開獎,以100%的機率控制賭博成功。

分析攻擊合約可以發現該合約中的多個安全問題:

1、Dice2win是一個不斷更新的合約,存在多個版本。但其中決定莊家身份的secretSigner值存在多個版本相同的問題,導致同一個簽名可以在多個合約中使用。

2、placebet中對於最後一個commitlaskblock的check存在問題

用作簽名的commitlastblock定義是uint256,但用作簽名的只有uint40,也就是說,我們在執行placeBet的時候,可以修改高位的數字,導致某個簽名信息始終有效。

3、Merkle proof邊界檢查不嚴格。

在最近的一次commit中,dice2win修復了一個漏洞是關於Merkle proofcheck的範圍。

https://github.com/dice2-win/contracts/commit/b0a0412f0301623dc3af2743dcace8e86cc6036b

這裏檢查使Merkle proof更嚴格了

4、settleBet 權限問題

經過我的研究,實際上在Dice2win的遊戲邏輯中,settleBet應該是隻有服務端才能調用的(只有莊家才能開獎),但在之前的版本中,並沒有這樣的設置。

在新版本中,settleBet加入了這個限制。

這裏繞過Merkle proof的方法就不再贅述了,有興趣可以看看原文。

refundBet下溢

感謝@Zhiniang Peng from Qihoo 360 Core Security 提出了我這裏的問題,最開始理解有所偏差導致錯誤的結論。

原文中最後提到了一個refundBet函數的下溢,讓我們來看看這個函數的代碼

跟入getDiceWinAmount函數,發現jackpotFee並不可控

其中 JACKPOT_FEE = 0.001 ether ,且要保證amount大於0.1 ether,amount來自bet變量

而bet變量只有在placebet中可以被設置。

但可惜的是,placebet中會進行一次相同的調用

所以我們無法構造一個完整的攻擊過程。

但我們回到refundBet函數中,我們無法控制jackpotFee,那麼我們是不是可以控制jackpotSize呢

首先我們需要理解一下,jackpotSize是做什麼,在Dice2win的規則中,除了本身的規則以外,還有一份額外的大獎,是從上次大獎揭曉之後的交易抽成累積下來的。

如果有人中了大獎,那麼這個值就會清零。

但這裏就涉及競爭了,完整的利用流程如下:

  1. 攻擊者a下注placebet,並獲得commit
  2. 某個好運的用戶在a下注開獎前拿走了大獎
  3. 攻擊者調用refundBet退款
  4. jackpotSize成功溢出

總結

在回溯分析完整個Dice2win合約之後,我們不難發現,由於智能合約和傳統的服務端邏輯不同,導致許多我們慣用的安全思路遇到了更多問題,區塊鏈的不可信原則直接導致了隨機數生成方式的難度加深。目前最爲成熟的 hash-commit-reveal 方法是屬於通過服務端與智能合約交互實現的,在隨機數保密方面完成度很高,可惜的是無法避免服務端獲取過多信息的問題。

hash-commit-reveal 方法的基礎上,只要服務端不能即時響應開獎,選擇中止攻擊就始終存在。有趣的是Dice2win合約中試圖實現的Merkle proof功能初衷是爲了更快的開獎,但反而卻在一定程度上減少了選擇中止攻擊的可能性。

任意開獎攻擊,是一個針對Merkle proof的攻擊方式,應驗了所謂的功能越多漏洞越多的問題。攻擊方式精巧,是一種很有趣的利用方式。

就目前爲止,無論是底層的機制也好,又或是隨機數的生成方式也好,智能合約的安全還有很長的路要走。


Paper 本文由 Seebug Paper 發佈,如需轉載請註明來源。本文地址: https://paper.seebug.org/717/

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