作者:Sissel@知道創宇404區塊鏈安全研究團隊
時間:2018年8月24日

0x00 前言

當你凝視深淵時,深淵也在凝視著你。

越來越多的樂透、賭博游戲與區塊鏈體系結合起來,步入眾多投資者和投機者的視野中。區塊鏈可以說是這類游戲的溫床。正面來說,區塊鏈的可信機制與合約的公開,保證了游戲的中立。另一方面,區塊鏈的非實名性,也讓玩家的個人信息得以隱藏。

分紅、邀約、股息,這些游戲看似利益誘人,實則一個個都是龐氏騙局。游戲火了,詐騙滿滿皆是。每個人都信心滿滿地走進游戲,投入大筆資金,希望自己成為受益者,別人都是自己的接盤俠。這樣的游戲,只有兩個結局,不是游戲所有者獲益,就是半路殺進游戲的區塊鏈黑客卷走一切,讓玩家血本無歸,無一例外。日復一日,無數投機者交了學費,空手而歸,卻又毫不死心,重入深淵。

游戲依然層出不窮,不信邪的人也是接連不斷。近日,國內出現了一款類PoWH的銀行游戲,在兩周的宣傳過后,短短數日,就完成了游戲創建、集資、黑客卷錢走人這一整個流程,讓無數玩家措手不及。

時間線

  • 2018年08月19日晚十一點半,宣傳良久的區塊鏈賭博游戲God.Game合約被創建于以太坊6176235區塊。在之后的兩天時間,游戲內加入了大量玩家,合約內存儲的以太幣也增加到了243eth。
  • 2018年08月21日凌晨一點鐘,攻擊者經過簡單的測試,部署了一個攻擊合約。短短幾分鐘時間,利用游戲合約漏洞,將合約賬戶的eth洗劫為空。

知道創宇404區塊鏈安全研究團隊得知此事件后,對游戲合約進行了仔細審計,復現了攻擊者的手法,接下來,將對整個事件進行完整的分析,并給出一種簡潔的利用方式。

0x01 合約介紹

智能合約名為God,地址為 0xca6378fcdf24ef34b4062dda9f1862ea59bafd4d,部署于 6176235,發行了名為God幣的代幣(erc20 token)。

God.Game主要是一個銀行合約,代碼有上千行,較為復雜。如果之前對PoWH3D等類似合約有過接觸,God便不難理解。下面我們介紹些簡單概念。

ERC20 token

token代表數字資產,具有價值,通過智能合約發行于區塊鏈上,我們可以稱之為代幣。符合ERC20協議的代幣可以更容易互換,方便的在交易所上市。God幣便是符合ERC20協議的代幣。

合約功能

在God.Game中,你可以通過eth購買token(god幣),當你擁有了token,相當于參加了這個游戲。

  • 購買token:會產生一定的手續費,除了主辦方會收取一部分外,還有一部分將會均分給所有token持有者,也就是所謂的分紅。
  • 轉賬token:你可以將手中的token轉賬給他人。
  • 出售token:將手中的token出售為可提款。
  • 提取紅利:將分紅轉為以太幣提取出來。
  • 邀請機制:當你擁有多于100個token,將開啟邀請系統。他人使用你的地址,你將會獲得較多的手續費提成作為分紅。【攻擊未涉及該功能】

token與eth的兌換、分紅的多少,都與token的總量以及持有者有關,不斷變化。

代碼淺析

我們將簡要介紹合約中出現的幾個重要變量。

在開始介紹前,請先記住一個概念:紅利由 賬戶token的價值 - payout 得到,時常變化,而不是記錄這個變量

用戶信息

  • token【代幣】是確定的數量,用戶的token僅可通過自己buy、sell、transfer變動。
  • token * profitPerShare 可以看作是賬戶token的價值
  • payouts 我們稱之為已經用過的錢。【這個定義并不嚴謹,可以叫控制賬戶紅利的值】
  • token * profitPerShare - payoutsTo_ 可以看作用戶在此合約內現在可以使用的錢, 定義為紅利。

合約通過控制payoutsTo的值,來控制用戶可用的錢,即紅利【用來提eth,或再向God合約購買token】。

全局變量

以下變量是全局中浮動的

重要的臨時變量

dividends = 賬戶總價值 - 已用的錢【payout】

dividends這個變量并不存儲,不然每當其他參數變動時,需要計算所有人的分紅。

每次使用時,通過myDividends(false)計算,而這個函數在不涉及推薦功能時,僅調用了dividendsOf(address customerAddress)

這里也是本次攻擊的溢出點。

0x02 漏洞點

漏洞點有兩處,簡而言之,是當被轉賬賬戶是合約賬戶時,處理有誤造成的。

計算分紅

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

從上面得知,分紅可用來提eth,或再次購買token。 分紅本應永遠為正數,這里的減法未使用safeMath,最后還強制轉換uint,會造成整數溢出。 我們需要控制payoutsTo和token的關系。

