首先需要说明的一点是,这个世界上没有绝对安全的技术。在区块链发展的十年里,各种基于区块链的数字货币引发的安全事故层出不穷,这些安全威胁主要来源有三个方面:

  1. 自身安全机制的问题,类似智能合约。

  2. 生态安全问题,交易所,矿池,网站等等。

  3. 使用者安全问题,包括个人账号密码的泄露,被钓鱼等。

作为普通的开发人员或者有一定编程知识的从业人员,我们首先应该确保的是自身安全机制没有问题,当然这个“没有问题”是一个相对的概念。智能合约的安全为什么这么重要,这很大原因在于智能合约编程和传统编程的巨大区别:

  1. 智能合约本身开发简单,但是却能够存储几千万到几十亿的的资产。

  2. 智能合约部署的过程是一次共识的过程,如果部署以后发现了安全问题,不能通过传统的打补丁或者升级的方式来避免。必须在设计和编码的过程中处理好这些容错和异常终止逻辑。

  3. 智能合约的代码都是开放的,多任何人可见。这其中就包括了一些不怀好意的黑客,没有传统开发过程中的加密,访问控制。

本系列希望通过对过往发生的一些安全事故的回顾,来提醒或者说警醒各位开发者,在开发的过程中,即便不能做到百分百安全,那么起码能做到“吸取前人的教训”,避免已经发生过的安全事故再次发生。

本文介绍的是对以太坊影响深远的The Dao 智能合约漏洞事件

事件介绍

The Dao 是一个去中心化的自治风险投资基金,通过发布的智能合约来募集资金,参与者可以通过投票的方式来投资以太坊上的应用,如果盈利,参与者就能获得回报。2016年6月17日,一名黑客发现了The Dao募资合约的漏洞,使得他可以无限的从合约中转出资金,短短几小时,360万的以太币被转出。这件事对以太坊的发展产生了巨大的影响,最后为了弥补用户的损失V神智能采用软分叉的方式,即所有通过这个The Dao的合约来减少新增用户余额的方式都被视为无效。

漏洞原因

首先请读者看一下合约中的代码,这端代码的业务逻辑是:如果用户不同意其他用户的投票,可以选择分裂出去。简单的说就是用户拿钱给基金会投资,中间用户如果反悔可以随时退钱。

//用户选择分裂出去调用的函数
function splitDAO(uint _proposalID, address _newCurator) noEther onlyTokenholders returns (bool _success) {
// ...
//利用平衡数组计算应该转移多少代币 p是提案对象
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;
// ...
// Burn DAO Tokens
Transfer(msg.sender, 0, balances[msg.sender]);
withdrawRewardFor(msg.sender); // 转移对应的金额给用户
// XXXXX Notice the preceding line is critically before the next few
totalSupply -= balances[msg.sender]; // 相应变量更新
balances[msg.sender] = 0; // 余额置为0
paidOut[msg.sender] = 0;
return true;
} function withdrawRewardFor(address _account) noEther internal returns(bool _success) {
if ((balanceOf(_account) * rewardAccount.accumulatedInput()) / totalSupply < paidOut[_account])
throw;
uint reward = (balanceOf(_account) * rewardAccount.accumulatedInput()) / totalSupply - paidOut[_account];
if (!rewardAccount.payOut(_account, reward)) // XXXXX vulnerable
throw;
paidOut[_account] += reward;
return true;
} function payOut(address _recipient, uint _amount) returns (bool) {
if (msg.sender != owner || msg.value > 0 || (payOwnerOnly && _recipient != owner))
throw;
if (_recipient.call.value(_amount)()) { // XXXXX vulnerable
PayOut(_recipient, _amount);
return true;
} else {
return false;
}
}

  

上面的代码在了解业务很容易明白:

用户提出分裂--》合约计算应该退给用户的金额--》调用call函数发送金额给用户--》用户的账户余额归为0,即先是调用splitDAO,splitDao中调用withdrawRewardFor,withdrawRewardFor中调用payOut执行转账。

