智能合约代码层漏洞小记

2020-04-26 6,419

一.前言

这周由于兴(lao)趣(shi)趋(yao)势(qiu),阅读了几篇以太坊智能合约安全综述,对其中的几个代码层漏洞进行整理归纳,并结合安全事件与相关案例进行分析,如有不正确的地方,还望各位大佬多多指正。

二.以太坊智能合约程序编程模型

1.程序结构

以太坊上的智能合约主要是通过Solidity进行编写,Solidity是一种具有面向对象性质的弱类型语言。在以太坊上部署智能合约时,开发人员需要先将使用Solidity编写的智能合约代码编译为以太坊虚拟机可执行的二进制代码。而在编译过程中,智能合约代码的入口会插入一小段称为函数选择器(Function Selector)的代码,用以在调用函数时快速跳转到相应函数并加以执行。在编译完成后,可以通过客户端发送合约创建交易(Contract Creation Transaction),或通过其他合约执行特殊的EVM指令CREATE来部署该编译后的智能合约。


2.调用方式

在以太坊成功部署的智能合约,可以通过如下的三种方式调用合约中的公共函数(External/Public):

1) 通过客户端发送消息调用交易(Message Call Transaction),其中包含了数据参数以及目标函数签名的哈希值。

这种函数调用方式必须在交易得到确认后才能生效。并且,矿工会对该交易收取Gas来作为执行函数时所需要的代价,因此,该方式是一种写操作,即会对消息调用者的账户余额以及合约的状态进行更改

2) 通过另一个合约来间接的调用。

这种函数调用方式最终可以被追溯成另一笔消息调用交易。

3) 通过客户端调用view(或pure)函数。

这种函数调用方式并不会改变合约的状态,也不需要耗费Gas。


3.存储结构

以太坊虚拟机(Ethereum Virtual Machine,EVM)的存储方式可以分为四种:栈(Stack)、状态存储(Storage)、虚拟机内存(Memory)和只读内存。EVM是基于栈的虚拟机,栈中的每一个元素的长度是256位,基本的算数运算和逻辑运算都是使用栈完成。虚拟机内存实际上是一个连续的数组空间,用于存放如字符串等较复杂的数据结构。状态存储时key-value的存储结构,用于持久化数据。与栈和虚拟机内存不同,状态存储的值会被记录到以太坊的状态树当中。只读内存是EVM最特殊的一种存储结构,主要用于存放参数和返回值。

三.代码层漏洞

1.可重入漏洞

该漏洞主要是因为智能合约调用一个未知的合约地址,攻击者可以精心构造一份智能合约,在回调函数中加入恶意代码。当智能合约向恶意合约地址发送以太币的时候,合约上的恶意代码将会被触发,这段恶意代码通常会进行开发者意想不到的操作。有点像编程语言里面的间接递归函数调用。在臭名昭著的The DAO事件中黑客使用了这种攻击,最终导致了以太坊的硬分叉。下例根据DAO合约改进而来:

EtherStore.sol

Attack.sol

假设已有许多其他用户将以太币存入EtherStore合约,因此当前余额为10个以太币。那么当攻击者调用攻击合约的pwnEtherStore函数时,将会发生以下情况:

1) Attack.sol-第10行-调用EtherStore合约的depositFunds函数,msg.value为1 Ether(及Gas),msg.sender为攻击合约的地址。执行结果为balances[攻击合约地址]=1 Ether。

2) Attack.sol-第11行-调用EtherStore合约的withdrawFunds函数,传入参数1 Ether。

3) EtherStore.sol-第18行-在EtherStore合约的withdrawFunds函数中,前三个require将成功通过,来到第18行合约将1 Ether发回给攻击合约。

4) Attack.sol-第18行-发送给攻击合约的以太币将执行回退函数。

5) Attack.sol-第19行-EtherStore合约之前有10 Ether,现在由于转账变成了9 Ether,可以成功通过if语句。

6) Attack.sol-第20行-攻击合约再次调用withdrawFunds函数,并“重新进入”EtherStore合约。

7) EtherStore.sol-第12行-再次调用withdrawFunds函数时,由于前一次调用没有执行第19和20行,因此仍有balances[攻击合约地址]=1 Ether,withdrawalLimit变量也没有改变,所以能够成功通过前三个require。

8) EtherStore.sol-第18行-提取另外1 Ether给攻击合约。

9) 步骤(4)-(8)将循环执行,直到EtherStore.balance<= 1,即不满足Attack.sol第19行的if语句。然后终于可以执行EtherStore.sol第19和20行,设置balances和lastWithdrawTime映射,程序结束。

最终攻击者通过这笔交易,从EtherStore合约中提取了几乎全部的以太币。


2.危险的DELEGATECALL

智能合约在使用DELEGATECALL时,会调用存在于其他智能合约中的代码,但是会保持当前的上下文关系,这种特性虽然方便了开发者使用,却加大了设计安全代码库的难度,攻击者会利用保持上下文不变的特性修改原有上下文的内容,从而进行攻击。2017年,恶意用户通过此漏洞攻击Parity钱包,导致价值将近1.7亿美元的ETH被冻结。具体例子如下:

