智能合约安全事故回顾(4)-针对Fomo 3D的DDOS攻击事件
上一章有介绍关于DOS的攻击。DDOS是DOS攻击的一个实现方式,如果把DOS理解为是一次野蛮的抢劫,那么DDOS可以说是一次针对服务器的群殴了。
DDOS全称是分布式拒绝服务攻击,攻击者往往通过控制一些“肉鸡”来针对某个服务器发起大规模的流量攻击。这些肉鸡一般是攻击者事先准备好的一些傀儡机,傀儡机的多少决定了攻击的强度。在区块链世界中,也存在一些通过DDOS攻击导致的安全时间。本文介绍的就是注明的Fomo 3D游戏被DDOS攻击导致崩盘的事件。
事件介绍
Fomo3D是一款运行在区块链上的“庞氏骗局”游戏。游戏中刚开始时就存在一个倒数24小时的计时,玩家通过花费eth来购买游戏中的key,这些花费的eth会被记入奖池,并且延长三分钟倒计时时间。后面加入的玩家需要支付高出前一次购买的价格来购买key,而当没有人继续购买key时,倒计时就会慢慢结束,游戏也随之结束。最后一个购买key的玩家会获得奖池中48%的奖金额,也就是奖池中的大部分eth。2018年8月22日,Fomo3D游戏第一轮结束,一个钱包开头0xa169的用户获得10469eth的奖金。这看起来没什么问题,然而人们后来发现这位玩家早就通过DDOS攻击的手段,让自己有很大概率成为最后的胜利者。人们发现在最后开奖的三分钟内,以太坊总共完成了12个区块链的打包,然而却没有一个关于Fomo3D合约的交易,事实上真的没有玩家发起交易吗?
攻击方式/原理介绍
那么我们前面讲过DDOS就是一次分布式的拒绝服务攻击。这个项目中,黑客的思路很简单:在黑客购买key之后,让服务器不接受其他人的购买请求,等待时间倒数,那么最后黑客就理所当然的成为了最后的赢家。
那么如何让服务器不接受其他人的请求呢?在这里需要需要了解的一个知识点是,以太坊大概14S会完成一次出块,矿工会将接收到的交易筛选打包,而最后完成pow的节点获得出块权。由于每个交易都带有以太坊规定的手续费gas,那么矿工为了自身的利益,自然回选择手续费高的交易优先打包。而每笔交易也可以设置gas limit(手续费上限),如果最后消耗的gas超过gas limit,交易就会失败进而回滚。
在回到攻击事件上来,聪明的读者应该会想到,攻击者正是通过发送大量的超高gas的交易,让矿工优先打包自己发送的无用的交易来让其他的正常交易不能被打包到块中。具体过程如下:
1.攻击者首先在恰当的时机买入key,让自己成为最后一个购买者。
2.攻击者开始进行DDOS攻击:通过提前准备的合约发送大量的交易,这些交易都有一个共同的特点就是高额的手续费。
3.攻击者同时不断调用Fomo3D合约的getCurrentRoundInfo方法(获取当前最后一个购买者地址),通过该方法判断自己是不是最后一个购买者。因为第二步只算是一种拥堵的方式,谁也不能保证没有其他交易被打包。
4.到这里,读者应该发现攻击者并不是一味的无脑花费巨额的手续费让区块打包。而是在最后的时间内(设定某个预置),通过第三步判断,如果自己不是最后的购买者,攻击者会回滚自己的交易,只需要消耗少量的gas。而如果自己是最后的购买者则让交易失败,但是花费大概的gas给矿工,吸引矿工打包。完成这一步的主要依据是assert()函数的妙用。以太坊中有assert函数和require函数,两者的作用都是判断某个条件是否满足。而区别在于require如果不满足会把gas退还给调用者,交易失败。而assert则会标记交易失败,但是会扣除gas。
5.到这里攻击基本完成,攻击时间段内其他关于Fomo3D游戏的交易通过正常的方式提交,矿工大概率不会打包这些交易。对于我们来说,就像是服务器拒绝服务。最后攻击者实现了一次巧妙的“小投资,高回报”的DDOS 攻击。
防范
讲到如何防范就得知道为什么会有这个漏洞可以被攻击。我们讲区块链是去中心化的,Fomo3D游戏一时火爆无外乎是人们觉得公开透明,数据无法被篡改。然而去中心化的区块链又跟“逐利”的人是冲突的,例如矿工总是会选择手续费高的交易进行打包,这也无可厚非。所以在防护时,并不是像之前那样修改某段代码就能解决。针对此次DDOS攻击:一方面应该在getCurrentRoundInfo接口屏蔽掉合约对该函数的调用,这样攻击者就不能完成自动化的攻击。同时提高攻击成本,我们在前面看到最后的获胜者只获得了48%的奖池奖金,那么剩下的部分可以用来作为交易费用的一部分,并且可以在规则中修改,最后一个购买者有大概率获奖,然而也并非百分百。当然这些都是在明确攻击手段的情况下给出的防范,真正安全的合约还是应该加强安全的同时也能在出现问题的时候既是修复。我们知道,Fomo3D游戏的第二轮的奖金得主同样被黑客摘走。
安全防范,任重而道远。