God.Game 漏洞复盘:跑路还是黑客攻击?

8 月 22 日,一个名不见经传的游戏 God.Game 发出通告,声称遭遇黑客攻击,游戏内的以太币被黑客全部转走。这一消息一时间传遍各大媒体以及讨论群,人们在质疑相关游戏安全性之余,也在揣测项目方动机,一时间难以分清到底是黑客攻击,还是游戏开发方留有后门跑路。

这里写图片描述

安比(SECBIT)实验室的小伙伴在得到消息后,迅速开展了追踪分析。

God.Game 简单规则如下:GOD 股份购买的所有数量的 10% 被征税并且作为被动的 ETH 收入分配给所有GOD所有者。

通过游戏官网规则,以及合约源码分析,不少人会认为 God.Game 是 PoWH3D 的直接仿品,安比(SECBIT)实验室仔细分析后发现,它其实是综合“借鉴” PoWH3D 和 Zethr 的混合仿品。

PoWH3D 是最近大热的 Fomo3D 游戏团队上一款作品。而 Zethr 则是在 PoWH3D 基础上进一步开发优化,新增不同玩法的另一款热门游戏。二者在玩法规则层面的细节差别,这里不做讨论。

God.Game 游戏漏洞本身不复杂,不少安全公司也发出相关攻击手法的短篇快讯消息,部分不够详尽,安比(SECBIT)实验室通过本文全面分析该漏洞,重点分享漏洞追踪和定位的详细过程与大家讨论交流。

疑点一:攻击是无意还是刻意

一下子提走合约内所有以太币的人,是参与游戏过程中无意发现了漏洞,还是刻意进行的操作?

安比(SECBIT)实验室认为是后者,因为分析下来,该漏洞虽然隐藏不是很深,但实际触发需要一系列组合操作(类似玩游戏按上上下下左左右右开启作弊),正常游戏玩家的普通参与几乎不可能触发漏洞。安比(SECBIT)实验室分析攻击者的交易记录,发现其手段十分娴熟,目的性极强,明显是奔着该漏洞而来。

疑点二:攻击者的时机选择

攻击者为什么恰好选择在奖池金额 243 个以太币时发动攻击?

这里写图片描述

上面是游戏合约账户余额趋势图,横坐标是区块高度,纵坐标是 God.Game 合约账户余额。此图可反映随着时间推进,入场资金情况。安比(SECBIT)实验室发现,God.Game 合约部署上线后,合约资金量长期一直在 50 以太币以下,但在 6179500 区块高度以后,入场资金开始猛增到接近 260 个以太币(大量韭菜入场),随后略有衰退(一部分人选择提现退出),进入一段很长的调整停滞期。

无论是 Fomo3D 还是 PoWH3D,这类庞氏游戏最早期入场资金优势都很大,可以较快速回本。后续只有在持续大规模地运营宣传下,才可能有大量新资金入场参与游戏。因此游戏第一时间内的入场资金,基本决定了游戏资金量的总体规模。

而攻击者正是在资金规模就快回升到前期高点时,果断出击,利用漏洞果断提走游戏内的所有以太币。于是形成了上图类似“高台跳水”的有趣情景。

这蹊跷的手法、这迷人的走势是不是有些眼熟?

疑点三:是漏洞还是后门

到底是无意引入的漏洞还是刻意暗留的后门?

前面提到,God.Game 游戏代码重度参考了 PoWH3D 和 Zethr。而与漏洞相关的关键函数名 transferFromInternal() 只在 Zethr 合约代码中有出现。

这里写图片描述
上图所示问题代码,“创新”地增加了一组关于转账双方是合约、还是普通账户的分支情况处理。面对这一串冗长的代码,只要清楚 PoWH3D 工作原理,就很容易能发现此处代码逻辑根本说不通,也无法在游戏实际规则中找到适配点。并且这种根据账户类型分别处理账本的逻辑在原版 PoWH3D 和 Zethr 中根本没有出现。

God.Game 代码此处函数命名仿照了 Zethr,但是具体变量名却是仿造 Fomo3D,代码风格十分诡异。可以推断以下两种情况:代码作者有可能是因为没有理解 PoWH3D 游戏机制引入了漏洞;也有可能是故意增加代码混乱度来迷惑他人,以达到埋藏后门之目的。

这里写图片描述

特别地,安比(SECBIT)实验室还发现同样是 transferFromInternal() 函数,God.Game 代码中唯一与 Zethr 相近的地方,就是上图示例中针对 ERC223 代码做的回落处理,似乎是想通过引入此段代码(需要判断目标地址是否是合约)来为前面提到的不合逻辑代码打掩护。

