搜索公众号:暗网黑客可领全套网络安全课程、配套攻防靶场

一.前言
这周由于兴(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下面讲一下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,那么就可以无限转账了。
四.后记
其实相关文献中还涉及到了更多的内容,这里只是一点皮毛,希望自己可以继续学习并有所收获。

每节课都附带
合理合法用来实战训练入侵
模拟靶场和黑客工具课件附赠靶场工具
扫码免费领取更多零基础黑客渗透实战视频教程!


作者: ChaMd5安全团队
转载自:https://www.secpulse.com/archives/129444.html
添加新手交流群:币种分析、每日早晚盘分析
添加助理微信,一对一亲自指导:YoYo8abc