作者:Zhiniang Peng from Qihoo 360 Core Security
博客:360 Technology Blog

Dice2win 目前是以太坊上一款異常火爆的區塊鏈博彩游戲。號稱“可證明公平的”Dice2win目前每日有近千以太(一百五十萬人民幣)的下注額,是總交易量僅次于etheroll的第二大以太坊博彩游戲。然而我們分析發現,dice2win中的所有游戲都存在公平性漏洞,莊家可以利用這些漏洞操縱游戲結果。

Dice2win游戲介紹

Dice2win目前有包括“拋硬幣”、“擲骰子”、“兩個骰子”、“過山車”幾款游戲。其介紹如圖:

在這些游戲中,每個用戶單獨下注與莊家進行一對一對賭。游戲的本質,是用戶和莊家在去中心化的以太坊智能合約平臺上通過一系列協議來生成隨機數。如果用戶猜中隨機數,則用戶勝利,否則莊家勝利。

在進一步介紹Dice2win工作流程和公平性分析之前,我們先討論一個歷史悠久的密碼學問題:Mental poker。 Mental poker是由Shamir, Rivest和Adleman 1978年在文章“Is it possible to play a fair game of ‘Mental Poker”中首次提出的概念。(Shamir, Rivest和Adleman有沒有很眼熟?沒錯,就是你知道的RSA)其本質是想解決:在沒有可信第三方的參與的情況下(可信平臺或軟件),兩個不誠實的參與方如何在網絡上進行一場公平的棋牌游戲。在公平性的定義中,有非常重要的一點:如果任何一方收到了游戲結果,那么所有的誠實方都應該收到結果。

Dice2win實際上是利用區塊鏈實現mental poker的典型案例。但我們發現,Dice2win并不滿足mental poker的公平性安全。

選擇性中止攻擊

這里我們來看看Dice2win的工作原理。Dice2win游戲的本質,是用戶和莊家在去中心化的以太坊智能合約平臺上通過一系列協議來生成隨機數。如果用戶猜中隨機數,則用戶勝利,否則莊家勝利。游戲總體工作流程如下:

  1. 【莊家承諾】莊家(secretSigner)隨機生成某隨機數reveal,同時計算commit = keccak256 (reveal)對該reveal進行承諾。然后根據目前區塊高度,設置一個該承諾使用的最后區塊高度commitLastBlock。 對commitLastBlock和commit的組合體進行簽名得到sig,同時把(commit, commitLastBlock,sig)發送給玩家。
  2. 【玩家下注】玩家獲得(commit, commitLastBlock,sig)后選擇具體要玩的游戲,猜測一個隨機數r,發送下注交易placeBet到智能合約上進行下注。
  3. 【礦工打包】下注交易被以太坊礦工打包到區塊block1中,并將玩家下注內容存儲到合約存儲空間中。
  4. 【莊家開獎】當莊家在區塊block1中看到玩家的下注信息后。則發送settleBet交易公開承諾值reveal到區塊鏈上。合約計算隨機數random_number=keccak256(reveal,block1.hash)。如果random_number滿足用戶下注條件,則用戶勝,否則莊家勝。此外游戲還設有大獎機制,即如果某次random_number滿足某個特殊值(如88888),則用戶可贏得獎金池中的大獎。

Dice2win在其官網和白皮書宣稱自己的游戲具有數學上可證明的公平性,其隨機數是隨機數生成過程由礦工和莊家共同決定,礦工或者莊家無法左右游戲結果,所以玩家可以放心下注。此外,在一些介紹以太坊智能合約安全的文章中,我們也看到一些作者將Dice2win的隨機數生成過程稱為極佳實踐。

然而我們分析發現,dice2win中的所有游戲都會受到莊家選擇性中止攻擊,莊家可以選擇性公布中將結果從而導致用戶無法獲勝或贏得彩票。我們考慮如下兩個攻擊場景:

場景1:

