转载 标题:  Fomo3D 游戏的第一轮是如何结束的

本文作者:风静縠纹平(区块链布道者)
原文地址:https://www.jianshu.com/p/3e9dfc3200ca

以太坊网络上备受瞩目的游戏Fomo3D(Fomo3D:Long)其第一轮在北京时间8月22日下午3点左右结束了。最终地址为0xa169…的玩家获得了 10469.66Eth的奖金,其取款交易被记录在了6191962区块中,该玩家在游戏中的总投入不到0.8Eth。那么,是不是这个玩家真的是靠运气“中了大奖”呢?当然不是,这是个有计划、有预谋的、精心设计的“技术性攻击”所取得的胜利结果。

本文将为大家讲解这次“技术性攻击”的原理、过程和攻击中的关键技术细节。前提条件是假设读者了解以太坊、智能合约、矿工/矿池、交易打包确认以及gas等术语的基本概念。

从游戏设定看达成”攻击“所需的必要条件

Fomo3D是近一个多月以太坊上最火爆的应用,也是个赌博游戏。本文的目的是做技术分析,这里只介绍其结束的设定:

  • 游戏启动后从24小时开始倒计时;倒计时结束时,最后一个够买key的玩家将获得奖池中48%的奖金;
  • 每有一个玩家购买key,倒计时会增加30秒。

因此,获胜条件很简单:在自己购买key之后到游戏倒计时结束,不再有其他人购买key,即可获胜。在现实世界中,要做到这点不那么容易,除非所有玩家都没钱了;但在区块链的世界中,具体到以太坊上,是可以通过“技术手段”做到不让其他人购买的(也就是不让其他人的“购买交易”得到“网络确认”)。这就是“拒绝服务攻击”(Denial of Service,DoS)。

攻击的原理

在目前成熟的Web服务技术里,制造DoS攻击一般是通过大量的并发请求和或大数据量的独立请求,将Web服务的带宽/服务资源占满,而使其无法再相应正常的数据请求。在以太坊中,则可以通过制造大量的“垃圾合约调用”来达到同样的效果。

具体机制:在矿工/矿池节点上,通常都会有一个交易池(transaction pool),网络上广播的所有新的交易都会被首先加入这个“池”,而后再由矿工/矿池选择那些“经济性更好”的交易优先打包确认。“经济性”,即由交易发送者在交易数据中指定的gasPrice,gasPrice越高,执行交易所附带的合约代码的执行费用也就越高,而这些费用通常是会作为手续费支付给矿工的。矿工/矿池会从交易池中选取那些gasPrice明显高于其他交易的交易来优先打包执行(确认)。并且,矿工并不能从技术上判断一个交易中附带的程序代码是否是“垃圾合约调用”(它们也没有这个“责任”),它们仅仅选取那些执行费用更高的交易来优先执行。基于这个原理,就允许了攻击者通过调高包含了“垃圾合约调用”的交易的 gasPrice,来在短时间内用这些“无效交易”占用区块的可用gas,以使其他“正常交易”无法被打包进区块。

几个基础科普知识:

  • 以太坊中的区块可包含的交易(计算量)是由区块的gasLimit来控制的,而并不是像比特币那样用数据大小来限制;以太坊中目前区块的 gasLimit上限是800万gas,矿工可以做5%以内的上下浮动;区块内能包含多少交易,是看这些交易执行所消耗的总gas是否达到这个区块的gasLimit;
  • 以太坊中执行交易的费用,是用交易基础执行费用的21000gas,加上交易中附加的代码的字节大小的费用(这里有一个折算公式,不详细讲了)以及实际执行代码所消耗的gas的总和乘以交易中指定的gasPrice来计算的;交易费用会从交易发送者账户中自动扣除;如果账户余额不足,交易不会被打包进区块;
  • 以太坊中的交易的实际执行所要消耗的gas是可以根据交易执行时的“世界状态”明确知道的,也就是这个交易的实际执行费用是明确知道的,矿工就是据此来判断打包交易的“经济性”。

在Fomo3D游戏的后期(即奖池金额已经很高),大多数玩家都会选择在倒计时的最后数分钟内才去购买key,以让游戏能继续下去。这时,如果有一个机会,在攻击者自己购买了key之后(这只会给剩余时间增加 30 秒),能在其后数分钟内让网络不再确认其他人的购买交易,攻击者就可以让游戏结束从而赢得大奖。

“30秒规则”。以太坊网络目前是基于PoW共识的,节点之间是通过“竞争”来决定记账权,这会导致区块链末端的“不稳定”,也就是会分叉。所以实际上某个交易会包含在哪个区块是可能在短时间内变化的,但基于过往的经验数据,如果合约中用区块的时间戳来判断,那么这个时间的精度大概会有30秒的误差,这就是所谓的“30秒规则”。

Fomo3D合约中的结束时间是使用now(也就是当前区块的时间戳)来判断的,所以如果要攻击的话,一定要多攻击30秒。比如攻击者在倒计时2分钟时购买key,这会使倒计时增加30秒,然后基于30秒规则,就需要保证在之后的3分钟内没有其他玩家的交易被打包确认。实际的攻击也是这样进行的。

攻击的过程

