毕晓冰,马兆丰,徐明昆
(1.北京邮电大学网络技术研究院,北京100876;2.北京邮电大学网络空间安全学院,北京 100876)
智能合约是一种计算机协议。以太坊是一个公有区块链平台,是目前最先进的支持智能合约的区块链平台。以太坊虚拟机负责将用户编写的智能合约代码编译成位码[1-2]。
以太坊智能合约漏洞的出现其实跟自身的语法(语言)特性有很大的关系。 在以太坊(今天最著名的智能合约平台)中编写表现良好且安全合同的创建过程是一项艰巨的任务。关于这一主题的研究最近才在工业和科学领域开始。
自智能合约发行以来,频发区块链漏洞诸多案例:美链蒸发60亿事件;区块链界最大众筹项目Decentralized Autonomous Organization(TheDAO)被攻击事件;交易所用户被钓鱼导致 APIKEY 泄漏; MyEtherWallet 遭域名系统Domain Name System(DNS)劫持致使用户Ether(ETH)被盗等等。频频爆出的区块链安全事件,使得越来越多的安全从业者将目标转到了智能合约上。对智能合约漏洞进行分析与应对显得尤其重要。
维也纳大学基于Solidity语言阐述了几种常见的智能合约安全模式。所呈现的模式描述典型安全问题的解决方案。卡利亚里大学系统阐述以太坊及其高级语言Solidity的安全漏洞。本文分析以太坊基于Solidity语言的重入、数据溢出、短地址攻击三个典型漏洞作为智能合约开发人员的参考,重现漏洞攻击过程并且在分析其成因的基础上提出安全模式下的应对策略[4-6]。
比特币的设计仅适合虚拟货币场景,由于存在非图灵完备性、缺少保存状态的账户概念、POW挖矿机制所带来的资源浪费与效率问题,在很多区块链应用场景中并不适用,以太坊在此情况下应运而生。
以太坊是通用的全球性区块链,可以管理应用的状态。同时以太坊完美结合了区块链与智能合约。它通过工作量证明机制实现共识,由矿工挖矿,通过P2P网络广播协议来实现对区块链的同步等操作。在以太坊上编写智能合约,可进行去中心化应用的开发,以满足金融或非金融的应用需求[1-2]。
在以太坊上部署的智能合约运行在以太坊特有的虚拟机上,通过以太坊虚拟机Ethereum Virtual Machine(EVM)和Remote Procedure Call(RPC)远程调用接口与底层区块链交互。
计算机科学家Nick Szabo描述到:智能合约是一个由计算机处理、执行的用于实现应用的协议。其总体目标是能够满足普通的合约条件,如支付、抵押、保密甚至强制执行。
从技术角度讲,智能合约可以被看作一种计算机程序代码,这种程序不经人为干涉,可以自主地执行全部或部分合约相关的操作。这种程序产生相应的可以被验证的证据,从而体现执行合约操作的有效性。
以太坊上的一个智能合约就是一段可以被以太坊虚拟机执行的代码,这些代码以以太坊特有的二进制形式存储在区块链上,并由以太坊虚拟机解释,因此被称为以太坊虚拟机位码(bytecode)。
智能合约一旦部署成功,就不能修改,因此出现漏洞就无法及时修正。
以太坊的智能合约漏洞根据引入的级别将漏洞分为三类:Solidity、EVM字节码、区块链。详细分类如下。
Solidity类:以太坊费用(Gas)快速消耗、误操作异常、可重入攻击、调用未知状态、输入类型。
EVM字节码:不可改变的错误、堆栈大小限制、以太币传输丢失、短地址漏洞。
区块链:可预测的随机处理、时间戳依赖、不可预测状态。
Solidity 是一种语法类似 JavaScript 的合约开发语言,是编写智能合约最流行的编程语言。开发者按一定的业务逻辑编写合约代码。编写后的智能合约发布在以太坊上,代码根据业务逻辑将纪录上链。以太坊更像是一个应用生态平台。发布合约,在以太坊上供业务直接使用。
使用Solidity进行合同开发时,合同的结构类似于面向对象编程语言中的类。 合同代码由读取和修改这些变量和函数的变量和函数组成,就像传统的命令式编程一样[7]。
由EVM指令集的限制,所有的指令都是针对256位这个基本的数据单位进行的操作,具备常用的算数、位、逻辑和比较操作。
美链漏洞就是因为操作数据发生溢出,黑客利用此转出近60亿人民币,导致BEC代币的市值接近归0,产生巨大损失。
2.1.1 关键函数
不安全代码:
batchTransfer函数功能为批量给若干用户地址转入_value个代币。
2.1.2 攻击原理
攻击者传入两个地址,value值为2^255,当参数传入时未进行溢出判断;合约中语句uint256 amount = uint256(cnt) ※ _value;执行后也未进行溢出判断;即假设uint256最大值为MAX的话,如果转账数值 uint256(cnt) ※ _value== MAX+1,则amount=0,转账的时候,sender账户-amount,而接受者账户+_value,至此,就能够无限转账BEC了。
在remix中调用函数batchTransfer,向两个地址转入token值_amount=2^255,即["0x14723a 09acff6d2a60dcdf7aa4aff308fddc160c","0x4b0897b 0513fdc7c541b6d9d7e929c4e5364d2db"]," 0x8000 000000000000000000000000000000000000000000 000000000000000000"
2.1.3 分析及安全应对模式
使用safeMath函数,safeMath函数是为了计算机安全而写的一个library函数如下。
使用safeMath函数的乘法在计算后,用assert 验证了结果是否正确。
计算 amount的时候,用了 mul的话, 则 c /a == b 也就是 验证 amount / cnt == _value此句执行报错,因为 0 / cnt 不等于 _value,也就不会发生溢出了。
EVM指令集的指令位数为256,即其所能处理的有符号数据的值范围为-2^255~2^255,无符号的数据范围为0~2^256-1。超出EVM所限制的范围将会发生数据溢出。因此在使用数据时一定要进行溢出判断;对数据进行操作时用safeMath函数。
安全模式代码如下:
2.1.4 安全代码不安全代码执行结果
安全代码不安全代码执行结果对比图如图1~图2所示。
图1 账户"0x14723a09acff6d2a60dcdf7aa4aff308fddc160c"余额
图2 账户"0x4b0897b0513fdc7c541b6d9d7e929c4e5364d2 db"余额
安全代码执行结果如图3所示。
图3 安全代码执行结果
短地址攻击通常发生在接受畸形地址的地方,如交易所提币、钱包转账。
早在2017年4月,Golem项目发布一篇博文,内容涉及一个影响Poloniex等交易所的安全漏洞。根据该帖子,当某些交易所处理ERC20令牌的交易时,没有对账户地址长度进行输入验证。结果是提供给合同的转移函数的输入数据格式不正确,以及操纵发送金额的后续下溢条件。影响是攻击者可能会盗走用户的token。
2.2.1 关键函数
sendcoin函数功能为向参数为to 地址转入值为 amount个 token。
2.2.2 攻击原理
取一个最后一个字节为00的地址,示例为0x254d383ab537ebeab73f816df8e1598f1321bc00,调用sendCoin函数,传入地址参数截掉最后一个字节,即向地址:
0x254d383ab537ebeab73f816df8e1598f1321 bc,转入1个token。
"0x254d383ab537ebeab73f816df8e1598f1321 bc"," 0x00000000000000000000000000000000000 00000000000000000000000000001"。执行后发现转入的amount值为即256。
2.2.3 分析及安全应对模式
EVM在进行sendcoin函数调用时传参。交易的输入数据由三部分组成。第一部分4个字节为方法的哈希值0xb90b98a11。第二部分32字节为以太坊的地址:
0x000000000000000000000000254d383ab537 ebeab73f816df8e1598f1321bc,由于地址为20字节因此高位自动补零。第三部分为32字节,在此函数中为需要传输的代币的数量为:
0x000000000000000000000000000000000000 0000000000000000000000000001。
输入值:
0xb90b98a11000000000000000000000000254 d383ab537ebeab73f816df8e1598f1321 bc00000000 000000000000000000000000000000000000000000 00000000000001共67字节。
EVM在执行时按照每一部分的位数,自动补取值。即取到的地址最后字节为amount参数的最高字节为0x254d383ab537ebeab73f816df8e1 598f1321bc00,由此导致第三部分的参数少了一个字节,末尾自动补零,即amount 值为:
0x000000000000000000000000000000000000 0000000000000000000100000000,即1<<8由原来的数值1变为256。由此取出的代币值远远大于原有应取出的值。使得合约调用者发生代币损失。
所以除了在编写合约的时候需要严格验证输入数据的正确性,而且在业务功能上也要对用户所输入的地址格式进行验证,防止短地址攻击的发生。
针对这个漏洞,以太坊有不可推卸的责任,因为EVM并没有严格校验地址的位数,并且还擅自自动补充消失的位数。此外,交易所在提币的时候,需要严格校验用户输入的地址,这样可以尽早在前端就禁止掉恶意的短地址。
2.2.4安全代码不安全代码执行结果
不安全代码执行结果如图4、图5所示。
图4 调用前账户"0x254d383ab537ebeab73f816df8e1598f13 21bc00"余额
图5 调用后账户"0x254d383ab537ebeab73f816df8e1598f13 21bc00"余额
进行前端控制输入后的执行结果如图6所示。
图6 前端控制输入执行结果
在执行智能合约时调用外部合约有很大的风险,因为此外部合约可以接管你当前合约的控制流程,恶意的外部合约可能会更改合约中的关键数据,这对当前合约造成的影响是巨大的。
当初始执行完成之前,外部合同调用被允许对调用合同进行新的调用时,就会发生重新进入。对于函数来说,这意味着合同状态可能会在执行过程中因为调用不可信合同或使用具有外部地址的低级函数而发生变化。
2.3.1 关键函数
该合约实现的是一个公共钱包功能。用户可以向钱包存钱,合约会记录每个用户的资产情况。每个用户也可以转账给合约内的用户。
2.3.2 攻击原理
攻击者部署一个恶意递归调用,利用EVM在交易时目标地址如果是个合约地址,那么默认会调用该合约的 fallback 函数。fallback函数在合约里显示为无返回值,无函数名。攻击者通过重写fallback函数,在其内部进行进行withdraw函数的调用。从而递归提取目标用户的所有资产。
2.3.3 分析及安全应对模式
deposit函数的功能是外部用户向该合约钱包存钱。withdraw函数为向合约内的其他用户转钱。而withdraw函数里转账所用关键语句为to.call.value(amount)();该语句会将剩余的 Gas 全部给予外部调用(fallback 函数)。不能有效防止重入。
send相对transfer方法较底层,不过使用方法和transfer相同,都是从合约发起方向某个地址转入以太币(单位是wei),地址无效或者合约发起方余额不足时,send不会抛出异常,而是直接返回false。
send()方法执行时有一些风险,调用递归深度不能超1024。如果gas不够,执行会失败。所以使用这个方法要检查成功与否。transfer相对send较安全。
安全模式代码:
使用transfer函数可以有效防止重入。
当发送失败时会 throw; 回滚状态;只会传递2300 Gas 供调用,防止重入。
2.3.4 安全代码不安全代码执行结果
不安全代码执行结果如图7所示。
图7 发生重入执行结果
安全代码执行结果如图8所示。
图8 安全代码执行结果
智能合约编译环境:Browser-Solidity;
运行平台:macOS 10.13.2;
检测工具Security、SmartCheck。
美链漏洞检测结果如图9所示。
图9 美链安全代码检测结果
结果表明改进后的代码即使用safemath函数能有效的防止操作数的溢出。
重入漏洞检测如图10、图11所示。
图10 重入漏洞不安全代码检测结果
图11 重入漏洞安全代码检测结果
针对美链漏洞即整数溢出问题、重入漏洞、短地址漏洞三个智能合约典型漏洞。其安全模式下的代码与不安全代码的对比分析如表1所示。
建立在区块链技术上的智能合约在新的业务应用程序和科学界受到极大的关注。在以太坊中编写安全的智能合约是一项艰巨的任务。
智能合约一经发布便不可再修改,且为代码公开的方式。因此,开发的时候一定要进行严格的代码审查、代码安全测试。本文通过对数据溢出、短地址攻击、重入智能合约中的三种典型安全漏洞进行分析,重现攻击并分析应对策略给出了安全模式。有助于编写更安全、良好的智能合约。
表1 不安全模式与安全模式对比分析