头条|区块链研究实验室-君士坦丁堡硬分叉后的可重入漏洞
摘要:这两天关于以太坊延迟君士坦丁堡升级的报导铺天盖地,可惜到现在都没看到一篇能把这个漏洞讲透彻的,就由我来给大家解密吧。即将到来的以太坊君士坦丁堡升级将降低部分SSTORE指令的gas费用。然而,这次升级
这两天关于以太坊延迟君士坦丁堡升级的报导铺天盖地,可惜到现在都没看到一篇能把这个漏洞讲透彻的,就由我来给大家解密吧。
即将到来的以太坊君士坦丁堡升级将降低部分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块钱是哪里来的呢?当时就是从其他充饭卡的人那里“偷”来的啦~
具体到代码层面,攻击的流程参见下图:

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

但是,这一保证被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,因此黑客就可以成功地发起重入攻击。
这个代码的问题何在?
下面代码是君士坦丁堡硬分叉之前没有可重入漏洞的一个代码段,它会在分叉后导致可重入性。我们的Github页面(https://github.com/ChainSecurity/constantinople-reentrancy)展示了包括攻击合约在内的完整源代码。

这个代码会被一种意想不到的方式攻击:它模拟了一个安全的资金共享服务。双方可以共同接收资金,决定如何分成,如果他们达成一致-,则可以获得支付。攻击者将创建一个对,其中第一个地址是下图列出的攻击合约地址,第二个地址则是任何攻击者账户。攻击者将在这个对存入一些以太币。

当攻击者在自己的合约中调用attack函数时,在同一个交易中就会发生以下事件:
1.攻击者利用updateSplit 函数设置当前的分成,从而确保之后的升级将会降低费用。这是君士坦丁堡升级带来的效应。攻击者通过这种方式设置分成,他的第一个地址(即攻击合约地址)就能接收到所有的以太币。
2.攻击合约调用splitFunds 函数,该调用将进行检查*等,并通过transfer()函数把其中存储的所有以太币发送到攻击合约。
3.通过回退函数,攻击者再次执行分成,这一次将所有以太币分配给他的第二个地址,即其自己的账户。
4.通过不断执行splitFunds 函数,在第一个地址中存储的所有以太币都被转移到了攻击者账户中。
简单来说,攻击者可以不断从PaymentSharer合约中盗取其他用户的以太币。
为什么这种漏洞变得可利用了?
在君士坦丁堡硬分叉前,一个存储指令至少需要5000 gas 手续费。这远远超过了在利用transfer()函数或send()函数调用合约时可用的2300 gas津贴。
在君士坦丁堡分叉后,会改变“脏(dirty)”存储器槽(storage slot)的存储指令只需200 gas。如果在交易执行期间对一个存储器槽做出修改,那么这个存储器槽就会被标记为“dirty”。如上所示,这可以通过在攻击合约调用一些改变所需变量的公共函数来实现。然后,通过使被攻击合约调用攻击合约,例如使用msg.sender.transfer(…)函数,攻击合约就可以利用gas津贴成功操纵这个被攻击合约的变量。
被攻击合约必须满足以下先决条件:
1.首先必须存在函数A,在调用transfer/send函数后将执行状态修改的指令。有时这种指令可能是不明显的,例如,第二次transfer的调用或与另一个智能合约的互动。
2.必须存在攻击者可调用的函数B ,能够实现(a)改变状态;(b)其状态改变与函数A的状态改变冲突。
3.执行函数B 的手续费必须小于1600gas (2300 gas 津贴?—?CALL指令花费的700 gas)。
我的智能合约是否易受攻击?
要测试一个智能合约是否易受攻击:
(a)检查在transfer事件后是否存在其他指令。
(b)检查这些指令是否修改了存储状态。最常见的修改是通过分配一些存储变量。如果你在调用另一个合约,比如代币的transfer函数,检查并列出所有被修改的变量。
(c)检查在你的合约中是否有任何非管理员可访问的方式使用了这些变量。
(d)检查这些方式本身是否修改了存储状态。
(e)检查这种方式的手续费是否低于2300 gas,记住,SSTORE指令理论上只需要花费200 gas。
如果上述所有情况皆符合,那么攻击者很可能可以对你的合约发起恶意攻击,使其陷入糟糕的状态。总的来说,这也再次提醒了我们“检查-生效-交互”(Checks-Effects-Interactions)模式的重要性。
本文转发:区块链研究实验室
- 免责声明
- 世链财经作为开放的信息发布平台,所有资讯仅代表作者个人观点,与世链财经无关。如文章、图片、音频或视频出现侵权、违规及其他不当言论,请提供相关材料,发送到:2785592653@qq.com。
- 风险提示:本站所提供的资讯不代表任何投资暗示。投资有风险,入市须谨慎。
- 世链粉丝群:提供最新热点新闻,空投糖果、红包等福利,微信:juu3644。

币圈观察



