God.Game 漏洞復(fù)盤:跑路還是黑客攻擊?

8 月 22 日,一個(gè)名不見經(jīng)傳的游戲 God.Game 發(fā)出通告,聲稱遭遇黑客攻擊,游戲內(nèi)的以太幣被黑客全部轉(zhuǎn)走。這一消息一時(shí)間傳遍各大媒體以及討論群,人們?cè)谫|(zhì)疑相關(guān)游戲安全性之余,也在揣測(cè)項(xiàng)目方動(dòng)機(jī),一時(shí)間難以分清到底是黑客攻擊,還是游戲開發(fā)方留有后門跑路。


安比(SECBIT)實(shí)驗(yàn)室的小伙伴在得到消息后,迅速開展了追蹤分析。

God.Game 簡(jiǎn)單規(guī)則如下:GOD 股份購(gòu)買的所有數(shù)量的 10% 被征稅并且作為被動(dòng)的 ETH 收入分配給所有GOD所有者。

通過游戲官網(wǎng)規(guī)則,以及合約源碼分析,不少人會(huì)認(rèn)為 God.Game 是 PoWH3D 的直接仿品,安比(SECBIT)實(shí)驗(yàn)室仔細(xì)分析后發(fā)現(xiàn),它其實(shí)是綜合“借鑒” PoWH3D 和 Zethr 的混合仿品。

PoWH3D 是最近大熱的 Fomo3D 游戲團(tuán)隊(duì)上一款作品。而 Zethr 則是在 PoWH3D 基礎(chǔ)上進(jìn)一步開發(fā)優(yōu)化,新增不同玩法的另一款熱門游戲。二者在玩法規(guī)則層面的細(xì)節(jié)差別,這里不做討論。

God.Game 游戲漏洞本身不復(fù)雜,不少安全公司也發(fā)出相關(guān)攻擊手法的短篇快訊消息,部分不夠詳盡,安比(SECBIT)實(shí)驗(yàn)室通過本文全面分析該漏洞,重點(diǎn)分享漏洞追蹤和定位的詳細(xì)過程與大家討論交流。

疑點(diǎn)一:攻擊是無意還是刻意

一下子提走合約內(nèi)所有以太幣的人,是參與游戲過程中無意發(fā)現(xiàn)了漏洞,還是刻意進(jìn)行的操作?

安比(SECBIT)實(shí)驗(yàn)室認(rèn)為是后者,因?yàn)榉治鱿聛?,該漏洞雖然隱藏不是很深,但實(shí)際觸發(fā)需要一系列組合操作(類似玩游戲按上上下下左左右右開啟作弊),正常游戲玩家的普通參與幾乎不可能觸發(fā)漏洞。安比(SECBIT)實(shí)驗(yàn)室分析攻擊者的交易記錄,發(fā)現(xiàn)其手段十分嫻熟,目的性極強(qiáng),明顯是奔著該漏洞而來。

疑點(diǎn)二:攻擊者的時(shí)機(jī)選擇

攻擊者為什么恰好選擇在獎(jiǎng)池金額 243 個(gè)以太幣時(shí)發(fā)動(dòng)攻擊?

上面是游戲合約賬戶余額趨勢(shì)圖,橫坐標(biāo)是區(qū)塊高度,縱坐標(biāo)是 God.Game 合約賬戶余額。此圖可反映隨著時(shí)間推進(jìn),入場(chǎng)資金情況。安比(SECBIT)實(shí)驗(yàn)室發(fā)現(xiàn),God.Game 合約部署上線后,合約資金量長(zhǎng)期一直在 50 以太幣以下,但在 6179500 區(qū)塊高度以后,入場(chǎng)資金開始猛增到接近 260 個(gè)以太幣(大量韭菜入場(chǎng)),隨后略有衰退(一部分人選擇提現(xiàn)退出),進(jìn)入一段很長(zhǎng)的調(diào)整停滯期。