用戶下注額大,且賠率高的情況下。用戶下下注產生block1后,block1.hash實際上就已經固定了。此時莊家已經可以計算出random_number,從而計算出用戶的投注結果和盈虧。則莊家可以選擇性中止交易。如果用戶不中獎,則莊家公布正常開獎結果。如果用戶中獎,則莊家可因為“網絡用戶和技術原因”從而導致用戶該筆下注失效。

場景2:

用戶下注額不大,但是block1產生后莊家發現random_number導致用戶中彩票。則莊家可以選擇性中止交易,導致用戶該筆下注失效。

在這兩種攻擊場景下,莊家都能夠輕松控制交易結果。當然莊家并不會對每筆交易都發起這種攻擊,而是可以選擇用戶獲獎特別大的交易進行操控。Dice2win官方實際上已經在智能合約代碼得注釋中聲明了可能會發生“技術問題和以太坊擁堵”原因造成荷官無法開獎(大約1個小時內),則用戶可以提回下注款。

造成該漏洞的本質原因在于,該方案的隨機數對于莊家而言并不是真正的隨機。莊家可以提前知道下注的結果。想一想如果你去一個聲稱“絕對公平”的賭場賭骰子。在完成下注后,莊家是有一種方法先偷看一眼骰子結果,算一算盈虧之后再決定是否開獎(重搖),這樣的骰子,是真的隨機的嗎?

實際上選擇性中止攻擊(selective abort attack)是針對mental poker公平性一種最常見的攻擊方式。要修復該問題實際也很簡單,只要以懲罰機制強制要求莊家在限制時間內打開承諾便可解決該問題。其他的適用于mental poker或安全多方計算的隨機數生成算法均可在此處適用。

選擇性開獎攻擊

選擇性中止攻擊的修復實際上非常簡單,要求莊家在限定時間內打開承諾便可。但這并沒有從機制上完全消除Dice2win莊家在游戲中的優勢。是不是簡單的直接引入其他mental poker或安全多方計算的隨機數生成算法到區塊鏈智能合約平臺上,就可以解決該問題了呢?實際上也未必能真正保證游戲的公平性。這里我們稱述一個我們有趣的發現: 區塊鏈智能合約上玩家交互的通訊模型,與傳統的互聯網用戶點對點通訊模型是有區別的。傳統的點對點通訊模型下證明的安全協議,直接套用到智能合約平臺上未必能保證其安全性。核心原因在于:傳統的點對點通訊模型下,協議的執行是順序的,不可逆的。而智能合約的通訊模型中,由于POW等共識算法存在分叉的可能性,協議的執行可能是非順序的可逆的。在下圖中,假設黑色區塊為網絡主鏈,白色區塊是分叉區塊。如果一個安全多方計算的協議步驟(某筆交易)在白色執行,那么該交易將不會生效。例如Alice和Bob在區塊鏈上進行某種計算。Alice在區塊B5上執行某筆交易,Bob隨后在區塊B6上公布某個秘密。隨后因為網絡發生分叉,B5、B6上的交易都失效了。但是Alice卻收到了秘密。

以太坊使用“幽靈協議(GHOST protocol)”來選擇區塊成為主鏈。分叉塊包括叔塊和孤塊。下圖中我們可以看到,當前以太坊叔塊的概率已經達到10%以上。所以直接簡單的將mental poker和安全多方計算的一些協議移植到智能合約平臺上時,發生不穩定事件的概率是不可忽略的。

解決該問題的最直接的方法是,智能合約中協議的每步執行之間等待足夠長的時間。當我們有接近100%把握上一個一筆交易已經在區塊中穩定了,再執行下一筆交易。但這樣的方式,一次交互可能要等待多個區塊(數分鐘的時間)才能完成。對于博彩游戲而言,顯然是不可接受的。