Lib.sol

UseLib.sol

Lib.sol是用来控制合约的起始时间和终止时间的,UseLib.sol是对Lib.sol代码进行使用的智能合约。

这个漏洞和智能合约对于storage变量的存储位置有关。在Lib.sol合约中变量的存储位置关系图如下:

在UseLib.sol合约中变量的存储位置关系图如下:

当运行UseLib.sol中第8行代码时,会调用Lib合约中的set_start函数,由于DELEGATECALL中的上下文不变的特性,要修改的slot[0]中的内容并不是开发者预想中的变量start,而变成了当前上下文中的变量lib。因此,修改lib地址变量后,就可为攻击者提供有效的攻击途径了。

值得一提的是,在实际的开发环境中,开发者为了兼顾代码的灵活性,经常会出现如下的DELEGATECALL滥用的写法:

这将引起public函数调用的问题,由于合约中DELEGATECALL函数的调用地址和调用的字符序列都由用户传入,那么完全可以调用任意地址的任意函数。


下面讲一下Parity钱包中曾爆出的两次安全事件。

⦁第一次安全事件

漏洞代码如下:

Parity钱包提供了一个多签合约的模板,用户使用这个模板可以很容易生成自己的多签智能合约。上面这段代码是生成多签合约的一部分,它的代码量很少,实际业务逻辑都是通过delegatecall内嵌式地调用了库合约WalletLibrary。这样做的一个主要好处是:多签合约的主逻辑(代码量较大)作为库合约只需要在以太坊上部署一次,而不会作为用户多签合约的一部分重复部署,因此可以为用户节省部署多合约所耗费的大量Gas。

上面这段代码是钱包的初始化函数和库合约的初始化函数。通过观察容易发现,我们可以DELEGATECALL调用initWallet函数,注意此处参数列表中的_owners,因为是多签合约,所以这里的address[]是地址数组,该函数原本的作用是用多重所有者的地址列表来初始化钱包,函数会继续向底层调用initMultiowned函数。经过这一步,合约的所有者就被改变了,相当于获取了Linux系统的root权限。接着便可以以owner身份调用execute函数提取合约余额到攻击者的地址了。


⦁解决方案

此漏洞产生的原因关键在于initWallet函数没有检查,以防止合约初始化后再次调用initMultiowned函数,进而使得合约的所有者被改成攻击者。因此问题的核心在于越权的函数调用,可以通过对initWallet和initMultiowned等相关函数重新定义如下权限来解决:

通过检查m_numOwners变量值,若已经初始化,则直接返回,不允许再执行initWallet等函数。


⦁第二次安全事件

漏洞代码如下:

可以看到,为修复第一次安全事件的漏洞,确保初始化逻辑只执行一次,initWallet和initMultiowned函数增加了only_uninitialized限定条件。但是在本次安全事件中,黑客直接调用了库合约的初始化方法,而对于调用者而言,这个库合约是未经初始化的,因此攻击者通过初始化参数的设置,将自己变成了owner,然后作为owner调用kill函数,抹除了库合约的所有代码。最终使得所有依赖这个库合约的用户多签合约都无法执行,代币全部被锁在合约内无法转移。


3.算数上溢/下溢

上溢/下溢在很多程序语言中都存在,在以太坊虚拟机中,uint类型最大为256位,超过此范围会出现上下溢情况。2018年美链(BEC)使用的batchTranfer函数由于存在上溢漏洞,造成了巨大的经济损失,下面以此为例子对该漏洞进行说明:

上图为美链接口batchTranfer函数的具体实现。通过观察代码可以发现,合约中对uint256 amount = uint256(cnt) * _value;没有进行溢出判断。因此,假设uint256的最大值为MAX,而uint256 amount = uint256(cnt) * _value=MAX+1,这将导致amount为0。在转账的时候就会出现,balances[msg.sender]=balances[msg.sender]-amount=balances[msg.sender]-0,而balances[_receiver]=balances[_receiver]+_value,那么就可以无限转账了。

四.后记

其实相关文献中还涉及到了更多的内容,这里只是一点皮毛,希望自己可以继续学习并有所收获。

五.参考文献

智能合约安全综述:漏洞分析

https://hash1024.org/topics/27

https://www.jianshu.com/p/96bd28a0953e

https://my.oschina.net/u/3620978/blog/1592511

https://blog.csdn.net/xuguangyuansh/article/details/80786691

https://www.jianshu.com/p/191aed2c0f74

本文作者:ChaMd5安全团队

本文为安全脉搏专栏作者发布,转载请注明:https://www.secpulse.com/archives/129444.html

Tags:
评论  (0)
快来写下你的想法吧!

ChaMd5安全团队

文章数:53 积分: 171

www.chamd5.org 专注解密MD5、Mysql5、SHA1等

安全问答社区

安全问答社区

脉搏官方公众号

脉搏公众号