浅谈以太坊智能合约的安全漏洞

无线安全 2019-11-10

本文作者:Evi1ran

智能合约的安全是区块链安全中的热议话题,但其实 89% 的智能合约都存在漏洞,本文将浅谈以太坊智能合约出现过的一些安全漏洞。

以太坊智能合约

智能合约(Smart contract)是在 1994 年由 Nick Szabo 首次提出的,目的是提供优于传统合约的安全方法,并减少与合约相关的其他交易成本。智能合约允许在没有第三方的情况下进行可信交易,这些交易可追踪且不可逆转。

简单的来说,智能合约就是一个“执行合约条款的计算机交易协议”。 我们可以把它想象成一个绝对可信的人,我们让它临时保管我们的资产,并且严格按照事先交易双方商定好的规则执行操作。

以太坊是一个开源的区块链底层系统,就像安卓一样,提供了非常丰富的 API 和接口,让许多人在上面能够快速开发出各种区块链应用。目前已经有超过 200 多个应用在以太坊上开发。

以太坊主要使用 Solidity (本文所引用代码)编写智能合约,并在微软云服务上提供了智能合约工具箱,运行在以太坊区块链上,保证交易公平进行。

举个简单的例子:

在传统的合约中,A 和 B 打赌 10 元今晚会不会下雨,结果 A 赢了,但是 B 耍赖,不愿意给 A 10 元,A 没有办法,只好做罢。

第二天,B 找到 A 继续打赌今晚会不会下雨,A 学聪明了,他找了互相都认识的 C 做见证人,自己和 B 分别押了 10 元给 C。晚上 A 抬头看见天下雨了,心想这次肯定赢了。结果 B 和 A 不在同一个区,A 居住的区下雨了,B 住的却没下,C 也不知道该判谁赢。最后一番争吵后,尽管 B 不满意,C 觉得在这个城市里任何地方下雨都算下雨,正准备给 A 钱,却发现走得太匆忙,忘了带钱。

所以,传统的合约会受到各种维度的影响,自动化维度,主客观维度,成本维度,执行时间维度,违约惩罚维度,适用范围维度等。

而智能合约便是在打赌之前便考虑并规定好了所有可能出现的情况,事件发生后程序就会按照预先设定好的合约内容自动严格执行。因此可以解决传统的合约出现的问题。

fallback 函数漏洞原理

我相信关注区块链的都知道 The DAO 攻击事件,

事件回顾:

2016 年 5 月,The DAO 正式发布。该项目使用了由德国以太坊创业公司 Slock.it 编写的开源代码。The DAO 的设计职能类似于一项风险投资基金,可以授权为以太坊项目提供资金。这个众筹超过 1.5 亿美元的分布式自治组织,遭受到了黑客利用递归调用以太坊发送漏洞的攻击,引发了广泛的市场抛售。

那么黑客到底是怎么利用这个漏洞的呢?我们先了解下智能合约 fallback 函数漏洞利用的原理。

当智能合约执行时,如果没有找到指定的函数,或者根本就没有指定哪个函数(如发送 ether)时,就会调用 fallback 函数。

我们先来看一下 addr.call.value()()addr.send() 的区别。两者都是向某个地址发送以太币,不同的是这两个调用的 gaslimit 不一样。send() 基于 0 gas(相当于 call.gas(0).value()()),而 call.value()() 基于当前剩余的所有 gas

所以当你通过 addr.call.value()() 发送 ether 时,没有指定哪个函数,fallback 函数就会被调用,并附带上当前剩余的所有 gas。

根据这一点,黑客可以通过 fallback 函数构造出一个递归调用直到 trasnfer 把所有币转完。

 function () { address addr = 0x6c8f2a135f6ed072de4503bd7c4999a1a17f824b;  if(COUNT<100){  addr.call(“withdrawBalance”);  COUNT++;  } }

在这段 fallback 代码中,当计数器小于 100 时,递归调用 withdrawBalance 函数。在这种情况下:

msg.sender.call.value(amountToWithdraw)() 将被调用 100 次,从而取走 100 * 10 ether。

简单地来说,Call.value() 存在漏洞,可以被黑客构造 while 循环漏洞直到 trasnfer 把所有币转完。

