類Fomo3D遊戲漏洞與修復方案全解析
2018-9-29 22:26
來源: SECBIT
摘要

無論是 Fomo3D 山寨版還是正宗原版都擺脫不了“一輪就涼涼”的宿命,這與其智慧合約的設計漏洞不無關係。本文從合約安全開發的角度出發,詳細分析了類 Fomo3D 遊戲的兩個問題,並提出若干個可能的解決方案。希望能有所幫助,歡迎感興趣的朋友加入技術社群討論。
Fomo3D 遊戲已正式進入第三輪。截止北京時間 9 月 29 日上午 11 點整,本輪獎池僅累積了 97.8988 Ether,外加上一輪滾入的 680 Ether,獎池總金額不足 800 Ether,相較前兩輪的盛況,可謂慘不忍睹。
安比(SECBIT)實驗室曾經撰文分析了類 Fomo3D 遊戲的衰敗現狀,先來簡單回顧一下 [1]。
圖一:Fomo3D 玩家參與度與入場資金狀況
上圖展示了「Fomo3D 玩家參與度與入場資金狀況」。紅色代表呼叫合約參與遊戲的人次,藍色則代表進入遊戲合約的資金量。圖左側出現數據曲線最高峰,對應時間分別是 7 月 20 日和 7 月 21 日。這兩天恰好大量媒體瘋狂報道 Fomo3D 這一現象級遊戲。當時眾多玩家跟風入場,遊戲合約的參與次數和入場資金均達到了最高峰,入場資金量超過 40,000 Ether,而參與次數最高超過 18,000 次。高峰過後,Fomo3D 遊戲熱度驟降,於 8 月 22 日前後結束第一輪,並隨即進入第二輪,但遊戲熱度已然無法恢復。
儘管如此,黑客卻沒有停止攻擊。
圖二:Fomo3D 遊戲合約被攻擊狀況
上圖是「Fomo3D 遊戲合約被攻擊狀況」,第一輪遊戲高峰前後以及第二輪開始後,有黑客瘋狂地利用 “空投漏洞” 進行攻擊,攫取高額收益 [2]。而在第一輪臨近結束,以及第二輪倒計時快結束之際,則有黑客瘋狂嘗試 “阻塞交易” 攻擊,企圖奪取最終大獎 [3]。
不僅僅是 Fomo3D 原版遊戲,其他眾多的類 Fomo3D 山寨遊戲,也成為黑客的攻擊目標。
Fomo3D 類遊戲參與形式是用 Ether 購買遊戲道具,最後一位購買者獲得 “最終大獎” ,平時參與者有一定概率獲得 “空投獎勵” ,分別從主獎池和副獎池中獲取。這兩類獎勵是遊戲設計層面對參與者的重要激勵。這一設計,目的在於利用“隨機”和“競爭”提升遊戲趣味度,吸引更多人投入資金參與,從而 延長遊戲時間 。
然而事與願違,由於合約程式碼存在漏洞,掌握攻擊技巧的黑客能夠以很高的概率持續獲得 “空投獎勵” ,而 “最終大獎” 也會被黑客利用特殊技巧奪走。普通參與者在這類遊戲中幾乎無法獲得這兩種重要獎勵。因此,他們僅能幻想在每輪遊戲開始後第一時間入場,然後靠後續他人的資金回本。但是,遊戲 最重要的兩個激勵機制已然失效 ,無法持續吸引新資金,最終形成惡性迴圈。
黑客是如何利用這兩個漏洞的?專案方難道就無計可施嗎?
先看看“空投獎勵”。
所有投入遊戲的 Ether,會有 1% 數量進到副獎池。空投的概率從 0% 開始,每增加一筆不小於 0.1 ETH 銷售訂單,空投概率會增加 0.1%。同時空投獎勵金額與購買金額也掛鉤,如果購買 0.1 ~ 1 ETH,就有概率贏得 25% 副獎池獎金,購買越多則比例越大。遊戲介面會鮮明顯示當前中獎概率和獎池金額。
Fomo3D 空投獎勵實現存在兩處問題:
-
合約中的“隨機數”可被預測
-
判斷呼叫者是否是合約地址的方法有漏洞
空投獎勵依靠智慧合約內生成的“隨機數”,在 Fomo3D 原始碼中由 airdrop()
函式控制。
/**
* @dev generates a random number between 0-99 and checks to see if thats
* resulted in an airdrop win
* @return do we have a winner?
*/
function airdrop()
private
view
returns(bool)
{
uint256 seed = uint256(keccak256(abi.encodePacked(
(block.timestamp).add
(block.difficulty).add
((uint256(keccak256(abi.encodePacked(block.coinbase)))) / (now)).add
(block.gaslimit).add
((uint256(keccak256(abi.encodePacked(msg.sender)))) / (now)).add
(block.number)
)));
if((seed - ((seed / 1000) * 1000)) < airDropTracker_)
return(true);
else
return(false);
}
airdrop()
函式中的“隨機數” seed
由各種區塊資訊和交易發起者地址計算得來。這顯然十分容易預測 [4]。
為了防止合約自動化攻擊,Fomo3D 開發者還使用 isHuman()
來防止合約賬戶參與 Fomo3D 遊戲,試圖以此方法來禁止玩家在合約內預測中獎隨機數。
/**
* @dev prevents contracts from interacting with fomo3d
*/
modifier isHuman() {
address _addr = msg.sender;
uint256 _codeLength;
assembly {_codeLength := extcodesize(_addr)}
require(_codeLength == 0, "sorry humans only");
_;
}
這裡犯了另一個常見錯誤。 extcodesize
操作符用來獲取目標地址上的程式碼大小。對於已部署成功的合約,由於其地址對應著特定程式碼, extcodesize
的返回值始終大於 0。因此不少人用此方法來判斷目標地址是否是合約,Fomo3D 甚至以此為依據來阻止合約呼叫特定函式。但該判斷方法存在明顯漏洞,在構造新合約的過程中(即合約構造方法裡)呼叫遊戲參與函式即可繞過該限制。這是因為合約在構造過程中,其地址並未對應任何程式碼, extcodesize
的返回值為 0 [5]。
上述的兩個安全問題綜合作用,最終導致黑客可以構造攻擊合約,通過合約參與遊戲,隨意預測隨機數,進而極大提高自己的勝率 [2]。
Part
3
如何修改空投漏洞
那麼究竟如何解決 Fomo3D 的“空投漏洞”?
黑客能夠成功攻擊,是利用了上文列出的兩個漏洞,構造攻擊合約來預測遊戲合約中的“隨機數”。因此,我們只需完成以下兩件事之一,使 攻擊所需的必要條件 不滿足即可:
-
防止智慧合約中的“隨機數”預測
-
採取更安全的方式判斷呼叫者是否是合約
方案一
防範智慧合約中的“隨機數”預測
讓我們先解決“隨機數”預測的問題。
智慧合約環境內“隨機數”容易被預測的原因在於,“隨機數”產生所依賴的“隨機源”可以被任何人輕易獲得。 攻擊者可以構造一個攻擊合約 ,在相同環境內執行“隨機數”計算公式,即可得到需要的“隨機數”,並以之作為下一步行動的判斷依據。
智慧合約內幾乎一切可用變數都是公開的,並且“隨機數”計算公式需要確保所有節點執行結果都一致。因此,很難找到十分簡潔的方法來產生無法被預測的“隨機數”。
但仍有一些稍複雜但可行的解決方案。如開發者可通過先提交再披露(commit/reveal)、或延遲若干個區塊開獎。此外,還有一些引入外部預言機(Oracle)的方案,如 Oraclize 和 BTCRelay [6]。
安比(SECBIT)實驗室結合 Fomo3D 遊戲機制,介紹一種利用 “當前/未來”區塊的雜湊值 來防止“隨機數”被預測的方案 [7]。
以太坊智慧合約中可以通過 block.blockhash()
來獲取特定區塊的雜湊值。該函式接受引數為區塊高度,可取範圍為除當前區塊外的最近 256 個區塊。當傳入其他值時,該函式均返回 0。
常見不安全的“隨機數”計算方法,會讀取當前塊的前一個塊的雜湊 block.blockhash(block.number-1)
作為隨機源。而在合約內執行 block.blockhash(block.number)
返回值為 0。我們無法在合約內獲得當前區塊的雜湊,這是因為礦工打包並執行交易時,當前區塊雜湊尚未被算出。 因此,我們可以認為“當前區塊”雜湊是“未來”的,無法預測。
我們可以在使用者首次購買道具參與遊戲時,記錄其地址、 當前區塊高度 N 至一個數組中,最終拿到一個唯一的 id(如下面 _purchase()
函式所示)。
function _purchase(address user) internal {
Purchase memory p = Purchase({
user: user,
commit: uint64(block.number),
randomness: 0
});
uint id = purchases.push(p) - 1;
emit KeysPurchased(id, user, packCount);
}
在接下來的 255 個區塊內,使用者可以用該 id 再次參與遊戲,此時高度為 N 的區塊雜湊可正常獲得,以此來生成“隨機數”,判斷使用者是否中獎(如下面 _airdrop()
函式所示)。
function _airdrop(uint id) internal returns(bool) {
Purchase storage p = purchases[id];
require(p.randomness == 0);
require(block.number - 256 < p.commit);
require(uint64(block.number) != p.commit);
require(p.user == msg.sender);
bytes32 bhash = blockhash(p.commit);
uint seed = uint(keccak256(abi.encodePacked(bhash, p.user, id)));
p.randomness = seed;
if((seed - ((seed / 1000) * 1000)) < airDropTracker_)
return(true);
else
return(false);
}
255 個區塊之後,使用者參與遊戲時的區塊雜湊在合約內無法正常獲得。因此,務必要限制使用者在一定時間範圍內查詢是否中獎,並及時參與遊戲領取獎勵。當然,為了遊戲體驗,如果使用者錯失領獎,也可以參照上面的原理再給他一次機會重新抽獎。結合遊戲規則,這裡仍有一些技術細節需注意,歡迎新增小安同學微信(secbit_xiaoanbi),加入到「SECBIT 智慧合約安全技術群」參與討論。
這種方法也用在知名的區塊鏈卡牌遊戲 Gods Unchained 中,用來控制使用者所購卡牌稀有程度。當然我們也可以用當前高度後指定數量(如五個)的區塊雜湊來作為隨機源,原理是一樣的 [8]。
方案二
防止合約自動化攻擊
另一個問題,我們如何判斷呼叫者是否是合約地址?
有一個簡便但是有效的方法。
modifier isHuman() {
require(tx.origin == msg.sender, "sorry humans only");
_;
}
以太坊安全開發最佳實踐中推薦儘量不要使用 tx.origin
,因為很多人將 tx.orign
和 msg.sender
混淆。 tx.orign
代表的是一筆交易的發起者,而 msg.sender
代表每一次合約呼叫( call
)的發起者。
A -> B -> C
如普通賬戶 A 呼叫合約 B,合約 B 再呼叫合約 C。在合約 C 內, msg.sender
是合約 B,而 tx.origin
是賬戶 A。 msg.sender
可以是合約地址,但 tx.origin
永遠不會是合約。因此,上面的方法可以有效防止合約呼叫合約。
Part
4
“阻塞交易”攻擊分析
再看看“最終大獎”。
Fomo3D 類遊戲存在倒計時,在每輪遊戲結束前最後一個購買道具的參與者獲勝,可以拿走主獎池中近半的資金。因此眾多參與者會在臨近結束時,發起購買交易參與遊戲,如果能幸運地在最後一刻被礦工打包入塊,即可獲勝。
普通人在遊戲快結束時都是類似的策略:緊盯著時間,調高 Gas 費用,發起參與遊戲的交易,然後閉上眼睛祈禱,希望自己能是最後一個參與者。然而,採用這種方法幾乎不可能中獎。
據安比(SECBIT)實驗室分析,Fomo3D 前兩輪獲獎者使用手法如出一轍,均在遊戲快結束時,發起攻擊交易。
獲獎者(黑客)通過提前部署好的攻擊合約,在合約內呼叫 getCurrentRoundInfo()
介面查詢遊戲資訊,重點關注 剩餘時間 和 最後一位購買者地址 。當遊戲剩餘時間達到一個閾值,並且最後一個購買者是自己時,則通過 assert()
讓整個交易失敗,並耗光所有 Gas;當剩餘時間很長或最後一個購買者不是自己時,則不做任何操作,僅消耗很少的 Gas。
獲獎者(黑客)就是利用這種方法,發起大量類似的可變神祕交易: 在自己極有可能成為中獎者時 ,利用這些高額手續費的神祕交易,吸引礦池優先打包,佔滿後續區塊,從而使得其他玩家購買 key 的交易無法被正常打包,最終 加速遊戲結束 ,並極大地提高自己的中獎概率。
普通玩家只能在遊戲快結束時手動調高 Gas 費用參與遊戲,也有人試圖使用自動指令碼在臨近遊戲結束時調高 Gas Price 發起參與遊戲交易。與這些盲目的方法相比,黑客的攻擊手法顯然高明許多。
Part
5
如何防範“阻塞交易”攻擊
其實,這一問題不止會威脅類 Fomo3D 遊戲。所有采用類似機制,即需要玩家搶在某個時間範圍內完成某種競爭操作的智慧合約,都會受此威脅。只要遊戲獎勵足夠豐厚, 攻擊回報遠大於投入 ,就會有人利用前文提到的方法來破壞遊戲公平性。
方案一
提高攻擊所需成本
要杜絕這一問題,安比(SECBIT)實驗室建議遊戲開發者,從遊戲機制入手,切斷遊戲最終勝利(獲得某個鉅額大獎)和倒計時結束(最後一個交易被打包)之間的必然聯絡,從而使黑客的 攻擊獲利概率 和 攻擊意願 都降到最低。
例如,我們可以修改遊戲規則為:每輪遊戲結束前最後一個購買道具的參與者 有概率 獲得最終大獎,並將此概率調整為一個 較低的值 ,如 5 %。在倒計時結束但大獎因概率原因沒有正常開出的情況下,合約自動給遊戲續一定時間。這樣一來,前面提到的堵塞區塊、阻止別人參與遊戲的技巧,無法確保攻擊者一定能獲得最終大獎。 而黑客持續進行“阻塞交易”攻擊需耗費大量 Gas 費用,成本會很高,最終會選擇放棄攻擊。
function buyCore(...)
private
{
...
// check to see if end round needs to be ran
if (_now > round_[_rID].end && round_[_rID].ended == false)
{
// check to see whether or not this round should end
if shouldRndEnd(lastCommitId) (
// end the round (distributes pot) & start new round
round_[_rID].ended = true;
_eventData_ = endRound(_eventData_);
...
) else {
...
updateTimer(_keys, _rID);
...
}
}
...
}
上面為示例程式碼,其中 shouldRndEnd()
函式用來在倒計時結束後控制中獎概率,決定這一輪遊戲是否真的結束。這裡的概率同樣依賴“隨機數”不能被預測,具體實現原理與 前文提到的空投概率控制程式碼 類似。
方案二
禁止合約呼叫遊戲資訊查詢介面
Fomo3D 最終獲勝者可以輕易攻擊成功的另一個原因是,遊戲合約開放了一個完整的遊戲進度資訊查詢介面,並且普通賬戶和合約賬戶都可以任意呼叫查詢。這方便了黑客在攻擊合約內 實時查詢遊戲狀態 ,進而執行 不同策略 來降低攻擊成本和提高命中率。
modifier isHuman() {
require(tx.origin == msg.sender, "sorry humans only");
_;
}
function getCurrentRoundInfo()
isHuman()
public
view
returns(...)
{
...
}
因此,針對 Fomo3D 遊戲,還有另一個簡易的防範方法。對 getCurrentRoundInfo()
函式使用前文提到的安全版的 isHuman()
校驗來保護,就可以有效避免合約自動化攻擊。
Part
6
總結
有安全和公平性問題的 Fomo3D 原版以及山寨版,僅是“黑客”掘金的物件,註定無法吸引更多普通玩家參加。隨著一輪一輪的進行,玩家會逐漸流失,這些遊戲會進一步沒落。
安比(SECBIT)實驗室呼籲後來者吸取教訓,不要再原封不動地複製程式碼,不要試圖僅靠“運營”來吸引新人入場。作出一些小小的改變,智慧合約的安全性會得到很大的提升,去中心化遊戲才能走得更遠。
SECBIT技術社群
「SECBIT 智慧合約安全技術討論」入群方式:歡迎新增微信(ID:secbit_xiaoanbi或掃描文末二維碼),由小安同學拉入群。
一
參考文獻
[1] Fomo3D二輪大獎開出,黑客獲獎,機制漏洞成遊戲沒落主因, https://zhuanlan.zhihu.com/p/45330743, 2018/09/25
[2] 智慧合約史上最大規模攻擊手法曝光,盤點黑客團伙作案細節, https://zhuanlan.zhihu.com/p/42318584, 2018/08/17
[3] Fomo3D 千萬大獎獲得者“特殊攻擊技巧”最全揭露, https://zhuanlan.zhihu.com/p/42742004, 2018/08/23
[4] How to PWN FoMo3D, a beginners guide, https://www.reddit.com/r/ethereum/comments/916xni/how_to_pwn_fomo3d_a_beginners_guide, 2018/07/23
[5] Using EVM assembly to get the address' code size, https://ethereum.stackexchange.com/questions/14015/using-evm-assembly-to-get-the-address-code-size, 2017/04/07
[6] Predicting Random Numbers in Ethereum Smart Contracts, https://blog.positive.com/predicting-random-numbers-in-ethereum-smart-contracts-e5358c6b8620, 2018/02/01
[7] Random Number Generation on Winsome.io — Future Blockhashes, https://blog.winsome.io/random-number-generation-on-winsome-io-future-blockhashes-fe44b1c61d35, 2017/05/07
[8] Gods Unchained, https://etherscan.io/address/0x482cf6a9d6b23452c81d4d0f0f139c1414963f89#code, 2018/07/16
宣告:“鏈門戶”登載此文出於傳遞更多資訊之目的,並不意味著贊同其觀點或證實其描述。文章內容盡供參考,不構成投資建議。投資者據此操作,風險自擔。如有不妥請及時聯絡QQ:3341927519進行處理。