下面我们就来根据区块链浏览器中可以查到的实际数据来看看这个攻击是如何发生的:

  • 区块号6191896:确认了一个由0xa169…到Fomo3D:Long的交易,调用了buyXid函数(即购买了若干key);这个区块的时间戳是06:48:22(UTC)。
  • 区块号6191897到6191902:攻击者开始使用“垃圾合约调用”来填充区块(大概占用了这几个区块中的一半左右的可用gas),但没有刻意调高gasPrice(使用的是平均水平,20GWei左右),这是个非常有耐心、也非常大胆的处理,在看到6个区块的时间内没有其他人购买key之后,攻击者知道机会来了!
  • 区块号6191903到6191908:从6191903开始,攻击者将“垃圾合约调用”交易的gasPrice提高到了190GWei,即平均水平的8倍以上,后续交易更是设置了500GWei的超高gasPrice,开始了真正的DoS攻击!直到6191908区块,这6个区块中只包含了不到10个简单的转账交易(即不包含合约执行的简单交易,固定消耗21000gas),其他可用gas完全被这些高 gasPrice的“垃圾交易”占用。
  • 区块号6191909:网络状况恢复正常。这个区块的时间戳是06:51:17(UTC)。在这个区块中,可以看到数个调用了Fomo3D:Long的buyXaddr 和buyXid的交易,但因为游戏合约内的时间戳判定条件已经达到游戏结束,所以这些购买就没有效果了。

值得一提的是,在区块6191907中,可以看到一个gasPrice高达 5559.7GWei的调用Fomo3D:Long的buyXaddr函数的交易,但很可惜,这个交易的gasLimit设置过低(仅设置了379000)导致发生了out of gas(即交易触发的合约执行实际gas消耗超过交易的gasLimit)的错误,而白白花费了2.1Eth的手续费,却没有抢到最终大奖!这应该是某个大神在读秒阶段发现了攻击者的企图,但由于时间过于紧张,没有将 gasLimit设置到合理范围(大概是手误少输入了一个0)。是不是有点儿看黑客大片的即视感啊?

可以看到,攻击者的计划、准备周密,很有耐心,且技术处理上几乎无懈可击,完美地达成了必要的DoS攻击(短时间内阻止了其他玩家的交易被确认),从而“技术性获胜”。

攻击中的几个技术细节

首先,可以看到在上边提到的这十几个区块中包含了很多“失败”的交易,这些失败的交易有个共同的特点,都是由Bad Instruction导致的。这里的Bad Instruction也就是以太坊协议里预设的EVM操作码0xfe(无效指令)。

Solidity语言的技术细节科普:

Solidity中有三个指令可以撤销本次合约执行中的所有状态修改并导致合约执行“异常停止”:require、revert和assert。根据EVM的指令设计,require和revert实际上都是使用了EVM操作码0xfd(停止执行,但会返还交易执行所剩余的gas,也就是会返还一部分执行费用),它们实际上都是revert,只不过require指令在执行revert之前做了一个条件检查;而assert,则在条件满足时会使用EVM操作码0xfe(无效指令,会消耗交易附带的所有可用gas)。

攻击者用来完成DoS攻击的合约源代码并不是公开的,但可以从实际的合约字节码中看到一些端倪(因为过于技术化,这里不再展开讨论)。

然后,从这些“垃圾交易”的整体设计上看,也是很有学问的。这些交易的gasLimit并不都是一样的,而是从十几万、几十万到几百万这样的离散值。这是因为在启动攻击的时候,网络状况仍然是正常状况,所以各大矿工/矿池可能已经有了打包了一半的区块,这时,当它们收到了新交易之后,除了判断经济性以外,还会判断其gas消耗能否在当前区块的剩余可用gas中包含。比如有些矿池打包的区块中已经只剩不到50万 gas,这时那些超过百万的大交易自然就不能包含进去;这样,如果没有适合的gas量的“垃圾交易”来填充,就有可能让其他玩家的正常购买交易填充进去。所以,从攻击的角度讲,这些gasLimit比较小的“垃圾交易”同样是非常重要也是非常必要的!我们不得不佩服攻击者思路的缜密。

最后,要完成这样精确的攻击,攻击者需要很多技术准备。

他们需要若干能连接到前五乃至前十矿池(或者能连接到与这些矿池节点在“网络上”非常接近的全节点),这一点非常重要。因为要实施这样的攻击,你必须具备能实时获知各大矿池节点最新区块数据的能力,以便在发起最终的DoS攻击之前能确定没有其他人的正常购买交易被打包!也就是刚刚提到的6191897到6191902区块的等待期,在越多的大矿池节点数据中得到确认,攻击成功的几率越高。

然后,在发起攻击的时候,一定要在短时间内将用来攻击的数十个“垃圾交易”同时发送到前五乃至前十矿池,让他们把这些交易加入“交易池”;以最大限度地避免因为网络延迟导致其他玩家的购买交易被某个大矿池先打包的情况;这同样对攻击的完成至关重要!

以上这两点,需要攻击者同时拥有数个可以联动的定制化的客户端,并且有相应的程序进行监控(检查区块数据)并发起实际攻击(连续发送数十个预设的交易),这大概不是通过单个客户端或者简单地用几个脚本就可以做到的。

小结

从Fomo3D:Long第一轮游戏的结束来看,虽然我们可以搞懂整个过程以及其中的技术细节,但能不能先于别人实施、考虑到尽可能多的细节、尽量提高成功的概率就是个纯粹的技术活儿了;也需要大量的时间和精力以及资金支持。不过这个例子也给了我们更大的动力去研究技术、去学习细节,只有掌握了足够多的细节才能做到一击必中!不是么?

攻击者在这次攻击中的总投入成本当然不是开头说的在合约上花的那点儿钱,这些“垃圾交易”的执行费用是非常高的,包括攻击者先前在主网上做的各种试水,总成本粗略估计在40Eth以上。貌似也不是我等屌丝能负担的啊……。

关注微信公众

更多技术、架构、管理等知识分享,请关注微信公众号:程序新视界(ID:ershixiong_see_world)
程序新视界

END
朱智胜的个人博客-微信公众号