区块链智能合约具有高效实时更新、准确执行、去中心化等优点,但是智能合约执行过程中的漏洞问题却给用户及投资者带来较大困扰。针对以太坊上运行的智能合约安全防护问题,分析已发现漏洞类型,对智能合约整数溢出问题、可重入攻击、短地址漏洞三个智能合约安全漏洞典型问题,从 Solidity 语言自身以及以太坊虚拟机特殊机制入手,剖析智能合约漏洞的成因与攻击原理。并针对以上三种典型漏洞,分别提出安全模式下智能合约安全问题的解决方案。同时,对安全代码与不安全代码的执行结果进行对比,结果表明所提方案可以实现应对智能合约的漏洞的安全策略。
引言
智能合约是一种计算机协议。以太坊是一个公有区块链平台,是目前最先进的支持智能合约的区块链平台。以太坊虚拟机负责将用户编写的智能合约代码编译成位码。
以太坊智能合约漏洞的出现其实跟自身的语法(语言)特性有很大的关系。 在以太坊(今天最著名的智能合约平台)中编写表现良好且安全合同的创建过程是一项艰巨的任务。关于这一主题的研究最近才在工业和科学领域开始。自智能合约发行以来,频发区块链漏洞诸多案例:美链蒸发 60 亿事件;区块链界最大众筹 项目 Decentralized Autonomous Organization (TheDAO)被攻击事件;交易所用户被钓鱼导致 APIKEY 泄漏; MyEtherWallet 遭域名系统Domain Name System(DNS)劫持致使用户 Ether(ETH)被盗等等。频频爆出的区块链安全事件,使得越来越多的安全从业者将目标转到了智能合约上。对智能合约漏洞进行分析与应对显得尤其重要。维也纳大学基于 Solidity 语言阐述了几种常见的智能合约安全模式。所呈现的模式描述典型安全问题的解决方案。卡利亚里大学系统阐述以太坊及其高级语言 Solidity 的安全漏洞。本文分析以太坊基于 Solidity 语言的重入、数据溢出、短地址攻击三个典型漏洞作为智能合约开发人员的参考,重现漏洞攻击过程并且在分析其成因的基础上提出安全模式下的应对策略。智能合约关键技术介绍
以太坊
比特币的设计仅适合虚拟货币场景,由于存在非图灵完备性、缺少保存状态的账户概念、POW 挖矿机制所带来的资源浪费与效率问题,在很多区块链应用场景中并不适用,以太坊在此情况下应运而生。
以太坊是通用的全球性区块链,可以管理应用的状态。同时以太坊完美结合了区块链与智能合约。它通过工作量证明机制实现共识,由矿工挖矿,通过 P2P 网络广播协议来实现对区块链的同步等操作。在以太坊上编写智能合约,可进行去中心化应用的开发,以满足金融或非金融的应用需求。在以太坊上部署的智能合约运行在以太坊特有的虚拟机上,通过以太坊虚拟机 Ethereum Virtual Machine(EVM)和 Remote Procedure Call(RPC)远程调用接口与底层区块链交互。智能合约
计算机科学家 Nick Szabo 描述到:智能合约是一个由计算机处理、执行的用于实现应用的协议。其总体目标是能够满足普通的合约条件,如支付、抵押、保密甚至强制执行。
从技术角度讲,智能合约可以被看作一种计算机程序代码,这种程序不经人为干涉,可以自主地执行全部或部分合约相关的操作。这种程序产生相应的可以被验证的证据,从而体现执行合约操作的有效性。以太坊上的一个智能合约就是一段可以被以太坊虚拟机执行的代码,这些代码以以太坊特有的二进制形式存储在区块链上,并由以太坊虚拟机解释,因此被称为以太坊虚拟机位码(bytecode)。智能合约一旦部署成功,就不能修改,因此出现漏洞就无法及时修正。安全漏洞
以太坊的智能合约漏洞根据引入的级别将漏洞分为三类:Solidity、EVM 字节码、区块链。
详细分类如下。Solidity 类:以太坊费用(Gas)快速消耗、误操作异常、可重入攻击、调用未知状态、输入类型。EVM 字节码:不可改变的错误、堆栈大小限制、以太币传输丢失、短地址漏洞。区块链:可预测的随机处理、时间戳依赖、不可预测状态。Solidity
Solidity 是一种语法类似 JavaScript 的合约开发语言,是编写智能合约最流行的编程语言。开发者按一定的业务逻辑编写合约代码。编写后的智能合约发布在以太坊上,代码根据业务逻辑将纪录上链。以太坊更像是一个应用生态平台。发布合约,在以太坊上供业务直接使用。
使用 Solidity 进行合同开发时,合同的结构类似于面向对象编程语言中的类。合同代码由读取和修改这些变量和函数的变量和函数组成,就像传统的命令式编程一样。智能合约漏洞分析
美链漏洞
由 EVM 指令集的限制,所有的指令都是针对 256 位这个基本的数据单位进行的操作,具备常用的算数、位、逻辑和比较操作。
美链漏洞就是因为操作数据发生溢出,黑客利用此转出近 60 亿人民币,导致 BEC 代币的市值接近归 0,产生巨大损失。关键函数
不安全代码:
function batchTransfer(address[] _receivers, uint256_value)public whenNotPaused returns (bool) {uint cnt = _receivers.length;uint256 amount = uint256(cnt) ※ _value;require(cnt > 0 && cnt <= 20);require(_value > 0 && balances[msg.sender] >=amount);balances[msg.sender] = balances[msg.sender].sub(amount);for (uint i = 0; i < cnt; i++) {balances[_receivers[i]] = balances[_receivers[i]].add(_value);Transfer(msg.sender, _receivers[i], _value);}return true;}}batchTransfer 函数功能为批量给若干用户地址转入 _value 个代币。攻击原理
攻击者传入两个地址,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^25,即 ["0x14723a09acff6d2a60dcdf7aa4aff308fddc160c","0x4b0897b0513fdc7c541b6d9d7e929c4e5364d2db"],"0x8000000000000000000000000000000000000000000000000000000000000000"分析及安全应对模式
使用 safeMath 函数,safeMath 函数是为了计算机安全而写的一个 library 函数如下。
functionmul(uint256 a,uint256 b)internalconstantreturns(uint256){uint256 c=a※b;assert(a==0||c/a==b);returnc;}使用 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 函数。安全模式代码如下:function batchTransfer(address[] _receivers, uint256_value) public whenNotPaused returns (bool) {uint cnt = _receivers.length;//uint256 amount = uint256(cnt) ※ _value;uint256 amount = _value.mul(uint256(cnt));require(cnt > 0 && cnt <= 20);require(_value > 0 && balances[msg.sender] >=amount);balances[msg.sender] = balances[msg.sender].sub(amount);for (uint i = 0; i < cnt; i++) {balances[_receivers[i]] = balances[_receivers[i]].add(_value);Transfer(msg.sender, _receivers[i], _value);}return true;}}安全代码不安全代码执行结果安全代码不安全代码执行结果对比图如图 1 ~图 2 所示。安全代码执行结果如图 3 所示。
短地址漏洞
短地址攻击通常发生在接受畸形地址的地方,如交易所提币、钱包转账。
早在 2017 年 4 月,Golem 项目发布一篇博文,内容涉及一个影响 Poloniex 等交易所的安全漏洞。根据该帖子,当某些交易所处理 ERC20令牌的交易时,没有对账户地址长度进行输入验证。结果是提供给合同的转移函数的输入数据格式不正确,以及操纵发送金额的后续下溢条件。影响是攻击者可能会盗走用户的 token。关键函数
pragma solidity ^0.4.11;contract TestToken{mapping (address => uint) balances;event Transfer(address indexed _from, addressindexed _to, uint256 _value);function TestToken(){balances[tx.origin] = 10000;}function sendCoin(address to,uint amount)returns(bool sufficient){if(balances[msg.sender] < amount) return false;balances[msg.sender] -= amount;balances[to] += amount;Transfer(msg.sender,to,amount);return true;}function getBalance(address addr) constantreturns(uint){return balances[addr];}}sendcoin 函数功能为向参数为 to 地址转入值为 amount 个 token。攻击原理
取一个最后一个字节为 00 的地址,示例为0x254d383ab537ebeab73f816d
f8e1598f1321bc00,调用 sendCoin 函数,传入地址参数截掉最后一个字节,即向地址:0x254d383ab537ebeab73f816df8e1598f1321bc,转入 1 个 token。"0x254d383ab537ebeab73f816df8e1598f1321bc"," x0000000000000000000000000000000000000000000000000000000000000001"。执行后发现转入的 amount 值为即 256。分析及安全应对模式
EVM 在进行 sendcoin 函数调用时传参。交易的输入数据由三部分组成。第一部分 4 个字 节为方法的哈希值 0xb90b98a11。 第二部分 32 字节为以太坊的地址:
0x000000000000000000000000254d383ab537ebeab73f816df8e1598f1321bc,由于地址为 20 字节因此高位自动补零。第三部分为 32 字节,在此函数中为需要传输的代币的数量为:0x0000000000000000000000000000000000000000000000000000000000000001。输入值:0xb90b98a11000000000000000000000000254d383ab537ebeab73f816df8e1598f1321bc0000000000000000000000000000000000000000000000000000000000000001 共 67 字节。EVM 在执行时按照每一部分的位数,自动补取值。即取到的地址最后字节为 amount 参数的最高字节为 0x254d383ab537ebeab73f816df8e1598f1321bc00,由此导致第三部分的参数少了一个字节,末尾自动补零,即 amount 值为:0x0000000000000000000000000000000000000000000000000000000100000000, 即 1<<8 由 原来的数值 1 变为 256。由此取出的代币值远远大于原有应取出的值。使得合约调用者发生代币损失。所以除了在编写合约的时候需要严格验证输入数据的正确性,而且在业务功能上也要对用户所输入的地址格式进行验证,防止短地址攻击的发生。针对这个漏洞,以太坊有不可推卸的责任,因为 EVM 并没有严格校验地址的位数,并且还擅自自动补充消失的位数。此外,交易所在提币的时候,需要严格校验用户输入的地址,这样可以尽早在前端就禁止掉恶意的短地址。安全代码不安全代码执行结果
不安全代码执行结果如图 4、图 5 所示。
进行前端控制输入后的执行结果如图6所示。
重入漏洞
在执行智能合约时调用外部合约有很大的风险,因为此外部合约可以接管你当前合约的控制流程,恶意的外部合约可能会更改合约中的关键数据,这对当前合约造成的影响是巨大的。
当初始执行完成之前,外部合同调用被允许对调用合同进行新的调用时,就会发生重新进入。对于函数来说,这意味着合同状态可能会在执行过程中因为调用不可信合同或使用具有外部地址的低级函数而发生变化。关键函数
该合约实现的是一个公共钱包功能。用户可以向钱包存钱,合约会记录每个用户的资产情况。每个用户也可以转账给合约内的用户。
function ComMoney() { owner = msg.sender; }function deposit() payable { balances[msg.sender]+= msg.value; }function withdraw(address to, uint256 amount) {require(balances[msg.sender] > amount);require(this.balance > amount);withdrawLog(to, amount); // 打印日志,方便观察 reentrancyto.call.value(amount)(); // 使用 call.value()() 进行 ether 转币时,默认会发所有的 Gas 给外部balances[msg.sender] -= amount;}攻击原理
function Attack() payable { owner = msg.sender; }
// 设置已部署的 ComMoney 合约实例地址function setVictim(address target) ownerOnly {victim = target; }function step1(uint256 amount) ownerOnlypayable {if (this.balance > amount) {victim.call.value(amount)(bytes4(keccak256("deposit()")));}}function step2(uint256 amount) ownerOnly {victim.call(bytes4(keccak256("withdraw(address,uint256)")), this, amount);}function stopAttack() ownerOnly {selfdestruct(owner);}function startAttack(uint256 amount) ownerOnly {step1(amount);step2(amount / 2);}function () payable {if (msg.sender == victim){ victim.call(bytes4(keccak256("withdraw(address,uint256)")), this, msg.value);}}攻击者部署一个恶意递归调用,利用 EVM在交易时目标地址如果是个合约地址,那么默认 会调用该合约的 fallback 函数。fallback 函数在合约里显示为无返回值,无函数名。攻击者通过重写 fallback 函数,在其内部进行进行withdraw 函数的调用。从而递归提取目标用户的所有资产。
分析及安全应对模式
deposit 函数的功能是外部用户向该合约钱包存钱。withdraw 函数为向合约内的其他用户转钱。而 withdraw 函数里转账所用关键语句为to.call.value(amount)(); 该语句会将剩余的 Gas 全部给予外部调用(fallback 函数)。不能有效防止重入。
send 相对 transfer 方法较底层,不过使用方法和 transfer 相同,都是从合约发起方向某个地址转入以太币 (单位是 wei),地址无效或者合约发起方余额不足时,send 不会抛出异常,而是直接返回 false。send() 方法执行时有一些风险,调用递归深度不能超 1024。如果 gas 不够,执行会失败。所以使用这个方法要检查成功与否。transfer 相对 send 较安全。安全模式代码:// 安全function ComMoney1() { owner = msg.sender; }function deposit() payable { balances[msg.sender]+=msg.value; }function withdraw(address to, uint256 amount) {require(balances[msg.sender] > amount);require(this.balance > amount);withdrawLog(to, amount); // 打印日志,方便观察 reentrancyto.transfer(amount);balances[msg.sender] -= amount;}function balanceOf() returns (uint256) { return balances[msg.sender]; }function balanceOf(address addr) returns (uint256){ return balances[addr]; }}使用 transfer 函数可以有效防止重入。当发送失败时会 throw; 回滚状态;只会传递2300 Gas 供调用,防止重入。安全代码不安全代码执行结果
不安全代码执行结果如图 7 所示。
安全代码执行结果如图 8 所示。
实验结果及评测
运行环境及检测工具
智能合约编译环境:Browser-Solidity;
运行平台:macOS 10.13.2;检测工具 Security、SmartCheck。检测结果及分析
美链漏洞检测结果如图 9 所示。
结果表明改进后的代码即使用 safemath 函数能有效的防止操作数的溢出。
重入漏洞检测如图 10、图 11 所示。不安全模式与安全模式对比分析
针对美链漏洞即整数溢出问题、重入漏洞、短地址漏洞三个智能合约典型漏洞。其安全模式下的代码与不安全代码的对比分析如表 1 所示。
结语
建立在区块链技术上的智能合约在新的业务应用程序和科学界受到极大的关注。在以太坊中编写安全的智能合约是一项艰巨的任务。
智能合约一经发布便不可再修改,且为代码公开的方式。因此,开发的时候一定要进行严格的代码审查、代码安全测试。本文通过对数据溢出、短地址攻击、重入智能合约中的三种典型安全漏洞进行分析,重现攻击并分析应对策略给出了安全模式。有助于编写更安全、良好的智能合约。作者
毕晓冰, 北京邮电大学网络技术研究院,硕士生,主要研究方向为区块链及安全技术,实验室研发完成了可信区块链智能合约安全评测系统等区块核心技术及系统,积累了丰富的区块链技术开发、部署和上线应用经验。
马兆丰, 北京邮电大学网络空间安全学院研究生导师,北京邮电大学区块链及安全技术联合实验室主任,当前主要研究区块链及安全技术、移动互联网与大数据安全创新、网络与信息安全、数字版权管理等多个课题取得了良好的科研成果和社会经济效益。徐明昆, 北京邮电大学网络技术研究院信息网络中心研究生导师,高级工程师,主要研究方向为云计算与中间件。科研团队完成 / 承担的网络安全、人工智能、大数据、数字水印等多个课题取得了良好的科研成果和社会经济效益。(本文选自《信息安全与通信保密》2018年第十二期)
声明:本文来自信息安全与通信保密杂志社,版权归作者所有。文章内容仅代表作者独立观点,不代表安全内参立场,转载目的在于传递更多信息。如有侵权,请联系 anquanneican@163.com。