無論是 Fomo3D 還是 PoWH3D,這類龐氏游戲最早期入場(chǎng)資金優(yōu)勢(shì)都很大,可以較快速回本。后續(xù)只有在持續(xù)大規(guī)模地運(yùn)營(yíng)宣傳下,才可能有大量新資金入場(chǎng)參與游戲。因此游戲第一時(shí)間內(nèi)的入場(chǎng)資金,基本決定了游戲資金量的總體規(guī)模。

而攻擊者正是在資金規(guī)模就快回升到前期高點(diǎn)時(shí),果斷出擊,利用漏洞果斷提走游戲內(nèi)的所有以太幣。于是形成了上圖類似“高臺(tái)跳水”的有趣情景。

這蹊蹺的手法、這迷人的走勢(shì)是不是有些眼熟?

疑點(diǎn)三:是漏洞還是后門

到底是無意引入的漏洞還是刻意暗留的后門?

前面提到,God.Game 游戲代碼重度參考了 PoWH3D 和 Zethr。而與漏洞相關(guān)的關(guān)鍵函數(shù)名 transferFromInternal() 只在 Zethr 合約代碼中有出現(xiàn)。

上圖所示問題代碼,“創(chuàng)新”地增加了一組關(guān)于轉(zhuǎn)賬雙方是合約、還是普通賬戶的分支情況處理。面對(duì)這一串冗長(zhǎng)的代碼,只要清楚 PoWH3D 工作原理,就很容易能發(fā)現(xiàn)此處代碼邏輯根本說不通,也無法在游戲?qū)嶋H規(guī)則中找到適配點(diǎn)。并且這種根據(jù)賬戶類型分別處理賬本的邏輯在原版 PoWH3D 和 Zethr 中根本沒有出現(xiàn)。

God.Game 代碼此處函數(shù)命名仿照了 Zethr,但是具體變量名卻是仿造 Fomo3D,代碼風(fēng)格十分詭異。可以推斷以下兩種情況:代碼作者有可能是因?yàn)闆]有理解 PoWH3D 游戲機(jī)制引入了漏洞;也有可能是故意增加代碼混亂度來迷惑他人,以達(dá)到埋藏后門之目的。


特別地,安比(SECBIT)實(shí)驗(yàn)室還發(fā)現(xiàn)同樣是 transferFromInternal() 函數(shù),God.Game 代碼中唯一與 Zethr 相近的地方,就是上圖示例中針對(duì) ERC223 代碼做的回落處理,似乎是想通過引入此段代碼(需要判斷目標(biāo)地址是否是合約)來為前面提到的不合邏輯代碼打掩護(hù)。

異常:飆升的 Token 售價(jià)

下面讓我們進(jìn)入安比(SECBIT)實(shí)驗(yàn)室安全研究員 sha3 的第一視角,讓 TA 為你撥開漫漫迷霧找出漏洞。

Good Luck Have Fun.

我們首先進(jìn)入游戲首頁,發(fā)現(xiàn)單個(gè)God token的售價(jià)已經(jīng)飆升至300ETH,而被God.Game抄襲的火爆原版游戲PoWH3D的單價(jià)才0.02ETH,面對(duì)不尋常的數(shù)值,敏感的安比(SECBIT)實(shí)驗(yàn)室小伙伴首先懷疑該游戲合約可能存在整數(shù)溢出漏洞。

為了考證我們的想法,我們想到了回溯God.Game過往的售價(jià)變化過程(感謝區(qū)塊鏈技術(shù),讓我們無法消滅歷史),尋找在何處觸發(fā)了整數(shù)溢出漏洞。

通過遍歷區(qū)塊歷史數(shù)據(jù),發(fā)現(xiàn)在6182409高度時(shí),buyPrice/sellPrice等數(shù)據(jù)飆升,于是我們便仔細(xì)分析該區(qū)塊中和God.Game相關(guān)的交易。

分析發(fā)現(xiàn),在6182409區(qū)塊中,唯一和God.Game產(chǎn)生關(guān)聯(lián)的交易是0x368688a944059fdd657e7842d8762b05250bd45f3a2a16cbae1b29727023b00f

在該交易中,0x2368Beb4調(diào)用0x7f325EfC的reinvest()后,繼而調(diào)用了0xca6378fc中的reinvest(),(通過簡(jiǎn)單的逆向分析,我們暫時(shí)認(rèn)為0x7f325EfC是一個(gè)簡(jiǎn)單的代理合約,實(shí)現(xiàn)了God.Game游戲的基本接口)。