事實上在Dice2win在上個月已經意識到叔塊所帶來的問題了,并聲稱他們的提出的MerkleProof方法可以解決該問題,從而使得游戲變得公平。并在commit中提出了使MerkleProof的方法來對叔塊中的下注進行開獎(https://github.com/dice2-win/contracts/commit/86217b39e7d069636b04429507c64dc061262d9c)。 現在我們看看Dice2win如何解決這個問題。在Dice2win的代碼中(https://github.com/dice2-win/contracts/blob/b0a0412f0301623dc3af2743dcace8e86cc6036b/Dice2Win.sol),我們可以看到看到方法settleBetUncleMerkleProof(uint reveal, uint40 canonicalBlockNumber):

Dice2win官方提出的MerkleProof方法的核心邏輯在于:為了提高用戶體驗(開獎速度),當荷官收到一個下注交易(Tx)的時候(假設在B5區塊),就立刻計算出該下注結果并對該交易進行開獎。然而由于GHOST算法原因,最終主網選擇了A5區塊作為主鏈上的塊(與B5區塊hash值不同,所以開獎結果不一樣)。那么此時,按合約原本SettleBet的方法是無法對B5區塊進行開獎的。針對這個問題,Dice2win的解決辦法是:直接對叔塊進行開獎就好了。

如何對叔塊進行開獎呢:因為叔塊的hash是包含在主鏈上某個合法的區塊上的。而以太坊的區塊結構中又有非常多的哈希關聯結構。具體我們可以看看以太坊區塊頭的定義:

所以我們能夠從B5中交易Tx的交易執行結果Receipthash,一直計算向上層計算哈希得到叔塊B5的 ReciptsRoot。然后再與其他結構進行Hash得到叔塊B5的區塊hash(我們稱為uncleHash)。假設A6區塊中引用了叔塊B5的uncleHash,那么我們最終以A6的canonicalHash作為根節點,構造一個非結構性Merkle Tree。交易Tx為其中一個葉節點。其結構圖如下:

Dice2win提出的Merkle proof算法的思路在于:當分叉塊產生時(如同時產生B5與A5),網絡可能有兩種開獎結果出現。但因為網絡原因,荷官可能先收到其中一個塊(假設為B5)。從荷官的視角來看,它并不知道B5是否會稱為主鏈上的塊。即使荷官多等待一段時間,收到了(A5、A6、B6)后,它仍然無法確定哪條鏈會稱為主鏈。為了提高開獎速度,當荷官收到某個交易塊之后,就馬上進行開獎。如果該交易塊最后成為主鏈(A5),則正常使用SettleBet方法開獎。如果該區塊最后成為叔塊(B5),則提交由該交易執行結果為葉根節點、引用該叔塊的主鏈塊canoinicalHash為根節點的非結構性Merkle tree(如上圖)的一個存在性證明( Merkle proof )。從而證明B5確實存在過,且交易Tx包含在B5中;荷官可以使用叔塊進行開獎。

Dice2win的Merkle proof算法看上去是一個解決以太坊上去中心化博彩游戲開獎速度的很好的思路。但實際上該做法并不公平,以太坊上接近10%的叔塊率可以導致荷官以比較大的優勢可以根據游戲結果進行選擇性開獎。如果A5莊家贏就開A5,如果B5莊家贏就開B5。這樣的方案顯然不公平。

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

在詳細閱讀Dice2win關于Merkle proof的實現后,我們發現目前該合約的非結構性Merkle Proof驗證存在諸多繞過方法。即荷官可以偽造一個叔塊B5的Merkle proof,欺騙合約實現對任意結果進行開獎。

一次已經發生過的Merkle proof驗證繞過攻擊分析

同時,我們翻閱合約歷史發現其實上個月就已經有攻擊者實現了對該Merkle proof算法的繞過,將該版本的合約余額洗劫一空(但該情況為引起社區重視,Dice2win官方對該事件進行了冷處理)。在介紹我們的漏洞之前,我們可以先看看這個已發生對該Merkle proof驗證算法的攻擊方法。其中一筆攻擊交易發生在: https://etherscan.io/tx/0xd3b1069b63c1393b160c65481bd48c77f1d6f2b9f4bde0fe74627e42a4fc8f81

攻擊者通過創建攻擊合約0xc423379e42bb79167c110f4ac541c1e7c7f663d8,并在合約0xc423379e42bb79167c110f4ac541c1e7c7f663d8調用placeBet方法自動化進行下注(17次下注,每次2以太)。然后偽造Merkle proof,調用settleBetUncleMerkleProof方法開獎,在贏取了33以太后將獎金轉到賬戶0x54b7eb670e091411f82f50fdee3743bd03384aff,最后合約自殺銷毀。通過對該合約bytecode的逆向分析,我們可以得知該攻擊利用了如下漏洞:

1.Dice2win不同版本的合約,存在secretSigner相同的情況。導致一個莊家的承諾可以在不同版本的合約中使用。【運維原因產生的安全漏洞】

2.placeBet方法中對commit的過期校驗可被繞過。commitLastBlock與當前block.number進行大小判斷時是uint256類型的。然后再帶入keccak256進行簽名驗證的時候卻轉換成了uint40。那么攻擊者將任意一個secretSigner簽名的commitLastBlock 的最高位(256bit)從0修改為1,則可繞過時間驗證。【漏洞在最新版本中仍未修復,詳細見下圖】

3.Merkle proof校驗不嚴格。在該版本的settleBetUncleMerkleProof中,每次計算hashSlot偏移的邊界檢查不嚴格(缺少32byte),導致攻擊者可以不需要將目標commit綁定到該Merkle proof的計算中,從而繞過驗證。【該漏洞已修復,詳見下圖】

Merkle proof驗證繞過漏洞

經過我們分析,Dice2win目前版本的Merkle proof仍然存在多種繞過方法。由于該方法目前只能由荷官調用,所以普通攻擊者無法利用該漏洞。但該漏洞可以作為荷官后門實現任意開獎。 這里我們大致整理當前驗證算法的驗證邏輯:

  1. 先調用requireCorrectReceipt方法校驗Receipt格式滿足條件。
  2. Recipt trie entry中包含的是一個成功的交易。
  3. 交易的目標地址是Dice2win合約。
  4. Merkle Proof驗證計算的起始葉節點包含目標commit。
  5. 最后計算得到的canonicalHash是一個合法的主鏈塊哈希。 條件1、2、3的滿足并不是強綁定的,我們只要構造滿足條件的數據格式就可以了。條件4、5的繞過,本質上是要迭代計算: hash_0=commit hash_{n+1}= SHA3(something_{n1},hash_n,something_{n2}) canonicalHash=hash_{lastone}

攻擊方法1:

一個執行成功的叔塊交易中包含目標commit并不是什么難構造的事情。荷官可以在某個合約調用交易的input輸入里面塞入該commit就能繞過。當然該繞過方法比較麻煩。

攻擊方法2:

由于hash_{n+1}= SHA3(something_{n1},hash_n,something_{n2})的迭代計算未進行深度檢查。所以荷官可以在本地生意一個新的merkle tree,該merkle tree的葉節點滿足1、2、3條件且包含多個commit_i。將該merkle tree的根hash嵌入到一個正常的區塊中,就能生成一個合法的證明。在該攻擊方法中,荷官可以一勞永逸,對所有的commit進行任意開獎。

這些繞過方法的核心問題在于:目前該非結構化的Merkle tree實際上并不滿足我們常說的Merkle hash tree的結構。常規的Merkle hash tree在加強限制的條件下能夠進行存在性證明,但Dice2win的非結構化Merkle證明算法難以實現該目的。

其他安全問題:

當用戶下注未被開獎,用戶可以調用refundBet來溢出jackpotSize,造成jackpotSize變為一個巨大的整數(由Gaia發現并指出)。

后記

  1. Dice2win并不是一個公平的博彩游戲。
  2. 智能合約的安全問題非常嚴峻。(這實際上是我分析的第一個智能合約)
  3. 傳統的安全多方計算的協議有時不能簡單套用的到智能合約環境中,因為其通訊模型有區別。

Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/715/