轉賬transfer()

// exchange tokens
tokenBalanceLedger_[_from] = SafeMath.sub(tokenBalanceLedger_[_from], _amountOfTokens);
tokenBalanceLedger_[_toAddress] = SafeMath.add(tokenBalanceLedger_[_toAddress], _amountOfTokens);

我們看到,如論如何轉賬,token一定是一方減少,另一方增加,符合代幣的特點。

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

} 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);

這里是God中,針對轉賬雙方的賬戶類型【外部賬戶、合約賬戶】采取的不同操作。

我們會發現,transfer()函數并未對合約賬戶的payoutsTo進行操作。而是僅修改了contractPayout這個和God合約參數有關的全局變量。

導致合約賬戶中 token(很多) * profitPerShare(常量) - payoutsTo(0) 非常大。正常來講,payoutsTo應該變大,令賬戶的dividends為 0。

這種寫法非常奇怪,在ERC20的協議中,當被轉賬賬戶為合約時,只需要合約擁有該代幣的回調函數即可,沒有別的要求。

0x03 攻擊鏈

這樣我們就可以得到大致的攻擊鏈: 再次注意,紅利 dividens = token * token價值 - payout(用戶已經花了的部分)。 即 可用的錢 = 總價值 - 已用的錢

  1. 攻擊者 ==轉賬==> 攻擊合約
    合約狀況:

  2. 攻擊合約 withdraw()
    合約狀況:

  3. 攻擊合約 ==轉賬==> 攻擊者
    合約狀況:

  4. 攻擊合約 reinvest()
    合約狀況:

再投資【使用紅利購買token】,通過大量的紅利,可以隨意購買token,進而sell()+withdraw()提出eth,完成攻擊。

0x04 實際流程

攻擊者首先部署了幾個測試的攻擊合約,因為一些原因之后未使用,可能僅供測試。

攻擊合約逆向

知道創宇404區塊鏈安全研究團隊使用昊天塔,對攻擊者部署的合約進行了逆向,得到了攻擊合約大致代碼。

得到的函數列表

0x0: main()
0xa2: withdraw()
0xb7: ownerWithdraw()
0xcc: owner()
0xfd: myTokens()
0x124: transfer(address,uint256)
0x148: tokenFallback(address,uint256,bytes)
0x1c5: sell(uint256)
0x1dd: exit()
0x1f2: func_ee2ece60
0x207: buy(address)
0x21b: func_f6613ff5
0x230: reinvest()

而具體分析函數內容,發現該合約大部分函數都是以本合約發起對God合約的調用,例如:

function withdraw() public {
    if (msg.sender == 0x2368beb43da49c4323e47399033f5166b5023cda){
        victim.call(bytes4(keccak256("withdraw()")));
    }
}

對照攻擊者交易明細,我們來復現攻擊流程。我們假設token對應紅利是1:1,便于解釋。

  1. 部署攻擊合約
    tx:1. 部署合約 攻擊者部署合約,準備攻擊。 合約地址:0x7F325efC3521088a225de98F82E6dd7D4d2D02f8

  2. 購買token
    tx:2. 購買token 攻擊者購買一定量token,準備攻擊。

  3. 向攻擊合約轉賬token
    tx:3. transfer(attacker -> attack-contract) 攻擊者本身購買了少量token,使用游戲合約中的transfer(),向攻擊合約轉賬。

  4. 攻擊合約withdraw()
    tx:4. withdraw() 攻擊合約調用了God的withdraw(),攻擊合約因此獲得了紅利對應以太幣【不重要】

  5. 攻擊合約transfer()
    tx:5. transfer(attack-contract -> attacker) 將token轉回,攻擊合約token不變,紅利溢出。

  6. 攻擊合約reinvest()
    tx:6. reinvest() 再投資,將紅利買token,可以大量購買token。

  7. 攻擊合約sell()
    tx:7. sell() 賣出一部分token,因為發行的token過多,會導致token價值太低,提取以太幣較少。

  8. 攻擊合約transfer()
    tx:8. transfer(attack-contract -> 受益者) 把智能合約賬戶的token轉給受益者(0xc30e)一部分。

  9. 受益者sell()+withdraw()
    受益者(0xc30e)賣掉token,并withdraw()紅利,得到以太幣。

0x05 更簡單的攻擊手法

回顧上述攻擊流程,攻擊成立主要依賴紅利由 token - payout 得到,時常變化,而不是記錄這個特性。

在交易token時,變化的只是雙方持有的token數,雙方的紅利應該不變,換言之,就是用戶的payout也需要變化才能保證紅利變化。

漏洞就在于在用戶和合約交易token時,合約方的payout并沒有相應的增加,導致紅利平白無故的多出來,最終導致了憑空生幣。

這樣一來,我們就可以使用更簡單的攻擊手法。