一番操作:定位到攻擊源

我們不禁問道,為何調(diào)用一次reinvest()就可以將游戲中的各個(gè)數(shù)據(jù)全部暴增,我們懷疑創(chuàng)建這個(gè)合約的賬戶就是攻擊者。

順著這條線索,我們觀察到這個(gè)合約的創(chuàng)建者,即0x2368Beb4,在6182409高度之前通過合約0x7f325EfC進(jìn)行了另外幾次操作。

具體操作歷史如下:

區(qū)塊高度 From To call
6182399 0x2368Beb4 0xca6378fc transfer(address,uint256)
6182399 0xca6378fc 0x7f325EfC tokenFallback(address,uint256,bytes)
6182403 0x2368Beb4 0x7f325EfC withdraw() 3ccfd60b
6182403 0x7f325EfC 0xca6378fc withdraw() 3ccfd60b
6182403 0xca6378fc 0x7f325EfC
6182406 0x2368Beb4 0x7f325EfC transfer(address,uint256)
6182406 0x7f325EfC 0xca6378fc transfer(address,uint256)
6182409 0x2368Beb4 0x7f325EfC reinvest() fdb5a03e
6182409 0x7f325EfC 0xca6378fc reinvest() fdb5a03e
6182419 0x2368Beb4 0x7f325EfC sell(uint256)
6182419 0x7f325EfC 0xca6378fc sell(uint256)
6182439 0x2368Beb4 0x7f325EfC transfer(address,uint256)
6182439 0x7f325EfC 0xca6378fc transfer(address,uint256)
6182462 0x2368Beb4 0x7f325EfC transfer(address,uint256)
6182462 0x7f325EfC 0xca6378fc transfer(address,uint256)

再看看reinvest()函數(shù)調(diào)用時(shí)產(chǎn)生的event。

我們看到了0000000000000000fffffffffffffffffffffffffffffffffffcf2ac578ec8d9這個(gè)極像溢出的值。

隨即我們打開God.Game源代碼,搜尋其中的蛛絲馬跡。

我們將目標(biāo)鎖定到reinvest()函數(shù)。

function reinvest()
onlyProfitsHolders()
public
{
    // fetch dividends
    uint256 _dividends = myDividends(false);
    // retrieve ref. bonus later in the code
    // pay out the dividends virtually
    address _customerAddress = msg.sender;
    payoutsTo_[_customerAddress] += (int256) (_dividends * magnitude);
    // retrieve ref. bonus
    _dividends += referralBalance_[_customerAddress];
    referralBalance_[_customerAddress] = 0;
    // dispatch a buy order with the virtualized "withdrawn dividends"
    uint256 _tokens = purchaseTokens(_dividends, 0x0);
    // fire event
    emit onReinvestment(_customerAddress, _dividends, _tokens);
}

首先大概瀏覽一下reinvest()函數(shù)主體,僅有兩個(gè)簡(jiǎn)單的加法,但是通常在減法溢出中會(huì)出現(xiàn)巨型整數(shù),我們便開始探索reinvest()調(diào)用的函數(shù)。

首先引起我們注意的便是myDividends(false)函數(shù),由于參數(shù)傳入了false,我們就直接研究它最終調(diào)用的函數(shù)dividendsOf(_customerAddress)。

function dividendsOf(address _customerAddress)
view
public
returns (uint256)
{
    return (uint256) ((int256)(profitPerShare_ * tokenBalanceLedger_[_customerAddress]) - payoutsTo_[_customerAddress]) / magnitude;
}

Aha,看到減法了!

會(huì)不會(huì)就是在這里發(fā)生了溢出呢?

歷史回放:精確找到溢出點(diǎn)

帶著這樣的疑問,我們回溯了0x368688a944059fdd657e7842d8762b05250bd45f3a2a16cbae1b29727023b00f這筆交易的trace,我們跟著靜態(tài)分析結(jié)果,直接在trace中將PC指針定位到了dividendsOf函數(shù)中。

