以太坊君士坦丁堡漏洞分析

Posted TurkeyCock

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了以太坊君士坦丁堡漏洞分析相关的知识,希望对你有一定的参考价值。

这两天关于以太坊延迟君士坦丁堡升级的报导铺天盖地,可惜到现在都没看到一篇能把这个漏洞讲透彻的,就由我来给大家解密吧。

上一篇文章给大家介绍过EIP 1283,是为了优化SSTORE指令的gas计算方式的,这次的漏洞就出在这个EIP上,可能会导致“重入攻击”。

1.什么是“重入攻击”

所谓“重入攻击”,指的是在同一笔交易中,合约A调用合约B,而合约B又反过来调用合约A的现象。

这种情况是必须被禁止的,因为合约A可能需要依赖自身的一些状态来给其他账户转账,而如果在这个过程中间,合约B又反过来调用合约A并修改了状态,有可能导致状态紊乱,黑客可以通过这个漏洞“偷”走别的账户的钱。

2.漏洞原理分析

ChainSecurity组织最先向以太坊团队提交了这个漏洞,他们设计了下面这个场景:

这是一个“共享支付合约”,其实就跟我们平时去食堂刷饭卡是类似的。我去管理处办了张饭卡,往里面充了100块钱,现在这些钱100%都是属于我自己的。然后我去吃了顿饭花了20,这时候我就更新一下卡里的参数:这张卡里的钱80%归我,剩下归食堂。然后我突然接到通知,公司要搬家了,这张卡用不上了,于是我就去管理处退卡,管理处的会计就根据这个80%的比例,退我100*80%=80块钱,还有100*(1-80%)=20块钱打到食堂帐上。请注意,这个操作必须是原子的,假如他先退了80块给我,然后我在他给食堂打钱之前,把参数改成了0%,他就会给食堂帐上打100*(1-0%)=100块钱!也就是说,虽然我只充了100块,但是我跟食堂加起来却得到了180块钱,这多出来的80块钱是哪里来的呢?当时就是从其他充饭卡的人那里“偷”来的啦~

具体到代码层面,攻击的流程参见下图:

黑客首先给“攻击合约账户A”和一个“普通账户B”之间建立一条共享支付通道(办张卡),请注意,这两个账号都是黑客自己控制的。

然后黑客操纵账户A调用deposit()方法往“共享支付合约”里充了一些钱(比如100 ETH)。

接着,黑客调用攻击合约的attack()方法,这个方法会接连执行下面两个调用:

  • 调用“共享支付合约”的updateSplit()方法,把分配参数更新成100%(没毛病,这些钱都是账户A的)
  • 调用"共享支付合约"的splitFunds()方法销卡退款(理论上应该给账户A转100 ETH,账户B转0 ETH)

"共享支付合约"先给账户A转100 ETH,调用账户A的transfer()方法。但是账户A是个合约,并且没有transfer()方法,因此会调用到它的fallback方法。

在合约A的fallback方法里,它再次调用了“共享支付合约”的updateSplit()方法,把分配参数更新成了0%(这一步是通过内联汇编完成的,比较省gas,具体原因后面会说)。

接着,“共享支付合约”会继续给账户B转账,但是由于分配参数变了,现在账户B占100%了,所以它又给账户B转了100 ETH。

可以看到,黑客每发起一次攻击,都可以赚100 ETH(因为两个账号都是他自己的),而且可以无限次数攻击,直到把“共享支付合约”里的钱偷光,太可怕了。。。

3.为什么升级前没有这个漏洞

实际上在此之前,EVM是考虑过重入攻击问题的,在合约A调用合约B时,合约B的代码只能执行一些非常简单的操作(比如发送一个event,对应LOG指令),消耗的总gas不能超过2300,这被称为“调用津贴(CallStipend)”。由于CALL指令本身需要消耗700 gas,所以实际上可用的gas只有1600,这对于普通指令足够用了,比如LOG指令每个字节只需要消耗8 gas,因此最多可以写200个字节来记录这次调用事件。但是,SSTORE指令需要消耗5000 gas,因此如果合约B中使用了SSTORE指令,会导致Out of Gas从而中止交易的执行。因此,EVM是依靠SSTORE指令的高额油费消耗来避免重入攻击的。

但是,这一保证被EIP 1283打破了。我们先来回顾一下EIP 1283对SSTORE的gas计算方法(不熟悉的朋友请参考前一篇文章):

  • No-op状态:收取200 gas
  • Fresh状态:
    • 如果原始值是0,收取20000 gas
    • 否则,收取5000 gas。如果新值是0,退还15000 gas
  • Dirty状态:收取200 gas,并检查下面2个条件:
    • 如果原始值不是0
      • 如果当前值是0(说明新值不是0),收回退还的15000 gas
      • 如果新值是0(说明当前值不是0),退还15000 gas
    • 如果原始值等于新值(被reset回原始值了)
      • 如果原始值是0,退还19800 gas
      • 否则,退还4800 gas

黑客发起攻击时,先调用一次SSTORE把分配参数从0更改为100,进入Fresh状态,收取20000 gas。然后在fallback函数中再次把分配参数从100更改为0,此时会进入Dirty状态,只会收取200 gas,并退还19800 gas。这一数值远远低于1600 gas,因此黑客就可以成功地发起重入攻击。

4.如何重现这个漏洞

ChainSecurity在github上公开了攻击示例代码:https://github.com/ChainSecurity/constantinople-reentrancy

可以在Ganache上模拟测试攻击过程:

ganache-cli --hardfork=constantinople
truffle test

可以看到正常调用后余额几乎没有什么变化,而发起重入攻击后账户增加了1 ETH:

据称,目前为止还没有发现合约因为该漏洞而造成损失,但是这显然是一个极大的隐患。幸好,该漏洞在君士坦丁堡升级之前被发现,以太坊团队决定推迟升级时间,从这一点也可以看出项目社区化运营的巨大力量~

更多文章欢迎关注“鑫鑫点灯”专栏:https://blog.csdn.net/turkeycock
或关注飞久微信公众号:

以上是关于以太坊君士坦丁堡漏洞分析的主要内容,如果未能解决你的问题,请参考以下文章

针对网站漏洞怎么修复区块链漏洞之以太坊

以太坊君士坦丁堡:是利好?-千氪

以太坊的“分片”是指啥?

以太坊的“分片”是指啥?

君士坦丁堡分叉引起的安全问题

以太坊proxy合约