异常:飙升的 Token 售价

下面让我们进入安比(SECBIT)实验室安全研究员 sha3 的第一视角,让 TA 为你拨开漫漫迷雾找出漏洞。

Good Luck Have Fun.

我们首先进入游戏首页,发现单个God token的售价已经飙升至300ETH,而被God.Game抄袭的火爆原版游戏PoWH3D的单价才0.02ETH,面对不寻常的数值,敏感的安比(SECBIT)实验室小伙伴首先怀疑该游戏合约可能存在整数溢出漏洞。

为了考证我们的想法,我们想到了回溯God.Game过往的售价变化过程(感谢区块链技术,让我们无法消灭历史),寻找在何处触发了整数溢出漏洞。

通过遍历区块历史数据,发现在6182409高度时,buyPrice/sellPrice等数据飙升,于是我们便仔细分析该区块中和God.Game相关的交易。

分析发现,在6182409区块中,唯一和God.Game产生关联的交易是0x368688a944059fdd657e7842d8762b05250bd45f3a2a16cbae1b29727023b00f

在该交易中,0x2368Beb4调用0x7f325EfC的reinvest()后,继而调用了0xca6378fc中的reinvest(),(通过简单的逆向分析,我们暂时认为0x7f325EfC是一个简单的代理合约,实现了God.Game游戏的基本接口)。

一番操作:定位到攻击源

我们不禁问道,为何调用一次reinvest()就可以将游戏中的各个数据全部暴增,我们怀疑创建这个合约的账户就是攻击者。

顺着这条线索,我们观察到这个合约的创建者,即0x2368Beb4,在6182409高度之前通过合约0x7f325EfC进行了另外几次操作。

具体操作历史如下:

区块高度FromTocall
61823990x2368Beb40xca6378fctransfer(address,uint256)
61823990xca6378fc0x7f325EfCtokenFallback(address,uint256,bytes)
61824030x2368Beb40x7f325EfCwithdraw() 3ccfd60b
61824030x7f325EfC0xca6378fcwithdraw() 3ccfd60b
61824030xca6378fc0x7f325EfC
61824060x2368Beb40x7f325EfCtransfer(address,uint256)
61824060x7f325EfC0xca6378fctransfer(address,uint256)
61824090x2368Beb40x7f325EfCreinvest() fdb5a03e
61824090x7f325EfC0xca6378fcreinvest() fdb5a03e
61824190x2368Beb40x7f325EfCsell(uint256)
61824190x7f325EfC0xca6378fcsell(uint256)
61824390x2368Beb40x7f325EfCtransfer(address,uint256)
61824390x7f325EfC0xca6378fctransfer(address,uint256)
61824620x2368Beb40x7f325EfCtransfer(address,uint256)
61824620x7f325EfC0xca6378fctransfer(address,uint256)

再看看reinvest()函数调用时产生的event。

这里写图片描述

我们看到了0000000000000000fffffffffffffffffffffffffffffffffffcf2ac578ec8d9这个极像溢出的值。

随即我们打开God.Game源代码,搜寻其中的蛛丝马迹。

我们将目标锁定到reinvest()函数。

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()函数主体,仅有两个简单的加法,但是通常在减法溢出中会出现巨型整数,我们便开始探索reinvest()调用的函数。

首先引起我们注意的便是myDividends(false)函数,由于参数传入了false,我们就直接研究它最终调用的函数dividendsOf(_customerAddress)

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

Aha,看到减法了!

会不会就是在这里发生了溢出呢?

历史回放:精确找到溢出点

带着这样的疑问,我们回溯了0x368688a944059fdd657e7842d8762b05250bd45f3a2a16cbae1b29727023b00f这笔交易的trace,我们跟着静态分析结果,直接在trace中将PC指针定位到了dividendsOf函数中。

这里写图片描述

这里写图片描述

发现在0x0a8c指针处执行了sub指令,并且sub指令在栈上的两个参数分别为0和0x30d53a87137270000000000000000减法、除法执行完毕后栈顶变成了0xfffffffffffffffffffffffffffffffffffcf2ac578ec8d9,和event中的值完全相同。

果然,我们找到了发生溢出的地方,并且确定reinvest()函数使用了溢出后的值。

顺藤摸瓜:推理怎么构造溢出条件

那么问题来了,我们有什么办法能让这个减法溢出呢?

回想sub指令的参数:

  • 第一个参数 0 对应 profitPerShare_ * tokenBalanceLedger_[_customerAddress]
  • 第二个参数 0x30d53a87137270000000000000000 对应 payoutsTo_[_customerAddress]