因为智能合约的开放性,也存在着 token 信息泄露漏洞。

The DAO 攻击

在 The DAO 攻击事件中,黑客就是通过上文所说的原理利用 fallback 函数存在的漏洞进行递归调用攻击。黑客分析了 DAO.sol,并且注意到了 splitDAO 功能,这个功能最后会更新用户的余额和总额,所以如果我们能在它访问 splitDAO 之前再调用这项功能,我们就可以无限递归调用来 transfer 我们想要数量的代币。

漏洞代码(DAO.sol):

  function splitDAO(  uint _proposalID,  address _newCurator)noEther onlyTokenholders returns (bool _success){  …  uint fundsToBeMoved =  (balances[msg.sender] * p.splitData[0].splitBalance) /  p.splitData[0].totalSupply;  if(p.splitData[0].newDAO.createTokenProxy.value(fundsToBeMoved)(msg.sender)  == false) throw;  …  withdrawRewardFor(msg.sender);  totalSupply -= balances[msg.sender];  balances[msg.sender] = 0;  paidOut[msg.sender] = 0;  return true;  }

当合约执行到:

withdrawRewardFor(msg.sender);

会进入相应的函数:

  function withdrawRewardFor(address _account)  noEther internal returns (bool _success){  …  if(!rewardAccount.payOut(_account,reward)) //漏洞代码  throw;  …  }

payOut 函数定义如下:

  function payOut(address _recipient, uint _amount) returns (bool){  …  if(_recipient.call.value(_amount)) //漏洞代码  PayOut(_recipient, _amount);  return true;  }else{  return false;  }  }

黑客通过将下面的代码调用多次,以转移多份以太币:

p.splitData[0].newDAO.createTokenProxy.value(fundsToBeMoved)(msg.sender)

智能合约一旦发布便不能修改,只能通过硬分叉解决。

The DAO 已经丢失了 360 万以太币,这些以太币在被分离到一个独立的团组 child DAO 后,目前正被保存在一个独立的钱包内。 在攻击事件发生之后,Slock.it 制定了一个解决方案并把它上传到了 GitHub:

https://github.com/slockit/smart-contract/blob/master/DAOSecurity.sol

parity 多重签名钱包安全漏洞

事件回顾:

2017 年 7 月 19 日,用作 Parity Wallet 的多重签名钱包(“multi-sig”)代码中存在的漏洞被黑客所利用。持有 ETH 大额余额的三个钱包账户已被入侵,余额转入攻击者持有的账户。攻击者从三个高安全的多重签名合约中窃取到超过 15 万以太坊(约 3000 万美元)。原始报告:

https://paritytech.io/the-multi-sig-hack-a-postmortem/

我们可以看到黑客的资金账户:

https://etherscan.io/address/0xb3764761e297d6f121e79c32a65829cd1ddb4d32#internaltx

一共盗取了 153,037 个 ETH。

img

漏洞代码:

  // constructor - just pass on the owner array to the multiowned and   // the limit to daylimit   function initWallet(address[] _owners, uint _required, uint _daylimit) {   initDaylimit(_daylimit);   initMultiowned(_owners, _required);   }

源码地址:

https://github.com/paritytech/parity/blob/4d08e7b0aec46443bf26547b17d10cb302672835/js/src/contracts/snippets/enhanced-wallet.sol

当转账交易执行到 _walletLibrary.delegatecall 的分支时,因为该函数能无条件地调用合约内的任何一个函数,黑客通过调用 initWallet() 函数,初始化钱包,重新调用了 owner 函数。将 owner 直接分配给 library 后,可以把这个 library 转换为一个常规的多重签名钱包。再取得 owner 权限后,黑客还可以调用 kill() 指令,导致所有依赖于第三方 party 库的钱包瘫痪。

以太坊节点 Geth/Parity RPC API 鉴权缺陷

近日,慢雾安全团队观测到一起自动化盗币的攻击行为,攻击者利用以太坊节点 Geth/Parity RPC API 鉴权缺陷,恶意调用 eth_sendTransaction 盗取代币,持续时间长达两年,单被盗的且还未转出的以太币价值就高达现价 2 千万美金,还有代币种类 164 种,总价值难以估计(很多代币还未上交易所正式发行)。 原始报告

https://mp.weixin.qq.com/s/Kk2lsoQ1679Gda56Ec-zJg

黑客通过全球扫描 8545 端口(HTTP JSON RPC API)、8546 端口(WebSocket JSON RPC API)等开放的以太坊节点,发送 eth_getBlockByNumbereth_accountseth_getBalance 遍历区块高度、钱包地址及余额并不断重复调用 eth_sendTransaction 尝试将余额转账到攻击者的钱包。当正好碰上节点用户对自己的钱包执行 unlockAccount 时,在 duration 期间内无需再次输入密码为交易签名,此时攻击者的 eth_sendTransaction 调用将被正确执行,余额就进入攻击者的钱包里了。

unlockAccount 函数将使用密码从本地的 keystore 里提取 private key 并存储在内存中,函数第三个参数 duration 表示解密后 private key 在内存中保存的时间,默认是 300 秒;如果设置为 0,则表示永久存留在内存,直至 Geth/Parity 退出。详见:

https://github.com/ethereum/go-ethereum/wiki/Management-APIs#personal_unlockaccount

溢出漏洞

事件回顾:

BEC 智能合约批量转账函数中有一行代码存在 bug,导致了溢出漏洞。被黑客所利用,出现一笔高达 57,896,044,618,658,100,000,000,000,000,000,000,000,000,000,000,000,000,000,000 的 BEC 代币转账。瞬间套现抛售大额 BEC,6 亿在瞬间归零。

img

交易地址:

https://etherscan.io/tx/0xad89ff16fd1ebe3a0a7cf4ed282302c06626c1af33221ebe0d3a470aba4a660f

漏洞代码:

 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; } 

我们看到这一行代码:

uint256 amount = uint256(cnt) * _value;

这行代码没有对 amount 做溢出的检测,uint256(cnt) 把 cnt 转成了 uint256 类型,它的取值范围是 0 到 2 的 256 次方 -1 , 传入的 _value 值在第一张图中是 8000000000000000000000000000000000000000000000000000000000000000,而转账地址有两个,所以 cnt 为 2, amout 已经超过了最大值,从而导致了溢出。

合约地址:

https://etherscan.io/address/0xc5d105e63711398af9bbff092d4b6769c82f793d#code

其实开发者已经考虑了溢出问题,除了 amount 的计算外, 其他转账都用了 safeMath 的方法(sub,add) 。唯独 amount 的计算用了 require 而不是 assert 来验证。两者都是干同一件事,他们有什么区别呢?原来 assert 会让程序的 gas limit 消耗完毕,而 require 只会消耗掉当前执行的gas。正式这一行代码,蒸发了 ¥6,447,277,680 人民币!

结语

如上所说,本文只列举了一部分以太坊智能合约中出现过的安全漏洞。

在伦敦大学学院 (University College London,UCL) 计算机科学系副教授伊利亚·谢尔盖最新的研究论文 《Finding The Greedy , Prodigal , and Suicidal Contractsat Scale》 中,通过对将近 100 万份智能合约进行每份合约 10 秒分析时间的分析后发现,这其中有 34200 份智能合约很容易受到黑客攻击。同时他们又对 3759 份智能合约抽样调查,在这之中,3686 份智能合约有 89% 的概率含有漏洞。

除此之外,许多交易所也没有设置过滤器,可以通过简单的字符串拼接构造 POC 取走交易所虚拟账户中的所有代币。

参考链接

谈谈区块链:以太坊智能合约的安全漏洞

http://geek.csdn.net/news/detail/139516

关于昨天蔡文胜的 BEC 智能合约出现漏洞,又一个要归零的币

https://www.v2ex.com/t/448992

以太坊生态缺陷导致的一起亿级代币盗窃大案

https://mp.weixin.qq.com/s/Kk2lsoQ1679Gda56Ec-zJg


本文由 信安之路 创作,采用 知识共享署名 3.0,可自由转载、引用,但需署名作者且注明文章出处。

楼主残忍的关闭了评论