前言
最近以太坊也算是问题不断,多个游戏都相继被爆出了黑客攻击,首当其冲的当然还是最近比较火爆的类Fomo3d的游戏,比如last winner所遭遇的薅羊毛的攻击,虽然相关的攻击手法早在一个月前就已经有相关的披露,但是last winner并没有开源,似乎也没有采取什么安全措施,加上黑客的手法也进行了升级,导致了大量的eth被黑客通过空投的方式拿走,不过这并不是今天的重点,这部分可以看看相关的解析
另外一个比较劲爆的消息当然就是fomo3d首轮的结束,获胜者成功夺得了高达一万eth的奖金,而背后的黑客所使用的手法正是阻塞以太坊网络,不让fomo3d相关的交易能够被打包进区块,于是作为最后的购买人的他便成功将巨额的奖金收入囊中,可以说是利用对以太坊网络的DDOS完成了这次攻击,在这里我们就来简单谈谈以太坊中的DDOS
针对以太坊网络的DDOS
其实说来也有趣,本身以太坊设计的时候就想过要抗衡DDOS的,其中的gas机制有一部分作用也是为了防止DDOS,抬高DDOS所需的成本,不过随着以太坊的不断发展现在也暴露出越来越多的问题
垃圾交易导致的DDOS
这个问题差不多是两年前提出来的,主要存在的问题还是在EXTCODESIZE这个操作码上,熟悉智能合约的人应该也知道这是用来读取合约的code的大小的,所以其涉及到了相应的磁盘操作,但是它所需的gas又非常少,这就导致了恶意的攻击者可以在交易里实现调用很多次这个操作码,只要这些操作的gas加起来不超过区块的gas limit即可,得益于此操作码的极少gas消耗,当时的攻击交易在每个区块调用了该操作码近50000次,因此这一个区块内的交易所占用的计算时间就被大大延长,从而导致了整个以太坊网络的瘫痪,在以太坊官方博客上也有对应的说明和相应的处理
说到这里我们还是不得不提一下以太坊特色的gas机制,对于它的存在目前确实也是褒贬不一,我也不作过多的评价,我们主要来关注一下要注意的问题
我们知道只要发送交易就必定会产生gas费用,而我们在发送一个交易时其实也有两个关于gas可选项,即gas Price和gas limit,gas Price即我们愿意为每个gas所支付的单价,而gas limit则为我们设定的gas的最多用量,也即我们直接发送过去的gas数量,然后在节点处打包后计算出了该交易中的操作所消耗的gas数,即gas used,然后gas used*gas Price就是我们实际支付的tx fee的数目,剩余的gas则将返还给我们的账户,如果设定的gas limit还不够操作所需gas的话该交易就失败了,对应的操作也将回滚,但是交易仍然会打包进区块,当然已经给了的那部分gas就别想了
所以有时候我们碰到发送的交易失败可能就是设定的gas limit少了点,你可能会觉得直接都把gas limit设置的特别大不就什么事也没有了,但是这样的做法可能并不会得到矿工的好感,因为以太坊还有一个特点就是它的区块gas limit,不同于比特币是直接限定区块的大小,以太坊是通过区块内交易所使用的gas来对区块进行限制,对应的限制值就是区块gas limit,矿工在选择要打包进区块的交易时必须保证所有交易使用的gas值要小于区块的gas limit,所以一个区块内所能容纳的交易数目还是比较有限的,同时一般的交易的gas limit在21000左右,因为这也是很多客户端默认的gas limit,当出现一个gas limit特别大的交易时矿工可能并不会给与其优先权,因为可能打包了这个交易后实际发现它实际所使用的gas也将那么点,大部分都返还回去了,这样它倒不如多打包几个gas limit较小的交易,能赚取的gas可能更多,所以说较高的gas limit反而可能导致你的交易的效率变低,如果真的希望交易能较快地打包进区块,应该提升的值还是gas price,这样最终矿工所得到的tx fee才会变高,也就相应的能取得优先权
那么回到这个垃圾交易的DDOS攻击上来,官方的缓解方案其实也是跟区块的gas limit有关,一方面是增加对应的操作码所需的gas,另一方面则是减小区块gas limit的大小
我们可以来看看当时区块gas limit的变化
本来区块的gas limit是在470万左右,之后为了缓解攻击下调到了150万左右,然后又调整到200万,这其实也很好理解,目的就是为了减少每个区块内的交易数目,避免出现一个区块在计算交易的过程中出现阻塞几十秒的情况,虽然说这么做也还是治标不治本,不过也是为以太坊对该漏洞的修补争取缓冲时间,其实本身区块的gas limit也是有动态的调节机制的,它也是在根据前面的区块的gas used进行上下波动,不过在这种局面下它还是失去了调节的能力,之后随着以太坊又进行分叉来对漏洞进行了修补,区块的gas limit又开始了不断的增长,一直到目前的800万,当然,太高的话也还是会影响性能,因为这样每个区块里的交易数就变多了,现在的800万的gas limit也已经稳定了很久了
虽然现在这种DDOS方式已经被修复,不过还是值得拿出来说道说道
爆发性的交易导致的DDOS
其实严格说来这种可能也谈不上DDOS攻击,这就相当于双十一的时候淘宝也爆卡一个道理,不过它也确实造成了以太坊的堵塞,这种情况在最近一年来倒也变得愈发频繁
我们不妨先来看看近一年来gas Price的变动
其实平均设置的gas Price就在一定程度上反应了以太坊的堵塞程度,因为当等待的交易过多时想让自己的交易被矿工优先选中就必须提高gas的价格,否则你的交易可能就一直在交易池里躺着了,这样就导致了以太坊堵塞时gas Price的飙涨
从图中我们可以大致看出几个上涨点,首先就是17年末到18年初的阶段,有印象的话应该记得那时候正是以太猫流行的时候,当时它名气那么大也正是因为它的存在一度堵塞了整个以太坊网络,数不清的交易躺在交易池里打包不了,带来的也是gas Price的飙涨,大家都在游戏里抢猫,在上线不到两周的时间里便交易了七万只,交易额达到了七千多万,让人不禁感叹资本的力量,那应该也是以太坊首次出现一个如此流行的游戏,当时也算是最接近于杀手级的应用了,意义也还是很重大的
之后有一个比较明显的涨幅就是来自于不久前也就是七月份爆火的FCoin交易所,因为其特殊的排名模式,也就是对对应的代币采用一账户一票的方式进行排名,导致了对应的项目方疯狂地开账户并进行投票,这就造成了大量的交易,直接堵塞了整个以太坊网络,这样的行为也受到了多方的谴责,毕竟这样的行为除了扩大了其影响力并没有任何意义,还造成了gas Price的居高不下,偏偏对于这样的无赖方式还没有办法反抗,这次的事件也再次对以太坊的性能问题敲响了警钟,毕竟它确实是很容易引发大面积的堵塞,可以想象以太坊的网络带宽也就是每15秒800万gas左右,还有计算性能的限制,v神也在不断寻求解决方案,其设想的分片技术也是为了提升以太坊性能来减少拥堵情况的发生,不过却是会牺牲部分去中心化的特性,这倒是有点类似于EOS
最后,离我们最近的一个峰值表示的就是最近仍然火爆的类Fomo3d游戏了,影响到甚至阻塞网络的正是风头正劲的last winner游戏,在开始时他们团队还准备了2大量eth进行刷单,强行抬高了交易量,吸引用户注资,当时其交易数据甚至夸张到3天吸金近1亿,这在当时的市场环境下确实是过于夸张,也一度造成了以太坊网络的拥堵,而且在这场游戏的背后也是疑云重重,从其营销策略来看应该还是针对国人的,只能说对待这样的游戏必须得多加小心
关于gas Price的实时情况我们可以在这里观察到,而其在最近一段时间的变动情况也可以在此处得到很好的分析
高gas交易导致的DDOS
这种类型的DDOS涉及的就是前几天爆出的Fomo3d的获胜黑客所使用的攻击手法了,这名黑客使用这种手法阻断了以太坊网络近三分钟,使得与fomo3d相关的交易都无法打包进区块,于是作为最后一个购买者的他便成为了最终的获胜者,获取了巨额的奖金
这里涉及到的还是我们前面提到的区块的gas limit的限制,目前每个区块的gas limit是800万左右,攻击者要做的就是想办法让自己的交易耗尽区块的gas limit,努力让区块内尽量只存在自己的交易,这部分的优先权可以通过提高gas Price取得,而对于gas的消耗则涉及到另一个操作,也就是对assert的应用
熟悉智能合约相关操作的人应该也都知道assert与require类似,都可以用来进行错误的处理,不同之处在于require触发使用的是0xfd操作码,也就相当与调用了revert,它会回退合约状态并返还剩下的未消耗完的gas,而assert则霸道得多,触发过后并不会返还剩下的没消耗的gas,你只能当贡献给矿工了,你可能会奇怪既然如此为何还要使用assert,实际上主要还是在于使用的场景不同,对于require的应用场景来说,哪怕其触发了,表示的其实也是正常的合约状态,因为其一般是检查用户的输入抑或是执行的状态条件,而assert的触发场景则一般代表着代码可能存在某些问题,你可以把这理解为一个规范,遵循这样的规范的代码就可以毕竟容易的使用分析工具进行检查,关于二者更详细的分析比较可以参加此处的资料
对于这一特性,我们可以简单地来看看效果
部署一个简单的测试合约
contract C{
function requires(){
require(false);
}
function asserts(){
assert(false);
}
}
我们将其部署到ropsten测试链上,因为测试链的区块gas limit在470万左右,我们设置一下发送交易的gas limit,对于调用asserts的交易,我们直接将其gas limit设置为465万,至于调用requires的交易我们使用300万即可,然后我们来看看结果
对比很明显,调用asserts的交易直接耗光了我们设置的gas limit,而requires仅使用了所需的gas,我们再看看asserts所在交易的区块情况
果然,这个区块仅有两个交易被打包,我们再看requires所在区块
可以看到该区块打包了一百多个交易,那么至少我们是阻塞了asserts所在的区块了,不过事实上这种单个巨额gas limit的堵塞效果并不是很好,如果你也进行尝试的话应该能注意到这个交易也是等待了一段时间才被打包,比require那个交易要慢很多,要知道我设置的gas Price已经很大了,其实原因前面也提到了,这种单个的超高gas limit的交易矿工并不是很待见,所以比较好的办法其实是将其拆分为几份,攻击fomo3d的那位黑客也明白这一点,所以他的攻击的交易大都集中在两三百万gas limit左右
这是当时黑客攻击时所产生的某个区块,可以看到该区块的交易数量仅为10个,而正常区块下一个区块的交易数目是有好几百的,其中显示失败的那三个交易其实就是黑客发出的交易,目标都是一致的,也就是黑客部署的攻击合约,其实这目的也就是利用assert消耗gas
我们来看看这几个交易分别消耗了多少gas
很有意思,分别是20万,330万和420万,加起来就是770万,顶去了整个区块gas使用量的绝大部分,在整个攻击过程中黑客其实也在不断调整,修改gas使用量,其实前面也经历了很多的失败,不过在最后这三分钟他还是取得了成功,可能还是有一点运气的成分吧,这整个调整的过程也挺值得深入研究的,至于这次攻击手法的详细解读可以参见此处的分析
随着这种攻击方式的成功应用,很多黑客团体也着手展开了对类fomo3d游戏的攻击,目前也已经有了不少成功的例子,不久前last winner 第二轮也宣告结束,这是否又意味着这类游戏的新的篇章,在这种形势下确实是很难说清了
针对智能合约的DOS
这里要说的就不是ddos攻击了,关于这部分内容我感觉真正谈得上可攻击可利用的还是对于区块gas limit的限制利用,因为区块得gas的800万限制,如果想办法让合约的部分操作的调用所需的gas超出了800万的限制,那么这个合约自然就废了
比如下面这个合约的主体部分
contract DistributeTokens {
address public owner; // gets set somewhere
address[] investors; // array of investors
uint[] investorTokens; // the amount of tokens each investor gets
function invest() public payable {
investors.push(msg.sender);
investorTokens.push(msg.value * 5);
}
function distribute() public {
require(msg.sender == owner);
for(uint i = 0; i < investors.length; i++) {
transferToken(investors[i],investorTokens[i]);
}
}
}
攻击者就可以通过创建大量的investor来使得最终调用distribute函数时所需的gas超过了区块的gas limit,从而使得这个合约作废,当然这也只算是使智能合约无法提供服务的方法中的一种,其他的利用可见此处的资料
写在最后
最近以太坊所出现的一系列问题还是让人非常担忧,所涉及的金额也越来越大,这样其实是侵犯了很多普通用户的权益的,希望在后面以太坊团队能解决以太坊目前存在的性能问题,避免拥堵的再次发生