思路来了,我们需要达成2个目标

  1. 使得 profitPerShare_ * tokenBalanceLedger_[_customerAddress]的计算结果为0,
  2. 使得 payoutsTo_[_customerAddress]中存储的值为正数

首先我们看目标1,对于profitPerShare_变量,纵观全部代码,只有增加没有减,无法有效地将这个值变为0,那么只有tokenBalanceLedger_[_customerAddress]才能给我们修改的机会。

然后目光看向目标2payoutsTo_[_customerAddress]必须设置为正值。

我们看到在transferFromInternal()withdraw()函数均中对该值有操作,不由得想到了上文看到攻击者的4步组合拳,这4步具体发生的操作如下:

  1. transfer (攻击者调用God.Game合约的transfer函数将token转移到到攻击者创建的合约)
  2. withdraw (攻击者调用代理合约的withdraw函数,代理合约调用God.Game的withdraw函数讲ETH提入代理合约)
  3. transfer (攻击者调用代理合约的transfer函数,将God.Game中代理合约的token转移到攻击者账户)
  4. reinvest (攻击者调用代理合约的reinvest函数,代理合约调用God.Game的reinvest函数触发溢出)

谜底揭开:漂亮的组合拳

我们不妨看看在God.Game合约中发生了什么?

攻击者第1步:从自己账户转移token到合约,触发transferFromInternal函数中的第一个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]被减为负数,紧接着修改`tokenBalanceLedger_[from/to]进行普通的token转账操作,给代理合约一些token以便拥有dividens。

攻击者第2步:代理调用withdraw()payoutsTo_[_customerAddress]值增加。

攻击者第3步:将代理合约的token转移到攻击者账户,触发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]值增加,同时将代理合约中的token全部转移到攻击账户中。

可以看到,这3步组合让代理合约满足了减法溢出所需条件:

  1. 通过第1、第3两步将代理合约的tokenBalanceLedger_[_customerAddress]值为0
  2. 通过第2步将payoutsTo_[_customerAddress]值置为正数

攻击者第4步,猛烈一击,调用reinvest()触发dividendsOf()的减法溢出,攻击者获取巨量的dividens,将divisions转换为token。

由于token数量过多,合约储备金额不够withdraw(),攻击者便将部分token转移到0xC30E89DB73798E4CB3b204Be0a4C735c453E5C74,通过0xC30E89DB73798E4CB3b204Be0a4C735c453E5C74进行sell操作卖出token换取ETH,合约储备金面对巨量增发的token,瞬间消耗殆尽,攻击者成功提取了几乎所有的ETH。

Good Game.

God.Game 风波带来的思考

近来各类山寨版本智能合约游戏盛行,已经发生很多起安全事件,安比(SECBIT)实验室不久前已针对 Fomo3D 和 Last Winner 进行了一系列详细的漏洞披露和解析。而这次 God.Game 甚至上线不久就宣告被黑客攻击游戏结束,很多玩家的投入无法兑现,导致出现一些「游戏(投资)群」秒变「维权群」的闹剧。

安比(SECBIT)实验室提醒广大智能合约游戏爱好者,务必要擦亮双眼,提高安全意识,谨慎参与不明游戏(小心漏洞与后门),并且对于任何游戏都不要投入超出承受能力范围的资金。

游戏智能合约在一定程度上比一般 Token 合约更复杂,一些漏洞会隐藏得更深,触发条件更苛刻。很多游戏甚至会有更高层次的关于公平性、博弈机制的漏洞存在,需要从游戏模型设计、代码实现等各种角度进行安全评估。安比(SECBIT)实验室建议所有负责任的智能合约游戏开发商,都应该提高安全意识,加大安全投入。

参考文献

以上数据均由安比(SECBIT)实验室提供,合作交流请联系info@secbit.io。


安比(SECBIT)实验室

安比(SECBIT)实验室专注于区块链与智能合约安全问题,全方位监控智能合约安全漏洞、提供专业合约安全审计服务,在智能合约安全技术上开展全方位深入研究,致力于参与共建共识、可信、有序的区块链经济体。

安比(SECBIT)实验室创始人郭宇,中国科学技术大学博士、耶鲁大学访问学者、曾任中科大副教授。专注于形式化证明与系统软件研究领域十余年,具有丰富的金融安全产品研发经验,是国内早期关注并研究比特币与区块链技术的科研人员之一。研究专长:区块链技术、形式化验证、程序语言理论、操作系统内核、计算机病毒。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值