乍一看没什么问题,讲述黑客的攻击手段之前,回顾一下solidity编程中的知识点:如果call函数的调用结果是true就一定是执行成功的吗?答案是NO,因为有可能是执行了回调函数。当调用call.value的时候,会把所有的gas发送到合约地址上并执行默认函数。所以这个默认函数将会有足够的gas执行任何操作,包括重新调用原合约的接口。本次攻击的黑客正式利用了这一点。

攻击手段

  1. 黑客先是通过自己创建了一个合约Child Dao,这个合约拥有一个回调函数,这个函数的作用就是去调用The Dao中的splitDao。

  2. 黑客提交了splitDao,地址是Child Dao的地址,当然在此之前的操作都是合法的操作,满足The Dao定义的调用splitDao的条件。

  3. 结合上面的代码,你会发现,开发者的代码先是在函数withdrawRewardFor中把金额退还给了用户,然后在退出函数之后将用户的余额置为0。那么如果攻击者在withdrawRewardFor和余额置空之间在此调用withdrawRewardFor,将会再次向攻击者提交的地址转移账户金额。结合刚才介绍的call函数知识点,聪明的读者应该能够想到攻击的原理了。黑客利用了call函数的机制,在合约中再次调用转账申请,由于上一次转账申请的余额还没有更新,所以第二次也会成功。相当于在循环中的重复调用自己,编程中的递归。

如何防范

其实The Dao的开发者的漏洞代码在传统的编程中没有任何问题,传统编程为了应对事务处理的结果,往往在转账之后进行余额的更新,因为有可能因为网络等原因导致转账不成功,如果程序提前把用户的账户余额置为0则容易引发数据丢失的问题。本次The Dao事件的代码修复可以从多方面来考虑:

  1. 调整代码顺序,在转账之前执行余额减扣。

  2. 避免不可控的函数调用,黑客利用call函数fallback的调用机制来攻击,这个场景其实在很多别的攻击事件中也可能发生,后面介绍的DOS攻击中黑客也利用了这一点。一方面应该避免这种方式调用,其实还应该避免在合约中直接使用转账操作,可以在设计的时候提供一个转账mapping,每个用户可以提现金额的多少对应其中的key value,让用户主动去操作这个接口完成调用。因为合约主动调用本身就存在安全隐患,合约的权限大于所有人。

【原创】智能合约安全事故回顾分析(1):The Dao事件的更多相关文章

  1. 智能合约安全事故回顾(3)-DOS漏洞导致的KotET事件

    现实世界中的网络都是有带宽限制的,想象一下,一个访问量稳定的网站,突然有人利用某种方式爆发式的将网站的访问量提升,这个时候系统会作何反应?如果系统没有合理的防DOS攻击的方式,这种时候往往会造成服务器 ...

  2. 智能合约安全事故回顾(2)-BEC溢出攻击

    讲溢出攻击之前,先给大家讲个故事:2014年的时候,美国的宾夕法尼亚州的某个小镇上发生了一个乌龙事件,征兵系统对一万多名1893年到1897出生的男子发去信函,要求他们注册参军,否则面临罚款和监禁.收 ...

  3. 智能合约语言 Solidity 教程系列4 - 数据存储位置分析

    写在前面 Solidity 是以太坊智能合约编程语言,阅读本文前,你应该对以太坊.智能合约有所了解, 如果你还不了解,建议你先看以太坊是什么 这部分的内容官方英文文档讲的不是很透,因此我在参考Soli ...

  4. 【精】EOS智能合约:system系统合约源码分析

    系统合约在链启动阶段就会被部署,是因为系统合约赋予了EOS链资源.命名拍卖.基础数据准备.生产者信息.投票等能力.本篇文章将会从源码角度详细研究system合约. 关键字:EOS,eosio.syst ...

  5. 以NGK 呼叫河马为例分析智能合约漏洞在哪?

    合约交易是指买方和卖方根据约定,在未来某一时刻,以指定价格接受某一资产的协议. 合约是买卖双方之间权利义务的表现形式.合约交易是一种金融衍生工具,与现货市场相比,用户通过判断期货合约交易的涨跌,选择买 ...

  6. 智能合约语言 Solidity 教程系列3 - 函数类型

    Solidity 教程系列第三篇 - Solidity 函数类型介绍. 写在前面 Solidity 是以太坊智能合约编程语言,阅读本文前,你应该对以太坊.智能合约有所了解,如果你还不了解,建议你先看以 ...

  7. 智能合约语言 Solidity 教程系列8 - Solidity API

    这是Solidity教程系列文章第8篇介绍Solidity API,它们主要表现为内置的特殊的变量及函数,存在于全局命名空间里. 写在前面 Solidity 是以太坊智能合约编程语言,阅读本文前,你应 ...

  8. 智能合约语言 Solidity 教程系列7 - 以太单位及时间单位

    这是Solidity教程系列文章第7篇介绍以太单位及时间单位,系列带你全面深入理解Solidity语言. 写在前面 Solidity 是以太坊智能合约编程语言,阅读本文前,你应该对以太坊.智能合约有所 ...

  9. 智能合约语言 Solidity 教程系列9 - 错误处理

    这是Solidity教程系列文章第9篇介绍Solidity 错误处理. Solidity系列完整的文章列表请查看分类-Solidity. 写在前面 Solidity 是以太坊智能合约编程语言,阅读本文 ...

