智能合约的安全问题

智能合约的安全问题

智能合约的安全问题

智能合约的安全问题

根据腾讯腾讯安全2018上半年区块链安全报告,智能合约引发的安全问题以成为区块链自身机制安全性的主要问题。区块链上的智能合约是由一种具有一定存储空间(状态)的,由特定语言编写的程序,合约的参与者通过调用合约代码来参与合约、发送或是获得资产。其中,基于以太坊的智能合约最具有代表性。本文以以太坊智能合约为例,就目前文献中提到的主流安全性问题做出总结,将其安全性问题分为4大种,即编码规范问题、ERC规范问题、设计缺陷问题、编码安全问题,帮助智能合约的开发者和安全工作者快速入门智能合约安全。

编码规范问题

1、 编译器版本

合约代码的第一行应指定编译器版本。如下代码所示,这行代码说明该合约需要用0.4.25及以上版本的编译器编译:

pragma solidity ^0.4.25;

老版本的编译器可能会导致各种已知的安全问题,例如/aper.seebug.org/631/#44-dividenddistributor,因此建议使用最新的编译器版本,

2、 构造函数书写

对应不同编译器版本应使用正确的构造函数,否则可能导致合约构造函数被任意调用,考虑属下代码

智能合约的安全问题

在小于0.4.22版本的solidify编译器语法要求中,合约构造函数必须和合约名字相等,名字受到大小写影响。以上代码由于构造函数大小写不一致,导致原先设计的构造函数成为了一个任何人都可以调用的普通函数,因此该合约的所有者可以被恶意替换。

在0.4.22版本以后,引入了constructor关键字作为构造函数声明,避免了这一问题,声明代码如下:

智能合约的安全问题ERC规范

1、 返回标准

ERC20规范要求transfer、approve函数应返回bool值,需要添加返回值代码

智能合约的安全问题

2、 事件标准

ERC20规范要求transfer、approve函数触发相应的事件

智能合约的安全问题

3、 假充值问题

转账函数中,对余额以及转账金额的判断,需要使用require函数抛出错误,否则会错误的判断为交易成功

以下代码可能会导致假充值。

智能合约的安全问题

正确代码如下:

智能合约的安全问题设计缺陷问题

1、 依赖于时间戳/区块高度的代码

考虑如下代码,程序使用了实践和区块高度作为随机种的种子:

智能合约的安全问题

攻击者可以控制区块高度和修改区块时间戳(由于同步机制,eth节点允许区块时间戳±900s)以此控制随机数。

2、无Gas发送

如下代码,若合约C调用合约D1,由于fallback函数修改了storage变量(这是一个消耗大量gas的操作)导致了超过fallback的gas上限(2300gas)导致fallback失败,调用D2时,由于没有超过上限,调用成功。

智能合约的安全问题智能合约的安全问题

3、重入漏洞

在以太坊智能合约中,进行转账操作,一旦向被攻击者劫持的合约地址发起转账操作,迫使执行攻击合约的回调函数,回调函数中包含回调自身代码,将会导致代码执行“重新进入”合约。这种合约漏洞,被称为“重入漏洞”。

考虑简单易受伤害的合约IDMoney,该合约充当以太坊保险库:

智能合约的安全问题

但是,当恶意攻击者,使用“重入漏洞”对合约进行攻击时,将不会按照合约创建者希望的逻辑进行执行。

考虑下面这个恶意攻击者创建的攻击合约Attack.sol,攻击者可以利用攻击合约不按照规则进行Ether的提取撤回。

智能合约的安全问题智能合约的安全问题

攻击过程如下:

    账户A部署IDMoney合约,账户B部署Attack合约账户A调用IDMoney()方法,并附加10ether账户B部署Attack合约,附加2ether账户B调用Attack.setVictim()方法,设置victim变量为IDMoney合约地址账户B调用Attack.step1()方法,设置amount=1000000000000000000,即合约Attack调用合约IDMoney.deposit()方法账户B调用Attack.step2()方法,设置amount=500000000000000000账户B调用Attack.stopAttack()方法,获得IDMoney的所有余额(包括A的存款,严格说是合约中除了500000000000000000wei的余额)

