Checkmarx安全研究团队成员Paulo Silva和Guillaume Lopes进行了本研究。
根据官方文件资料的定义,Solidity“是一种面向合约的、为实现智能合约而创建的高级语言。”早在2014年, Gavin Wood就提出了Solidity的概念。当时,Solidity由几个人共同负责开发,其中大多数人是以太坊平台的核心构建者。Solidity的设计目的是为了能够在以太坊等区块链平台上编写智能合约。
Solidity是围绕ECMAScript语法设计的语言,其静态类型类似C++。Solidity的设计目的是为了让网络开发人员熟悉如何给inheritance、库、用户定义的数据类型提供支持。
Solidity的概念被提出时,它与mappings、structs、inheritance等其他同样以EVM(比如Serpent 、LLL,Viper以及Mutan等)为目标的语言有着显著的不同,甚至与自然语言规范NatSpec也有着本质的区别。
像其他以虚拟机(VM)为目标的编程语言一样,Solidity使用编译器solc编译成字节码。
智能合约可视为一种计算机协议,旨在根据合约规则完成某些任务。在加密货币的背景下,智能合约加强了交易的可追溯性和不可逆转性,让交易双方摆脱了对银行等第三方监管机构的需要。智能合约概念于1994年由Nick Szabo提出。
本文从安全角度介绍了Checkmarx安全研究团队创建的Solidity。
如今,越来越多的人/企业将区块链视为一项有前途的技术,愿意开发区块链技术。在此背景下,如果他们需要创建智能合约,则必须采用代码审查、测试和审核等软件开发最佳做法。由于智能合约是在公开的条件下执行的,其源代码通常可以利用,因此上述做法变得尤为关键。
我们很难保证软件的使用能够完全满足预期要求,因此了解软件最常出现的问题以及智能合约运行环境的可及性至关重要。软件漏洞攻击的目标可能不是智能合约本身,而是编译器或虚拟机(例如EVM)。
我们将在接下来的章节中讨论这些内容,提供概念验证,证实所讨论的主题。
前言
在以太坊(简称Eth)的环境中,智能合约是可以处理资金的脚本。Miners(多台计算机)负责执行和认证这些智能合约,将交易(执行智能合约或支付加密货币)添加到公共分类账(区块)中。人们将多个区块称为区块链。
Miners使用“Gas”完成作业(举例而言,发布智能合约、运行智能合约函数,或执行账户间转账)。这种“Gas”是用以太坊支付的。
常见问题
隐私
在Solidity中,隐私的概率可能和您想象的相距甚远。如果您习惯使用类似Java等语言面向对象编程更是如此。
私有变量并不表示他人无法查阅该内容,只是说该内容的访问途径只有合约。您应该记得,区块链存储在许多计算机上,让其他人能够看到存储在这些“私有”变量中的内容。
请注意,私有函数不会被其他合约继承。为了启用私有函数继承机制,Solidity提供了内部关键字。
纯函数/视图函数
要防止函数在EVM级别读取状态是无法实现的,但防止这些函数写入状态是可行的(换言之,视图函数可在EVM级别执行,而纯函数则不能)。
编译器开始执行纯函数并未读取版本0.4.17中的状态。
资料来源
可重入性
可重入性是一个众所周知的计算概念,也是2016年6月导致7,000万美元损失的黑客攻击的原因(这起黑客攻击事件也被称为分散式自治组织(DAO)攻击事件)。David Siegel在他的书中《Understanding The DAO Hack for Journalists》详细介绍了整个事件的时间轴,并对所发生的事情进行了全面说明。
“就计算而言,如果某个计算机程序或子程序在执行过程中被中断,然后在完成其前次调用前被再次安全引入(‘重入’),这样的程序就称为可重入程序。”(维基百科)
凭借通用计算模式,开发智能合约是可行的,仍然有希望。调用()函数是这次攻击的核心,值得注意的是调用函数:
以下警告信息摘自Solidity文档:
“与其他合约直接发生任何交互都会构成潜在危险,如果事先并不了解合约的源代码,更容易产生问题。当前合约将控制权移交给被调用的合约,这可能会导致任何事情发生。即使被调用合约继承了已知母合约,继承合约只需要有一个正确的接口。然而,用户可能任意执行该合约,从而构成危险。此外,用户应做好准备,避免合约首次调用返回前,系统的其他合约被调用,或甚至返回到调用合约。这意味着被调用合约可通过自身的函数改变调用合约的状态变量。举例而言,采用以下方式编写函数:在合约状态变量发生变化后,对外部函数进行调用,这样您的合约就不会受到可重入性攻击。”
以上粗体突出显示的内容正是网络攻击者如何利用可重入性的漏洞对智能合约进行攻击的原因所在。在下面概念验证内容和随附视频中,我们准备了一个可运行的示例。为了避免这种攻击,请确保:
<address>.transfer(uint256 amount)/ <address>.send(uint256 amount) return (bool)
当前设置的Gas限值为2300,能够有效避免智能合约因可重入性的问题受到攻击;
溢位
由于256位虚拟机(EVM)的存在,Solidity数据类型很难处理。该语言不提供浮点表示,短于32字节的数据类型被打包放置于同一个32字节槽中。字面量0类型表示字节,而不是我们预期的整数。
限于256位,上限溢位和下限溢位是我们可以预料的。最大值为255(2ˆ8-1或11111111)的uint8的情况可能会发生
OverflowUint8.sol源代码或最大值为1.157920892×10 (2ˆ256-1)的uint256
OverflowUint256.sol源代码
尽管uint256不太可能发生溢位,人们认为它(更)安全,但它和其他数据类型一样,也存在相同的问题。batchOverflow bu (CVE-2018–10299)是uint256发生溢位的很好证明。
概念验证
请参考以下银行智能合约,该合约持续跟踪签约地址的余额。
仔细看看函数withdraw()函数,就会发现内部状态更新前,上文通信常见问题>可重入部分外部调用中突出显示的可重入模式。
现在我们需要一个恶意设计的智能合约攻击银行的漏洞:
盗窃
我们用Solidity开发环境预演“打劫”。要运行上述恶意程序,我们只需要一个支持docker的环境。
克隆solidity-ddenv项目,在solidity-ddenv文件夹中移动
$ git clone https://github.com/Checkmarx/solidity-ddenv && cd solidity-ddenv
我们启动开发环境
$ ./ddenv
使用默认驱动程序创建网络“solidityddenv _ default”
创建ganache …完成
创建truffle…完成
如果ddenv启动正确,您将进入workspace文件夹(您可以看它运行pwd)。
我们进入银行智能合约和盗窃智能合约所在的可重入目录
$ cd可重入
现在,是编译源代码的时候了
$ ddenv truffle编译
启动ganache…完成
编译./contracts/Bank.sol…
编译./contracts/Migrations.sol…
编译./contracts/Thief.sol…将任务图写入./build/contracts
并将智能合约部署到我们的开发网络中:
现在,我们已经做好发起攻击的准备。我们给自己的开发网络生成一个控制台,这样我们就可以发出一些命令。
$ ddenv truffle控制台——网络开发
启动ganache…完成
truffle(开发)>
truffle(开发) >是提示。如果您想自己运行攻击程序,只需从下面的脚本中复制提示旁边的命令,并将其粘贴到您之前启动的控制台提示中。
现在,我们执行以下操作
Checkmarx安全研究团队开展调查研究的原因正是因为发现了上述的漏洞。我们团队持续开展上述研究活动的目的是为了让全球各地的企业都能在软件安全实践方面进行必要的变革。更多内容,关注Checkmarx微信公众号或官网。