随机推荐

  1. 分享知识-快乐自己:Liunx 安装 Zookeeper

    Zookeeper可以安装在windows中也可以安装在linux中! 但是我们的服务器基本都是在linux之上 安装我们的 zookeeper 注册中心! 安装成功之后  修改 nat 模式对应的端 ...

  2. xml字符串转xml对象,xml对象转json对象

    xml字符串转xml对象: function loadXml(str) { if (str == null) { return null; } var doc = str; try{ doc = cr ...

  3. freeMarker(二)——模板开发指南之入门

    学习笔记,选自freeMarker中文文档,译自 Email: ddekany at users.sourceforge.net 模板开发指南-入门  1.模板+数据模型=输出 假设在一个在线商店的应 ...

  4. poj2420 A Star not a Tree? 模拟退火

    题目大意: 给定n个点,求一个点,使其到这n个点的距离最小.(\(n \leq 100\)) 题解 模拟退火上 #include <cmath> #include <cstdio&g ...

  5. Python-Redis的List操作

    Redis列表是简单的字符串列表,一个列表可以包含超过40亿个元素 lpush(name,values):在name对应的list中添加元素,每个新的元素都添加到列表的最左边 rpush(name, ...

  6. 在VirtualBox中安装CentOS 7【转载】

    当初接触Linux的时候,因为条件限制,只能在VirtualBox虚拟机中安装Linux系统使用,由于是小白,爬了好多坑.于是决定写一篇关于在虚拟机中安装linux系统的文章.一是为了巩固自己的知识, ...

  7. linux命令-yum工具详解

    yum比rpm的优势在于解决依赖关系.可以指定源. [root@wangshaojun ~]# yum list ////列出rpm包资源 网络资源 rrdtool-php.      x86_64 ...

  8. hibernate.PropertyAccessException: Null value was assigned to a property of primitive type

    日志: [WARN-2016/07/26/18/:45/:52]ProcessEngineServiceImpl.(257) - 审批流程log日志--submitProcess方法:提交人userI ...

  9. 【总结整理】JavaScript的DOM事件学习(慕课网)

    事件:在文档或者浏览器窗口中发生的一些,特定的交互瞬间 HTML和JavaScript的交互通过事件 来实现 比如:1.滚动条向下滑动,加载图片 2.图片轮播,鼠标由2-5页调换 本章内容1.理解事件 ...

  10. 在虚拟机中的Ubuntu搭建java开发环境

    前提: 安装好虚拟机 在虚拟机中装好了Ubuntu系统 以上两步请参见我的博客(python进阶) 1 安装JDK 1.1 到官网下载jdk压缩包并保存在本地 jdk1.8:点击前往 1.2 在Ubu ...