發(fā)現(xiàn)在0x0a8c指針處執(zhí)行了sub指令,并且sub指令在棧上的兩個(gè)參數(shù)分別為0和0x30d53a87137270000000000000000減法、除法執(zhí)行完畢后棧頂變成了0xfffffffffffffffffffffffffffffffffffcf2ac578ec8d9,和event中的值完全相同。

果然,我們找到了發(fā)生溢出的地方,并且確定reinvest()函數(shù)使用了溢出后的值。

順藤摸瓜:推理怎么構(gòu)造溢出條件

那么問題來了,我們有什么辦法能讓這個(gè)減法溢出呢?

回想sub指令的參數(shù):

  • 第一個(gè)參數(shù) 0 對(duì)應(yīng) profitPerShare_ * tokenBalanceLedger_[_customerAddress]
  • 第二個(gè)參數(shù) 0x30d53a87137270000000000000000 對(duì)應(yīng) payoutsTo_[_customerAddress]

思路來了,我們需要達(dá)成2個(gè)目標(biāo)

  1. 使得 profitPerShare_ * tokenBalanceLedger_[_customerAddress]的計(jì)算結(jié)果為0,
  2. 使得 payoutsTo_[_customerAddress]中存儲(chǔ)的值為正數(shù)

首先我們看目標(biāo)1,對(duì)于profitPerShare_變量,縱觀全部代碼,只有增加沒有減,無法有效地將這個(gè)值變?yōu)?,那么只有tokenBalanceLedger_[_customerAddress]才能給我們修改的機(jī)會(huì)。

然后目光看向目標(biāo)2payoutsTo_[_customerAddress]必須設(shè)置為正值。

我們看到在transferFromInternal()、withdraw()函數(shù)均中對(duì)該值有操作,不由得想到了上文看到攻擊者的4步組合拳,這4步具體發(fā)生的操作如下:

  1. transfer (攻擊者調(diào)用God.Game合約的transfer函數(shù)將token轉(zhuǎn)移到到攻擊者創(chuàng)建的合約)
  2. withdraw (攻擊者調(diào)用代理合約的withdraw函數(shù),代理合約調(diào)用God.Game的withdraw函數(shù)講ETH提入代理合約)
  3. transfer (攻擊者調(diào)用代理合約的transfer函數(shù),將God.Game中代理合約的token轉(zhuǎn)移到攻擊者賬戶)
  4. reinvest (攻擊者調(diào)用代理合約的reinvest函數(shù),代理合約調(diào)用God.Game的reinvest函數(shù)觸發(fā)溢出)

謎底揭開:漂亮的組合拳

我們不妨看看在God.Game合約中發(fā)生了什么?

攻擊者第1步:從自己賬戶轉(zhuǎn)移token到合約,觸發(fā)transferFromInternal函數(shù)中的第一個(gè)else if分支:

else if (fromLength <= 0 && toLength > 0) {
    // human to contract
    contractAddresses[_toAddress] = true;
    contractPayout += (int) (_amountOfTokens);
    tokenSupply_ = SafeMath.sub(tokenSupply_, _amountOfTokens);
    payoutsTo_[_from] -= (int256) (profitPerShare_ * _amountOfTokens);
}

