30

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

 5 years ago
source link: https://blog.csdn.net/TurkeyCock/article/details/86547899?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

上一篇文章给大家介绍过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块钱是哪里来的呢?当时就是从其他充饭卡的人那里“偷”来的啦~

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

Ajmm6jj.png!web

黑客首先给“攻击合约账户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指令的高额油费消耗来避免重入攻击的。

myiIZ3B.jpg!web

但是,这一保证被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:

iumIBb6.jpg!web

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

更多文章欢迎关注“鑫鑫点灯”专栏: https://blog.csdn.net/turkeycock

或关注飞久微信公众号: Nrq2eyJ.jpg!web

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK