作者:Sissel@知道創宇404區塊鏈安全研究團隊 發佈時間:2018/08/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(用戶已經花了的部分)。 即 可用的錢 = 總價值 - 已用的錢
- 攻擊者 ==轉賬==> 攻擊合約 合約狀況:
- 攻擊合約 withdraw() 合約狀況:
- 攻擊合約 ==轉賬==> 攻擊者 合約狀況:
- 攻擊合約 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,便於解釋。
- 部署攻擊合約 tx:1. 部署合約 攻擊者部署合約,準備攻擊。 合約地址:0x7F325efC3521088a225de98F82E6dd7D4d2D02f8
- 購買token tx:2. 購買token 攻擊者購買一定量token,準備攻擊。
- 向攻擊合約轉賬token tx:3. transfer(attacker -> attack-contract) 攻擊者本身購買了少量token,使用遊戲合約中的transfer(),向攻擊合約轉賬。
- 攻擊合約withdraw() tx:4. withdraw() 攻擊合約調用了God的withdraw(),攻擊合約因此獲得了紅利對應以太幣【不重要】
- 攻擊合約transfer() tx:5. transfer(attack-contract -> attacker) 將token轉回,攻擊合約token不變,紅利溢出。
- 攻擊合約reinvest() tx:6. reinvest() 再投資,將紅利買token,可以大量購買token。
- 攻擊合約sell() tx:7. sell() 賣出一部分token,因爲發行的token過多,會導致token價值太低,提取以太幣較少。
- 攻擊合約transfer() tx:8. transfer(attack-contract -> 受益者) 把智能合約賬戶的token轉給受益者(0xc30e)一部分。
- 受益者sell()+withdraw() 受益者(0xc30e)賣掉token,並withdraw()紅利,得到以太幣。
0x05 更簡單的攻擊手法
回顧上述攻擊流程,攻擊成立主要依賴紅利由 token - payout 得到,時常變化,而不是記錄這個特性。
在交易token時,變化的只是雙方持有的token數,雙方的紅利應該不變,換言之,就是用戶的payout也需要變化才能保證紅利變化。
漏洞就在於在用戶和合約交易token時,合約方的payout並沒有相應的增加,導致紅利平白無故的多出來,最終導致了憑空生幣。
這樣一來,我們就可以使用更簡單的攻擊手法。
下面是詳細的介紹:
- 攻擊者 ==轉賬==> 攻擊合約 合約收到轉賬時,紅利本應爲0,卻變得很多,賬戶可用資金變得很多。
- 攻擊合約 withdraw() 把可用的錢提款爲eth,token不變。
- 攻擊合約 ==轉賬==> 攻擊者 token原路返回攻擊者,token不變,但合約中多出了 eth 。
我們發現智能合約在這個過程中,因爲接受轉賬未增加payout,導致在第二步中可以提取不少的以太幣,並在第三步將token原路轉回。 這一過程,合約賬戶便可憑空得到以太幣。而只需要支付一部分手續費以及token的輕微貶值。如此反覆創建新的合約,並按以上步驟,可以提出God.Game中大量的以太幣。
注意事項
此攻擊方法理論成立,還需仔細考察手續費和token價值變化等細節問題,但從合約中提取部分以太幣是可行的。
具體分析
- 購買token 攻擊者購買一定量token,準備攻擊。
- 向攻擊合約轉賬token 攻擊者本身購買了少量token,使用遊戲合約中的transfer(),向攻擊合約轉賬。
- 攻擊合約調用 withdraw() withdraw() 的主要邏輯如下: 攻擊合約調用withdraw(),通過以太幣的形式取出利息 dividents。
- 攻擊合約transfer() 將token轉回,攻擊者token恢復爲1000。
0x06 總 結
以上就是God.Game合約的分析,以及本次攻擊的復現。這次攻擊的發生距離合約部署僅有兩天,整個攻擊流程非常巧妙。按照前面的分析,僅通過合約賬戶的withdraw()就可以提出以太幣。但攻擊者還利用了紅利溢出,進而獲得了大量的token。根據上面多方面因素,雖然主辦方在事件發生後聲明自己是受害者。但是根據telegram上記錄,主辦方在遊戲開始之前就再未查看玩家羣。這些現像,引人深思。
區塊鏈遊戲看似充滿誘惑,實則迷霧重重。無論如何謹慎,都有可能跌入深淵。誰也不知道遊戲背後的創建者究竟有什麼打算,但人皆貪婪,有錢財的地方,必有隱患。
0x07 相關鏈接
- PoWH 3D 源碼分析 https://github.com/Fabsqrt/BitTigerLab/tree/master/Blockchain/Classes/PoWH3D
- God.Game官網 http://god.game/#/
附錄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; } }