攻擊者將自身的payoutsTo_[_customerAddress]被減為負(fù)數(shù),緊接著修改``tokenBalanceLedger_[from/to]`進(jìn)行普通的token轉(zhuǎn)賬操作,給代理合約一些token以便擁有dividens。

攻擊者第2步:代理調(diào)用withdraw(),payoutsTo_[_customerAddress]值增加。

攻擊者第3步:將代理合約的token轉(zhuǎn)移到攻擊者賬戶,觸發(fā)transferFromInternal中的if分支:

if (fromLength > 0 && toLength <= 0) {
    // contract to human
    contractAddresses[_from] = true;
    contractPayout -= (int) (_amountOfTokens);
    tokenSupply_ = SafeMath.add(tokenSupply_, _amountOfTokens);
    payoutsTo_[_toAddress] += (int256) (profitPerShare_ * _amountOfTokens);        
}

攻擊者的payoutsTo_[_customerAddress]值增加,同時(shí)將代理合約中的token全部轉(zhuǎn)移到攻擊賬戶中。

可以看到,這3步組合讓代理合約滿足了減法溢出所需條件:

  1. 通過第1、第3兩步將代理合約的tokenBalanceLedger_[_customerAddress]置為0
  2. 通過第2步將payoutsTo_[_customerAddress]值置為正數(shù)

攻擊者第4步,猛烈一擊,調(diào)用reinvest()觸發(fā)dividendsOf()的減法溢出,攻擊者獲取巨量的dividens,將divisions轉(zhuǎn)換為token。

由于token數(shù)量過多,合約儲(chǔ)備金額不夠withdraw(),攻擊者便將部分token轉(zhuǎn)移到0xC30E89DB73798E4CB3b204Be0a4C735c453E5C74,通過0xC30E89DB73798E4CB3b204Be0a4C735c453E5C74進(jìn)行sell操作賣出token換取ETH,合約儲(chǔ)備金面對(duì)巨量增發(fā)的token,瞬間消耗殆盡,攻擊者成功提取了幾乎所有的ETH。

Good Game.

God.Game 風(fēng)波帶來的思考

近來各類山寨版本智能合約游戲盛行,已經(jīng)發(fā)生很多起安全事件,安比(SECBIT)實(shí)驗(yàn)室不久前已針對(duì) Fomo3D 和 Last Winner 進(jìn)行了一系列詳細(xì)的漏洞披露和解析。而這次 God.Game 甚至上線不久就宣告被黑客攻擊游戲結(jié)束,很多玩家的投入無法兌現(xiàn),導(dǎo)致出現(xiàn)一些「游戲(投資)群」秒變「維權(quán)群」的鬧劇。

安比(SECBIT)實(shí)驗(yàn)室提醒廣大智能合約游戲愛好者,務(wù)必要擦亮雙眼,提高安全意識(shí),謹(jǐn)慎參與不明游戲(小心漏洞與后門),并且對(duì)于任何游戲都不要投入超出承受能力范圍的資金。

游戲智能合約在一定程度上比一般 Token 合約更復(fù)雜,一些漏洞會(huì)隱藏得更深,觸發(fā)條件更苛刻。很多游戲甚至?xí)懈邔哟蔚年P(guān)于公平性、博弈機(jī)制的漏洞存在,需要從游戲模型設(shè)計(jì)、代碼實(shí)現(xiàn)等各種角度進(jìn)行安全評(píng)估。安比(SECBIT)實(shí)驗(yàn)室建議所有負(fù)責(zé)任的智能合約游戲開發(fā)商,都應(yīng)該提高安全意識(shí),加大安全投入。

參考文獻(xiàn)

以上數(shù)據(jù)均由安比(SECBIT)實(shí)驗(yàn)室提供,合作交流請(qǐng)聯(lián)系info@secbit.io。


安比(SECBIT)實(shí)驗(yàn)室

安比(SECBIT)實(shí)驗(yàn)室專注于區(qū)塊鏈與智能合約安全問題,全方位監(jiān)控智能合約安全漏洞、提供專業(yè)合約安全審計(jì)服務(wù),在智能合約安全技術(shù)上開展全方位深入研究,致力于參與共建共識(shí)、可信、有序的區(qū)塊鏈經(jīng)濟(jì)體。

安比(SECBIT)實(shí)驗(yàn)室創(chuàng)始人郭宇,中國(guó)科學(xué)技術(shù)大學(xué)博士、耶魯大學(xué)訪問學(xué)者、曾任中科大副教授。專注于形式化證明與系統(tǒng)軟件研究領(lǐng)域十余年,具有豐富的金融安全產(chǎn)品研發(fā)經(jīng)驗(yàn),是國(guó)內(nèi)早期關(guān)注并研究比特幣與區(qū)塊鏈技術(shù)的科研人員之一。研究專長(zhǎng):區(qū)塊鏈技術(shù)、形式化驗(yàn)證、程序語言理論、操作系統(tǒng)內(nèi)核、計(jì)算機(jī)病毒。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容