下面是詳細的介紹:

  1. 攻擊者 ==轉賬==> 攻擊合約
    合約收到轉賬時,紅利本應為0,卻變得很多,賬戶可用資金變得很多。

  2. 攻擊合約 withdraw()
    把可用的錢提款為eth,token不變。

  3. 攻擊合約 ==轉賬==> 攻擊者
    token原路返回攻擊者,token不變,但合約中多出了 eth 。

我們發現智能合約在這個過程中,因為接受轉賬未增加payout,導致在第二步中可以提取不少的以太幣,并在第三步將token原路轉回。 這一過程,合約賬戶便可憑空得到以太幣。而只需要支付一部分手續費以及token的輕微貶值。如此反復創建新的合約,并按以上步驟,可以提出God.Game中大量的以太幣。

注意事項

此攻擊方法理論成立,還需仔細考察手續費和token價值變化等細節問題,但從合約中提取部分以太幣是可行的。

具體分析

  1. 購買token
    攻擊者購買一定量token,準備攻擊。

  2. 向攻擊合約轉賬token
    攻擊者本身購買了少量token,使用游戲合約中的transfer(),向攻擊合約轉賬。

  3. 攻擊合約調用 withdraw()
    withdraw() 的主要邏輯如下:
    攻擊合約調用withdraw(),通過以太幣的形式取出利息 dividents。

  4. 攻擊合約transfer()
    將token轉回,攻擊者token恢復為1000。

0x06 總結

以上就是God.Game合約的分析,以及本次攻擊的復現。這次攻擊的發生距離合約部署僅有兩天,整個攻擊流程非常巧妙。按照前面的分析,僅通過合約賬戶的withdraw()就可以提出以太幣。但攻擊者還利用了紅利溢出,進而獲得了大量的token。根據上面多方面因素,雖然主辦方在事件發生后聲明自己是受害者。但是根據telegram上記錄,主辦方在游戲開始之前就再未查看玩家群。這些現像,引人深思。

區塊鏈游戲看似充滿誘惑,實則迷霧重重。無論如何謹慎,都有可能跌入深淵。誰也不知道游戲背后的創建者究竟有什么打算,但人皆貪婪,有錢財的地方,必有隱患。

0x07 相關鏈接


智能合約審計服務

針對目前主流的以太坊應用,知道創宇提供專業權威的智能合約審計服務,規避因合約安全問題導致的財產損失,為各類以太坊應用安全保駕護航。

知道創宇404智能合約安全審計團隊: https://www.scanv.com/lca/index.html

聯系電話:(086) 136 8133 5016(沈經理,工作日:10:00-18:00)

歡迎掃碼咨詢:

區塊鏈行業安全解決方案

黑客通過DDoS攻擊、CC攻擊、系統漏洞、代碼漏洞、業務流程漏洞、API-Key漏洞等進行攻擊和入侵,給區塊鏈項目的管理運營團隊及用戶造成巨大的經濟損失。知道創宇十余年安全經驗,憑借多重防護+云端大數據技術,為區塊鏈應用提供專屬安全解決方案。

歡迎掃碼咨詢:


附錄1 此次事件相關地址

  • God合約創建者 0x802dF0C73EB17E540b39F1aE73C13dcea5A1CAAa

  • God合約地址 0xCA6378fcdf24Ef34B4062Dda9F1862Ea59BaFD4d

  • 最終以太幣存儲的賬戶 0xC30E89DB73798E4CB3b204Be0a4C735c453E5C74

  • 攻擊者 0x2368beb43da49c4323e47399033f5166b5023cda

  • 攻擊合約 0x7f325efc3521088a225de98f82e6dd7d4d2d02f8


附錄2 God.Game合約的函數分析

  • buy() - 購買token

  • sell() - 出售token

未使用的分紅增加,可用來withdraw(提款)或reinvest(再投資)。

  • withdraw() - 將分紅清0,分紅換為eth取出

清零分紅,獲得相應的eth。

  • reinvest() - 再投資

消耗掉賬戶的分紅,換成token。

  • transfer() - 轉賬

from:

to:

附錄3 根據昊天塔逆向結果,構造的攻擊合約

pragma solidity ^0.4.23;

contract Attack {
    address public owner;
    address public victim;

    function Attack() payable { owner = msg.sender; }

    function setVictim(address target) public { victim = target; }

    function withdraw() payable public {
        victim.call(bytes4(keccak256("withdraw()")));
    }

    function reinvest() payable public {
        victim.call(bytes4(keccak256("reinvest()")));
    }

    function transfer(address to_, uint256 amount) payable public{
        victim.call(bytes4(keccak256("transfer(address,uint256)")),to_,amount);
    }

    function () payable public{}

    function tokenFallback(address _from, uint _amountOfTokens, bytes _data) public returns (bool){
      return true;
    }
}

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