最终的结果是,立即从IDMoney.合约中取出了(除去amount数量)所有的 Ether。注意到合约IDMoney.withdraw()方法已经存在检查账户余额的代码,但是却未能生效,原因是递归调用时没有执行到balances[msg.sender] -= amount;,因此调用时,账户的余额是不变的,而真正导致递归调用退出的是require(this.balance >= amount);,这也是为何调用结束后合约还剩下amount数量的以太币的原因。有人会问,如果把这句话删掉呢?我本以为合约会报错,但是很遗憾,合约依然能够正常运行,并且合约中不再剩下任何以太币。

4、 逻辑问题

由于智能合约程序的调用方式与传统软件不相同,很多智能合约自身存在逻辑问题,如未检查余额就先转账,或是忽视合约地址的调用。如下代码的becomePresident函数忽略另一合约调用自身的情况,由于合约A调用合约B地址时,会默认调用合约B的callback方法,因此攻击者可以部署Attack合约,导致合约拒绝服务。

智能合约的安全问题

具体步骤如下:

    账户A部署PresidentOfCountry合约设置_price为1e18(1ether)账户B调用PresidentOfCountry,并附加1 ether,成为President,price=2 ether账户C部署Attack,调用start_attack(address(PresidentOfCountry))并附加2 ether,账户C成为President,账户B调用PresidentOfCountry,并附加4 ether,但是并不能称为President,说明合约代码无法正常运行。

这是由于在第三步Attacker成为President后,新的调用者成为President时会执行president.transfer()语句,从而执行合约Attack的fallback语句,又因为fallback语句固定报错(revert()),因此becomPresident函数永远无法再调用成功。

编码安全问题

1、溢出问题

由于Solidity是强类型语言,因此存在普通编程语言也存在的溢出问题,其中整数溢出最为常见,具体可分为上溢和下溢:

上溢:

智能合约的安全问题

下溢:

智能合约的安全问题

建议使用SafeMath解决溢出问题,代码如下

智能合约的安全问题

2、 Call注入

在EVM的设计中,如果call的参数data是一个合约的函数地址+参数,那么合约就会调用另一个合约的函数,若使用底层call函数(delegatecall)调用一个不受信任的函数,那么会对自身造成可怕后果。

在如下示例中,Mark合约使用delegatecall调用了恶意合约Steal的方法,导致自身被销毁:

智能合约的安全问题

具体操作为:

1.用A账户部署Steal,用B账户部署Mark合约,并在部署时为合约附加10个ether。

2.账户B调用Mark.call(address(Steal)),即用B调用Steal的Innocence方法,实际上innocence会在Mark的上下文环境运行,发现账户B收到合约的10 ether(注意不是A账户)

3.用C账户执行Mark.deposit()方法,并附加10ether,再调用destruct方法,发现B无法收到10ether,说明合约确实已经在第二步被销毁。

3、重放攻击

在BlackHat 2018会场,Bai等人指出之智能合约存在重放攻击风险,即如果合约存在相同的代码,则攻击者可以使用合约A函数的参数调用合约B。考虑如下代码:

智能合约的安全问题

该函数用来代理转账操作,具体情况为:付款人要为收款人转账,但是付款人没有足够的ETH,因此找一个代理人,并支付一定的代币作为代理费。如果其他合约同样包含TransferProxy函数,并且实现相似,那么攻击者可以在B合约上使用受害人的地址重放函数参数,B合约会执行成功。

相关链接
    Solidity知识点集 — 溢出和下溢(十一),https://www.jianshu.com/p/26d66aa1c122以太坊智能合约审计 CheckList,https://paper.seebug.org/741/智能合约安全性问题CheckList,https://www.freebuf.com/vuls/183357.html
致谢