区块链研究实验室-君士坦丁堡硬分叉后的可重入漏洞-part1

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

即将到来的以太坊君士坦丁堡升级将降低部分SSTORE指令的gas费用。然而,这次升级也有一个副作用,在Solidity语言编写的智能合约中调用address.transfer()函数或address.send()函数时存在可重入漏洞。在目前版本的以太坊网络中,这些函数被认为是可重入安全的,但分叉后它们不再是了。

重入攻击

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

当攻击者通过递归调用目标的撤销功能从目标中抽取资金时就会发生重入攻击,就像DAO的情况一样。当合同在发送资金之前未能更新其状态(用户的余额)时,攻击者可以不断调用撤销功能以消耗合同的资金。只要攻击者收到以太坊,攻击者的合同就会自动调用其回退函数function(),该函数被写入以再次调用撤销函数。此时攻击已进入递归循环,合同的资金开始向攻击者发出冲击。由于目标合同因调用攻击者的后备功能而陷入困境,因此合同永远无法更新攻击者的余额。目标合同被认为没有任何问题......要明确,回退函数是合约的功能,只要合约收到以太坊和零数据,它就会自动执行。

漏洞原理分析

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

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

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

区块链研究实验室-君士坦丁堡硬分叉后的可重入漏洞-part1_第1张图片

黑客首先给“攻击合约账户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(因为两个账号都是他自己的),而且可以无限次数攻击,直到把“共享支付合约”里的钱偷光,太可怕了。。。

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

实际上在此之前,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指令的高额油费消耗来避免重入攻击的。

区块链研究实验室-君士坦丁堡硬分叉后的可重入漏洞-part1_第2张图片

但是,这一保证被EIP 1283打破了。

  •  No-op状态:收取200gas

  • Fresh状态:

  • 如果原始值是0,收取20000gas

  •  否则,收取5000 gas。如果新值是0,退还15000gas

  • Dirty状态:收取200gas,并检查下面2个条件:

  • 如果原始值不是0

  • 如果当前值是0(说明新值不是0),收回退还的15000gas

  •  如果新值是0(说明当前值不是0),退还15000gas

  •  如果原始值等于新值(被reset回原始值了)

  • 如果原始值是0,退还19800gas

  • 否则,退还4800 gas

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

 

本文转载公众号:区块链研究实验室

你可能感兴趣的:(区块链,智能合约,以太坊,Hyperledger,区块链技术)