惊魂33天:The DAO被盗 | 区块链安全经典案例

Posted vigor2323

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了惊魂33天:The DAO被盗 | 区块链安全经典案例相关的知识,希望对你有一定的参考价值。

作者按:这是一篇长文,如果你耐着性子看完,不论你的基础如何,一定会让你对区块链、DAO、以太坊、网络安全有更深入的理解。对于比较技术的地方,如果看不懂,直接跳过即可。这篇文章是目前为止你能看到的对The DAO事件描述最为全面的文章,我已经把它写得尽量易读。从这件事中,你可以看到技术的较量、人性的较量、理念的较量,而且都是顶尖级的较量。这其中,有很多内容是人类历史上从未遇到过的……

说起区块链,除了老大比特币,就是老二以太坊了。

说起以太坊安全,就怎么也绕不过The DAO被盗事件。

2015年7月30日,V神创建的以太坊问世。

2016年4月30日,The DAO智能合约在以太坊上完成部署,开始众筹。

2016年5月27日,The DAO完成众筹,筹得了惊人的1.5亿美元,众筹人数超过11000人,成为史上最大规模众筹。当然,并不是真的美元,而是1200万个以太币(ETH)。

2016年6月17日,The DAO被黑客攻击,盗走了3,641,694个ETH。

2016年7月20日,万般无奈下,为了挽救当时以太坊上这个最著名的项目,V神和支持者说服众人,以太坊于区块高度1920000硬分叉。The DAO黯然收场。

本文回顾这个过程中惊心动魄的一切。

什么是DAO?

DAO,即去中心化自治组织(Decentralized Autonomous Organization)。

这一直是很多人的梦想:大家使用区块链上的智能合约来运转一个公司,没有老板,没有高层,没有财务,没有管理人员,没有暗箱操作,所有的管理规则都写在智能合约里,所有的投资回报和薪酬发放也写在智能合约里,整个公司是全自动化运营的,没有人能以超然的身份控制它,没有人能改变规则,除非大家投票改变规则。

人们之所以对DAO大感兴趣,是因为公司管理层很可能由于智商不足或状态不对导致决策错误,高层往往因为其刚愎自用而酿下大错。而且人们往往想当家作主,只要自己投了钱,自己就有投票权,人们对这种新的社会组织形态报有很大的期望。

DAO还解决了一个非常重要的问题:信任问题。如果大家投钱给一个基金,让它来代理大家的投资并定期分红,怎么能信任它不卷钱跑路呢,怎么能信任它不贪污舞弊呢?

但运行在以太坊上的DAO,只要代码没有问题,就肯定按照预定逻辑正常执行,肯定不会有猫腻。

按照区块链的通行玩法,这些合约代码都是开源的,都经过了若干眼睛的审查,甚至经过了知名审计公司的审计,人们大可以放心里面没有Bug。

加入一个以太坊上的DAO,投资者并不是把钱打给DAO,而是用以太币(ETH)换取DAO中的token。ETH是怎么得来的呢,除了以太坊矿工,绝大多数人,需要用真金白银从虚拟货币交易所买,或者用其他虚拟货币(如BTC)兑换,而其他虚拟货币,也是挖来的,或是买来的。在以太坊里,ETH就是硬通货,本文中所说的钱,指的就是ETH。

有了token,就可以对DAO中的提议(比如投资某个项目)进行投票,token越多,投票的分量越重(有100个token就是100票)。然后,投资回报也根据token进行分红,谁的token多,谁得到的回报就多。

这是不是很完美?

那什么是The DAO?

The DAO是Slock.it公司按照DAO的理念,做的一个具体实现。

The DAO的创建人,是Slock.it的创始人:德国人Christoph Jentzsch,他有理论物理学背景,自2014年以来,他一直是以太坊项目的首席测试员,他深信区块链技术的长期愿景,他有7个孩子。

Slock.it公司总部位于德国,成立于2015年。公司的目标是将智能合约嵌入到物联网设备中,让任何人都可以不通过中间商,直接出租、出售或共享任何物品。大家知道,当时共享经济的概念非常火热,Slock.it公司融合了区块链、物联网和共享经济的概念,在当时非常先进。

Christoph Jentzsch

2015年11月,Jentzsch在伦敦举行的以太坊开发者大会(Devcon1)上为与会者演示1了如何通过区块链技术出租和打开一把智能门锁,然后,他说“我们还有一个更酷的东西”,随即提出了DAO的设计和愿景,“投资者将用以太币购买DAO代币(token);代币将允许他们投票支持他们喜欢的项目。如果他们支持的项目赚钱,则代币持有人分享利润”,这个演讲的反响异常热烈,很多人深受鼓舞。

Jentzsch在devcon1上介绍DAO

The DAO的代码主要是Jentzsch写的(当时他32岁),深度参与编码的不过4、5人2,经过六个月的紧张编码,2016年4月底,The DAO问世了,Jentzsch认为The DAO能够融资500万美元。

The DAO声称完全透明,正如DAO所宣扬的那样:一切都由代码完成,任何人都可以看到并进行审计。这也正是DAO最大的卖点,在当年区块链的风口上,DAO无疑代表着这个世界上最先锋、最创新、最有前景的未来,所有crypo玩家都兴奋起来了,他们争先恐后地把自己的ETH打入The DAO,换取token,以期未来的巨大收益,即便等不及未来的分红,至少也可以等token升值以后在交易所卖掉。

如果你关心crypto世界,就会知道,后来的ConstitutionDAO(宪法DAO)是很好的一个翻版,而且目标更具体、更明确。宪法DAO虽然没有买到美国宪法的第一版,但其token(PEOPLE币)还活着。

在火热的众筹期间,《纽约时报》于2016年5月21日专门报道了The DAO3,标题是“拥有大量虚拟资本但无资本家的风险基金”,报道称:“虽然Jentzsch是DAO的主要作者,但他不会影响DAO的运转,也不掌控DAO里面的ETH,投资者拥有DAO的token,并依据token的多少,对潜在项目进行投票。Jentzsch认为这种结构免除了他对项目可能发生事情的任何法律责任。”

这篇文章很有先见之明地引用了以太坊创始人之一Joseph Lubin的话:“它还不是一个稳定的东西,年轻的、复杂的机器最容易出现预期外的缺陷和漏洞。”

但不管怎样,The DAO是史无前例最成功的众筹,在27天内,11000多人投资了1200万个以太坊(时价1.5亿多美元),几乎占到了当时以太坊数量的14%,远超Jentzsch的预期。

代币价格大约是100个The DAO token兑换1至1.5个以太坊。

为什么价格还不一样?另外,The DAO是怎么运转的,它的工作原理是什么?

如果你对这些细节并没有兴趣,可以放心跳过下一节,因为并不太影响阅读全文。

如果你有兴趣,可以看一看,了解一下The DAO到底是怎么玩的。

The DAO的基本工作原理

The DAO发行的代币称为The DAO token(TDT)。

为简单起见,以下说DAO,就是The DAO;以下说的token,就是TDT;以下说钱,就是ETH。除非我觉得有必要使用全称。

DAO的筹资阶段(Creation Phase)为27天,在这27天内,发送ETH到DAO的合约账户地址上,就可以按比例得到TDT。

在最初的14天里,1个以太币可以换100个TDT,然后,在接下来的10天里,要想得到100个TDT,每天都需要比前一天多付0.05个ETH,在最后3天里,每100个TDT需要1.50个ETH。筹资阶段的TDT不能被转移,之后则可以使用DAO提供的transfer函数转移给任何以太坊地址。

注:看代码的时候,一开始总找不到1ETH:100TDT这种配比,在代码中怎么看都是1:1的关系。后来才想明白,ETH的最小单位是10^-18(也即wei),而TDT的最小单位是10^-16。以太坊打款都是按最小单位计数的,你打1个ETH进去,事实上是打了1*10^18个wei,你对应得到的TDT也是1*10^18个,按最小单位计算,这就是100个TDT了。

筹资后期的投资者,由于买100个TDT花超过了1个ETH,超出的部分放在一个名为extraBalance的额外金额账户中。这笔钱只有在一定条件下被人提议并投票通过后,才会被转移到DAO的合约账户上。

说到提议(proposal),有必要重点介绍一下。在DAO里面,所谓提议,就是某人提出的将DAO账户中ETH投资到某项目(比如某服务提供商)的建议。比如某个提议可能建议将100个以太币打给某服装厂,用来生产10000件T恤,并声称在一定时间将获利200个以太币。这个投资回报被称为奖励(reward),这个奖励,可能是自动的,也可能是人工的,打回到DAO的奖励账户DAOrewardAccount。

如果要分掉金额奖励,仍然需要一个提议并投票通过。通过后,这个金额会转移到分红账户rewardAccount里,然后,token持有人就可以使用getMyReward函数从rewardAccount中取回自己应得奖励份额,具体可以看一下The DAO项目在Github上的wiki说明4

钱是大家的,投一个项目,总有人同意,也总有人反对,如果投票通过,钱就会投出去,当奖励回来时,所有人都可以分钱,投反对票的也可以拿到钱。

在投票阶段,支持方和反对方就可以展开辩论并投票。任何拥有TDT的人都可以投票,投YES或者NO,投票将根据他们持有的TDT数量加权统计。投票只能进行一次,不能更改。在投票期(也称辩论期)过后,任何人都无法再对其投票。一般类型的提议,投票期是14天,一种叫拆分DAO(split DAO)的提议,投票期是7天。

如果投YES的token多于投NO的token,而且投YES的token达到了总token的某个百分比要求(仅用于一般类型提议),该提议就算通过。这个百分比是通过一个算法来决定的,大约在20%-53%之间,取决于提议金额(amount)的规模。非常大的金额将需要53%的法定人数,而小的金额只需要20%。

投票如果通过,提议指定的ETH金额就会打给提议指定的被投资项目地址(recipient,通常是个合约地址),等这个项目有了收益,回报ETH(reward)就会以自动或其他方式打回到奖励账户(DAOrewardAccount)里。

如果token持有人对一项提议投了票,在投票结束之前,他就不能转移他的TDT。这是为了防止有人投票后拆分DAO,以免面临投票的后果。

这里就要讲到拆分DAO了(这正是攻击者使用的功能!),通常大家都在主DAO里玩,但如果有人觉得这里不好,他们可以拆分出一个子DAO来玩他们自己的,拆分DAO本身也得是一个提议,这种提议的投票期为7天,投票如果通过,就会有一个新的DAO产生,并进入筹资期(Creation phase),在这个子DAO里面,玩法和主DAO是一模一样的。子DAO对主DAO,就像子对象对父对象,他们的结构和方法一模一样,只不过里面的具体值不一样。另外,主DAO里面的token,和子DAO里面的token,不能通用,也不能直接转移。

你可能会问,一个主DAO不就挺好吗,为什么要设计这种拆分能力?这其实是给少数派一种机会,让他们可以玩自己的。倒不是说少数派不搞团结,而是要防范多数派作恶,因为在DAO这个世界里,所有的提议都是靠票来决定,如果多数派作恶,并再没有人能干预了(除非神来干预)。

你可能又会问,多数派能做什么恶呢,他们的钱不也是在里面吗,他们难道不想投好的项目吗?

要知道,所谓投项目,就是把ETH转到一个地址去而已,按照前面所说,如果多数派拥有了超过53%的token,那么,他们发起的提议,不管少数派怎么反对也没有用。在最好的情况,多数派硬是投了一个少数派坚决反对的项目;最坏的情况下,多数派提议把DAO里面的所有资金都转给他们自己控制的地址,这就是明抢少数派的钱。

不过,DAO还设计了一个curator机制来防范这种情况(curator不太好翻译,简单地翻就是馆长,但不够贴切)。一个提议,必须要由curator将提议的recipient地址加入白名单,才能完成ETH转账。curator的唯一责任就是阻止“恶意提议”。

虽然可能性不大,但如果curator也做恶呢。

所以,少数派必须要能有办法保护自己的资金。这就看出拆分DAO的必要性了,在少数派下决心不再和那帮多数派一起玩的时候,他们可以执行提交一个拆分DAO的提议,同时制定新的curator(通常就是提议拆分的人)。少数派可以对这个拆分提议投票,不管投票结果如何,都会拆分(因为并不是投项目,所以不限制),每个投YES的人,都可以通过执行splitDAO函数将他的资金(包括ETH和token)转移到这个新的DAO中,使得多数派无法再花费少数派的钱。少数派以前在主DAO里历史投资中的奖励(回报),也会打到子DAO里面来(注意这里,会对看懂攻击有帮助)。

这个想法起源于Vitalik Buterin发表的一篇博客5,这种另立门户的做法,和开源项目的分叉非常相似。“道不同不相为谋”,DAO给予参与者这种自由。

The DAO的curator是一个多签账户(5 out of 11),该账户由11个人控制,必须有5个以上(含)的人签名,才能行驶curator的权力。希望得到DAO资助的人,必须请求馆长将其地址添加到白名单中才能收到资金。

从事后之明来看,有了多签设计,足可以防范多数派作恶了,完全没有必要设计拆分DAO,拆分DAO给DAO带来大量的复杂性,这种复杂性,最终让DAO陷入万劫不复的噩梦。

对于token持有者,要想拿回ETH,只能是通过getMyReward拿回分红,无法直接取现(withdraw)。如果有人非要取回他的ETH,他只能通过split过程,将他的ETH和token从主DAO里面转移到子DAO里面(在拆分时,还会将他尚未提取的历史分红直接给他),然后在子DAO里再提议将ETH转走。最好这个子DAO里只有他一个人(因为需要投票通过),而且这一共需要7+27+14天,拆分DAO需要7天的投票期,新DAO本身需要27天的筹资期,提议将ETH支付给自己又需要14天的投票期,一切顺利的话,他就可以取走ETH了。 

如果他很着急的话,这个时间还可以再缩短7天,他可以在子DAO里面发起一次更新合约(newContract)的提议,更新合约是指修改DAO的代码,重新部署,然后自动把钱转到新的合约里(这个新的合约可以不受老DAO的限制),这种提议的投票期和拆分DAO一样,只需要7天。

拆分DAO操作有些麻烦,而且需要一定的技术,如果不想这么麻烦,token持有人完全可以把TDT发送给交易所账户,然后兑换为ETH。这相当于token转移了拥有者。

以上就是The DAO的大致工作原理。

在不了解它的时候,我总以为The DAO是一个很宏大的设计,现在看下来,主要是给一帮股东用来投票和分红的。

怕什么来什么

众筹结束当天,5月27日,Dino Mark、Vlad Zamfir、Emin Gün Sirer就写了篇论文6,分析了The DAO存在的若干个漏洞(不含导致DAO被盗的递归调用漏洞),并强烈呼吁人们暂停在DAO中发起项目提议。而当时,有50多个项目提议正在等待token持有者对它们进行投票。

注意其中的Emin Gün Sirer,后面还会提到他,当时他是康奈尔大学的计算机教授。他曾在贝尔实验室和Google工作过,后来选择进入学术领域。他同时是一位典型的极客,不仅从事加密货币、区块链、分布式系统的相关研究,更积极参与这些领域的实践。早在2002年,他就推出了名为Karma的虚拟货币系统,但彼时美国非常在意恐怖主义融资,Karma遇到种种监管障碍,所以最终未获成功。他不仅发表了众多关于加密货币体系的学术论文,还联合创立了IC3(The Initiative For CryptoCurrency & Contracts)组织 ,旨在聚集更多高校研究人员推进crypto的发展,他是共识协议Avalanche的发明人。

据彭博社采访他的记者Matthew Leising说,“Gün的黑发很短,看起来比45岁小10岁”。

Emin Gün Sirer

6月5日,以太坊开发人员Christian Reitwiessner在Github上(用户名:Chriseth)指出了一种对智能合约的攻击模式,即递归调用缺陷(也称重入漏洞),并于6月10日在以太坊基金会的博客予以了发布7

6月9日,区块链基金会创始人Peter Vessenes发表博客,指出这个漏洞实在是太可怕、太可怕了(a terrible, terrible attack )。

Peter Vessenes

6月12日,社区成员Eththrowa发现,The DAO存在递归调用漏洞8

很快,几乎是同一天,以太坊前CCO,Slock.it联合创始人,The DAO创建人之一Stephan Tual宣布,虽然The DAO存在递归调用缺陷,但“由于DAO的分红账户(rewardAccount)里还没有钱,所以没有资金受到威胁”9。因为这个漏洞在实施分红时才会被调用的,而DAO才刚开始,一个项目都没有投出去,没有什么回报,更不要说分红了(所有回报都要经过投票才能进入分红账户)

6月14日,在Github上,The DAO开发者之一Lefteris Karapetsas针对Vessenes提出的问题给出了修复补丁PR,并得到了Christoph Jentzsch的采纳和merge(形成DAO1.1),但是还没有部署。

即便部署,也需要在DAO中投票,需要有53%以上的票数同意,才能将合约以及其上的数据迁移到新地址上。按照前面的介绍,这种转移合约的投票期最少要7天。

6月16日,康奈尔大学的两名研究生在IC3的博客上再次提醒The DAO要注意递归调用漏洞10

6月17日,The DAO遭受攻击,在6个小时内,360万以太币(当时价值约为5000万美元)被转移到黑客拆分出来的子DAO上。

黑客正是利用The DAO代码中的递归调用漏洞,不停地从The DAO账户上转移ETH,那里当时有将近1200万个ETH。

也就是说,专家们刚刚发现问题,还没有搞得很明白,攻击者就已经利用得手了。

这些人虽然都指出The DAO有问题,但没有一个真正发现漏洞到底在哪一行代码,包括并没有部署的DAO1.1,其实也并没有补对地方。

只有Gün在6月13日,和他学生Daina邮件交流时,才提及DAO.sol的第666行可能有问题,但也不是很确定,Gün说“可能这是个大麻烦”,Daina则回复“我觉得没问题”。于是,他们没有再追究下去,也没有公开这个怀疑。

Gün其实说对了,就是第666行有问题。

即便不是第666行,也是第667行。

Gün怀疑的第666行

紧急反击

这天早上,在德国米特韦达,Jentzsch被他兄弟Simon电话叫醒,说DAO被黑了。Jentzsch平日里每天早上都很忙碌,因为那时他有5个孩子(从2岁到9岁)需要照料,他和妻子要给他们做早饭并送上学,但这次不同了,Jentzsch匆忙对妻子说,“你来管孩子,我今天有紧急的事”。

几个小时后,住在里约热内卢的以太坊钱包首席用户体验设计师Alex Van de Sande得知此事。当他弄明白手机为什么快要被Skype信息爆掉的时候,他转向他的妻子说:“还记得我告诉你那一大堆无人能偷走的钱吗?” 她点了点头,“它被黑掉了”。

他的第一个想法是把他的DAO代币拿出来。他拥有大约10万个,当时的价值约为15000美元。但很快他就意识到,不应该放弃DAO,而应该努力拯救它。

Alex Van de Sande

Jentzsch在之后的几周里几乎是全天候工作,token持有者纷纷问他该怎么做,他必须要想出办法,让投资者拿回他们的钱。

好在,攻击者并不能立刻拿走他偷的ETH,子DAO和父DAO有相同的结构、规则和漏洞,这个子DAO刚刚建立,它现在面临的是27天的筹资期(Creation Phase),在这个阶段,没有任何人可以挪动资金(多亏了Jentzsch这个设计!)。

这就有时间抢救DAO。

6月17日,V神(Vitalik Buterin)在以太坊基金会的博客上发布了“重要更新”声明11,称DAO受到攻击,他给出一个解决方案:

“我们已经提出了一个软件分叉,这个分叉不需要回滚,不会‘反转’任何交易或块,只是让任何试图减少DAO及其子DAO余额的交易无效。这样,攻击者即便在27天后,也无法取走资金。这将给我们采取进一步措施提供充足时间,包括让代币持有人有能力收回他们的ETH。”

“矿工和矿池应保持正常运转,如果同意以太坊生态系统的这一道路,待软分叉代码就绪后,下载代码并运行它。DAO代币持有者和以太坊用户应警惕而冷静,交易所则尽可以恢复ETH交易。”

换句话说,这个软分叉方案将在以太坊代码中建立一个黑名单,防止坏人领走它盗取的东西。

软分叉只是抛弃指定交易,它不改变以太坊上已经发生的交易,也不回滚任何数据,只需要矿工们更新软件就可以了(前提是大多数矿工们都同意)。其他人如果不更新软件,也不影响正常使用以太坊,升级的节点和未升级的节点是兼容的。

很快,当天,EthCore的Parity客户端就实现了软分叉,以太坊基金会的的Geth则在第二天完成更新。

这篇文章一出,攻击者就停了下来,不再继续盗取资金。他已经攻击了6个小时,偷走了3,641,694个ETH,占据了The DAO总资金的1/3,而The DAO里面的ETH,则占据了那时所有ETH的15%。

Alex Van de Sande 回忆他当时的想法:“我们甚至不明白这个人为什么停了下来。”12

黑客在那6个小时内,利用两个合约账号,调用主DAO的splitDAO函数,每个地址调用大约250次,几乎每次调用都会使得splitDAO递归执行30次,平均每次递归调用大概盗走7000多的ETH13,这样,总共盗走了360多万的以太坊。

事情刚开始的时候,没人知道攻击者是怎么做到的,尤其是攻击者为什么能重复发起那么多次攻击,因为执行完一次递归后他的token就会被清零,但他却执行了500多次!

在reddit上,人们激烈讨论了5,6个小时后,终于想明白攻击者是怎么搞的,这才有了后面的白帽子反击。当天晚上,Emin Gün Sirer的研究生Phil Daian就写了一篇详细分析攻击者手法的文章14

这篇文章最后,Daian向他的导师Gün喊话:“Gün,我们已经离得太近了——但很遗憾还不够:)。”

而Gün则在后来的一次采访中说:“如果当时Daian回复邮件确认是漏洞,我就会告诉大家”。

白帽子的行动

6月21日,DAO的账户再次出现转移,采用同样的攻击手法,更多的钱进入到另外两个子DAO,分别取走了727.7万和35.3万个ETH15

人们惊呼,攻击又开始了!

几个小时后,Sande在推特上宣布:“DAO正在安全转移资金,不要惊慌。”

Sande在推特上宣布白帽子行动

这是由两、三人组成的“罗宾汉小组”(Robin Hood Group)干的,其中一人是Griff Green,来自slock.it,同时是The DAO的主要开发人员之一,正是他第一个将袭击消息转发给Jentzsch的兄弟Simon的(Simon也是Slock.it的联合创始人)。

虽然人们不清楚这个小组里都有谁,但人们信任Sande,这些白帽子正使用攻击者的手法将剩余资金转移到他们拆分的子DAO里。与此相对应,黑客控制的DAO被称为“The Dark DAO”,简称DarkDAO。

但攻击者确实很聪明,他总是跟随新的split提议并投YES,他总能进入新split出来的DAO,以试图在新DAO里再发起splitDAO攻击。

另外一组白帽黑客则试图通过给DarkDAO打款以成为DarkDAO里面的成员,毕竟这个DarkDAO正处于筹资期!一旦成为DarkDAO的一员,他们就可以用同样的攻击手法拆分DarkDAO。

由于DAO的这个漏洞存在,如果不去管它,可能就会不停有这样的攻击和反击,DAO会不断分裂下去,白帽、黑帽不断加入,而且也不知道谁是好人谁是坏人,DAO会成为黑客们的乐园,而大部分原始代币持有人则可能一无所获。

如果DAO因此失败,整个以太坊网络都会遭殃。

第二天,ETH的价格应声下跌,暴跌约30%,从20多美元跌至13美元以下。

攻击原理是怎么样的

对于非技术读者而言,了解这个漏洞,只需要理解这个场景就可以了:

一名银行储户,同时也是一名黑客,对着一台自动取款机开展攻击,他的银行卡上有1万元,他插入卡片,要求自动取款机吐出1万元。

取款机的取款功能是这样设计的:

1. 读取储户余额;
2. 等待储户输入取现金额;
3. 如果取现金额<=余额,吐钱;
4. 更新储户余额(也即减去取现金额)。

如果黑客有能力在第3步(也即吐钱)后,通过奇怪的方式重新开始第1步,黑客就实现了重入攻击。

因为这时取款机还没有执行第4步,那么黑客的卡上始终有1万元,他就可以一次又一次地取现1万元。

取款机如何修改这个bug:

1. 读取储户余额;
2. 等待储户输入取现金额;
3. 如果取现金额<=余额,更新储户余额(也即减去取现金额);
4. 吐钱;如果吐钱失败,恢复储户余额。

对懂一些技术的读者而言,可能最想了解的是:在智能合约中,这个递归调用漏洞到底是怎样的?

简单地说,如果合约A的funcA函数,调用合约B的funB函数,而funcB里面,又去调用A的funcA,那么就会形成一个死循环,两个函数不断地调用对方(也即递归、重入),直到最初调用时所带gas耗尽,要知道,gas本来就是为了防止死循环的。

一个重入漏洞导致ETH被盗的原理示例:

1. 有两个智能合约,一个是合约A(受害合约),一个是合约B(攻击合约)。
2. 合约A的pay函数让用户可以将其所有存款余额提走。
3. 如果写A的程序员没有经验,在pay的实现中是先付钱后更新余额,就会有问题。
4. 在Solidity语言中,如果对一个合约执行call调用而不指定函数,合约的匿名函数(fallback函数)就会被调用。
5. 攻击者可以写一个合约B。在B的fallback函数中调用A的pay。
6. 攻击者用合约B调用A的pay,A用call打钱给B,触发B的fallback再调用pay,然后A又打钱,又触发B调用pay,循环往复。

下面用最少的代码量,予以代码层面的展示。(看这个代码,有助于理解攻击者如何攻击The DAO)

A合约的片段:

function pay() public 

    \\\\取出调用者的地址
    address addr = msg.sender;
    \\\\获取调用者的余额
    uint amount = balances[addr];
    \\\\将余额打给调用者的地址
    addr.callvalue: amount();
    \\\\将调用者的余额归零
    balances[msg.sender] = 0;


B合约的片段:

\\\\计划重入30次
unit times = 30;
fallback() external payable 

    \\\\对重入次数减1
    times = times - 1;
    \\\\如果仍有次数就再执行一次
    if (times > 0)
        A.pay(); \\\\调用A的pay函数

B先调用A.pay(),辅以足量的gas,A的addr.callvalue: amount();就会执行,然后就会触发B的fallback(),然后又执行pay(),这样B的余额清空那句就始终不能执行,直到执行30次后才走到余额归零那句,如果B本来只有10个ETH,这样就能搞到300个ETH(当然前提是A上有这么多钱)

修补的方法也不难:

一是可以先清零,再发送;
二是可以用send函数或者transfer函数发送金额,这两种调用只会发送2300个gas,不会带更多的gas;而call不一样,call会带上所有剩余的gas,如果一开始攻击者给的gas多,就会导致多次重入;
三是可以设置互斥量,也就是加一个变量来判断是不是已经在执行中。

这三种方法,用其中一个就可以。这篇文章16有很好的介绍和示例,有兴趣可以看一看。

黑客是怎么攻击The DAO的?

具体来说,黑客发起了一个splitDAO的提议,自己控制的两个合约账号都投了YES(虽然也有两个人投了NO,但是没用,具体见前面的介绍,投票结果并不影响拆分)

拆分DAO的提议是第59号提议:标题为“lonely, so lonely”,发起于2016–06–08 05:38:01 UTC,注意,Vessenes发布Bug的时间是6月9日。

攻击者控制的两个合约账户(也即攻击合约):

0xc0ee9db1a9e07ca63e4ff0d5fb6f86bf68d47b89,创建于6月15日
0xf835a0247b0063c04ef22006ebe57c5f11977cc4,创建于6月15日

过了投票期7天,攻击者可以执行拆分了。黑客控制这两个合约分别执行主DAO里的splitDAO函数,转移自己份额的ETH到子DAO,并在子DAO里自动获取自己应得的token,这个过程也会涉及分红的转移,而正是分红转移这块,DAO使用了能引起递归的call调用。

攻击者控制的合约里面,则肯定在fallback函数里再次调用了splitDAO,这个好戏就上演了。

黑客开始攻击的第一次交易17

攻击开始的准确时间:2016–06–17 03:34:48 UTC, block 171849718

“The Dark DAO”的地址为:0x304a554a310c7e546dfe434669c62820b7d83490
攻击者的主控账号:0x969837498944ae1dc0dcac2d0c65634c88729b2d

Tual前面说因为还没有分红金额,所以资金不会受影响,但对攻击者而言其实根本不是问题,攻击者自己可以往分红账户rewardAccount里面打一些ETH!

因为,rewardAccount账户并没有任何特殊之处,就是一个普普通通的地址,往里面打钱没有什么限制,而分红时,也仅仅是根据这个账号余额来计算的。

这充分说明,一个系统的开发者,往往不能意识到自己所写代码的问题,他们的思维容易被系统设计所束缚。

Tual只是想着目前还没有任何项目被投资,所以当然不会有回报,即便有回报,也还没有人提议分红,所有分红账户上的余额肯定为0。

也仅仅是业务逻辑层面的考虑,他没有想到攻击者自己可以将钱打入分红账户!(或者Tual想到了这点,但认为攻击者赚自己打的分红没有意义)。

攻击者,要赚的并不是自己打入的那点分红,而是能重入splitDAO,能给子DAO多次赋予主DAO账户上的ETH。

在代码层面上讲,这到底是怎么做到的呢?

对于开发者出身的读者,可以看下一节,否则尽可以跳过。

代码的漏洞在哪里?

你现在仍然可以在Github上下载DAO 1.0版本的代码19

在DAO.sol中,第603行到第676行,是splitDAO函数的定义。(不得不说,这个函数太长了,编程高手会知道,一个函数不应该这么长20

现在我们看一下splitDAO函数的代码,不太重要的部分我用“...”略去了,里面的中文注释是我加的。

function splitDAO( uint _proposalID, address _newCurator
    ) noEther onlyTokenholders returns (bool _success) 


    ...

    // Move ether and assign new Tokens
    // 根据调用者拥有token的多少,计算应该将多少ETH从主DAO打给子DAO
    // 具体算法:应该转移的ETH金额:调用者token余额*主DAO的ETH金额/总共的token数
    uint fundsToBeMoved =        
        (balances[msg.sender] * p.splitData[0].splitBalance) /
        p.splitData[0].totalSupply;
    // 调用子DAO的createTokenProxy函数(在TokenCreation.sol中),配上应该转移的ETH金额   
    // 子DAO收到ETH后,通过createTokenProxy函数生成调用者在子DAO里面的token
    // *下面这句就是攻击者通过递归调用偷走大量ETH的那句!!!*
    if (p.splitData[0].newDAO.createTokenProxy.value(fundsToBeMoved)(msg.sender) == false)
        throw;

    ...

    // Burn DAO Tokens
    // 注意这里只是调用Transfer事件函数做了个日志,并没有真的转移token
    // *如果这里的大写“T”是一个小写“t”,将调用transfer函数,代币将会清零,攻击将无法发生!!!*
    // 下面这句就是Gün在6月13日曾经质疑的第666行!
    Transfer(msg.sender, 0, balances[msg.sender]);
    // 将调用者应得的分红转给他。*正是这句(第667行)导致了递归调用!!!*
    withdrawRewardFor(msg.sender); // be nice, and get his rewards
    // 下面这几句是转移后的账户清理,将调用者的token余额在主DAO里清零
    totalSupply -= balances[msg.sender];
    balances[msg.sender] = 0;  //这个时候才开始更新账户余额,太晚了!!!
    paidOut[msg.sender] = 0;
    return true;

注意上面所示的第666行,正是Gün提出疑问的地方。

从Github的日志里面可以看出,这段代码是Jentzsch维护的21

但这事也怪不得Jentzsch,毕竟,写这段代码的时候,还没人知道有递归调用漏洞,这就很难苛求他了。

现在我们继续看,真正引起递归调用的那句,是第667行,也即withdrawRewardFor函数(也在DAO.sol里),这个函数是怎么写的?

function withdrawRewardFor(address _account) noEther internal returns (bool _success) 

    ...
    // 这句计算调用者应得的分红:调用着拥有的token*分红账户余额/总token数 - 调用者历史上已经提取过的分红
    uint reward =
        (balanceOf(_account) * rewardAccount.accumulatedInput()) / totalSupply - paidOut[_account];
    ...
    // 实施分红动作,也即将钱打到调用者的地址上。*正是这句引起了递归调用!!!*
    if (!rewardAccount.payOut(_account, reward))
        throw;
    // 将本次分红金额累计到调用者已经提取过的分红值中
    paidOut[_account] += reward;
    return true;

继续跟踪,引起递归调用的那个payOut函数(在ManagedAccount.sol中)是怎么实现的?

function payOut(address _recipient, uint _amount) returns (bool) 

    ...
    // 将ETH金额_amount打给_recipient,也就是调用splitDAO的那个地址
    // ***这才是真正的引起递归调用的call语句!!!***
    if (_recipient.call.value(_amount)()) 
        //记录日志
        PayOut(_recipient, _amount);
        return true;
     else 
        return false;
    

你看,通过我们不断深挖,终于找到了引起递归调用的元凶语句,正和我们前面介绍的原理攻击一样,调用了call来打钱!

而攻击者控制的合约账户,其fallback函数中必然有一句splitDAO!

比较老练的读者会提出这么个问题:递归调用总有结束的时候,那么,攻击者在主DAO中的balance就会被清零,攻击者就没有token了,那他控制的两个账户怎么都能发起500次之多的攻击呢?

这也是困惑我和当时很多黑客的问题,攻击者怎么做到的?

其实并不难,当攻击者在主DAO中的余额被清零后,他如果在主DAO里其他账户还有钱,调用transfer给这个攻击地址转账token就好了!(这可是以1博30的盈利机会!)

但攻击者做得更巧妙,这点钱他都没有花。

从最容易理解的角度来讲:他仅仅在递归调用快结束时,将攻击账户中的TDT余额转给自己控制的另一个账户,然后让递归结束(正如前面示例中通过计次,主动不再调用splitDAO),这时,上次调用的splitDAO函数终于可以执行耽搁已久的余额清零了,但攻击账户里的余额已经被转走了。

然后,这个余额再转回来,再发起一轮攻击。

“攻击者”发声

6月18日,有人自称是攻击者,并在pastebin上发声22

文章比较长,翻译过来就是:

我仔细研究了DAO的代码,发现split功能可以获得额外奖励后,我使用了这个功能,并合法地领到了3,641,694个以太坊,我非常感谢DAO的这个奖励。我认为,The DAO的这个功能,是为了促进去中心化和鼓励创建“子DAO”。

有些人把这种“特性使用”称为"盗窃“,我感到很失望。我是在很正当地使用智能合约的这一明确编码功能,我的律师事务所已经告诉我,我的行动完全符合美国的刑法和民事法。你们可以仔细看看The DAO的条款:

“The DAO的条款完全体现在智能合约代码中,代码部署在以太坊区块链的0xbb9bc244d798123fde783fcc1c72d3bb8c189413地址上。除了代码,任何条款解释、任何文档、任何交流,都不能修改或增加任何额外的义务或承诺。任何对条款的解释和描述,都只是出于教育目的,都不能并取代或修改区块链上的The DAO的代码;如果你认为这里提供的描述与The DAO代码的功能之间存在冲突或差异,The DAO的代码是最终的解释。

软分叉或硬分叉相当于扣押我合法和正当的以太坊,这是智能合约合法赋予我的。搞这样的分叉,将永久地、不可逆转地毁掉所有的信心,不仅是对以太坊的信心,而且是对智能合约和区块链技术的信心。许多以太坊持有者将抛弃他们的以太坊,开发人员、研究人员和公司将离开以太坊。不要搞错了:任何分叉,不管是软的还是硬的,都会进一步损害以太坊,破坏其声誉和吸引力。

我保留对任何非法盗窃、冻结或扣押我合法以太坊的帮凶采取法律行动的权利,并积极与我的律师事务所合作。这些帮凶将很快收到停止通知。

我希望这次活动成为以太坊社区的一次有价值的学习经历,并祝愿大家好运。

发文的尾部附上了消息hash值和签名。

很多明眼人看出来这大概是伪造的,那个签名说明不了问题,没有证据表明这个发帖人拥有黑客的私钥。

不管此人是不是攻击者,这篇文章写得很好,也很有说服力:The DAO项目在Github上给出了官方宣言:所有条款都以代码为准,代码超越一切解释和说明。

支持The DAO的人可以说,没错,是代码说了算,但代码没有让你重入啊!

黑客则可以反驳说:代码没有说不让重入!

如果坚持一切都是代码说了算,那黑客就没有做错什么。

所以,The DAO必须承认:通常都是代码说了算,但如果代码有Bug,那就不再说了算。

这个逻辑可以吗?

Gün在案发当天发表博客23,说到:

DAO没有自己的规格说明书,没有人知道DAO的程序员写代码的时候是怎么想的,如果说“代码就是最好的文档”,那就无法判断什么是黑客行为,什么不是。攻击者如果因为不小心而丢了钱,我相信开发者会毫不犹豫地挪用他的资金,并说“程序化的资金新世界就是这样运转的”,而当攻击者拿走钱时,开发者要想保持言行一致,他应该说:“干得好!”

Emin Gün Sirer

软分叉并不可行

V神提出的软分叉,想得挺好:抛弃那些减少DAO(包括子DAO)上金额的交易。

80名矿工在软分叉投票的头几天就决定支持,软分叉似乎很快就可以实现。

直到Gün于6月26日发表了一篇文章24称:软分叉会有DoS漏洞,攻击者可以使用这个漏洞发起拒绝服务攻击。人们才意识到软分叉不可行。

漏洞是这样的:因为从DAO减少金额的交易(比如含有调用DarkDAO.splitDAO语句的交易)将被抛弃,不会出现在区块上,所以矿工无法因此收取gas;另一方面,这种交易也并不抛出异常,矿工无法通过状态回退而收取gas;这成了前所未有的一种交易情况:执行交易,无异常抛出,不计入区块,不收gas费。

软分叉引入了这种可能性:矿工干活但不收交易费!

对以太坊不怀好意的攻击者,可以因此发送大量垃圾交易:执行耗费大量计算资源但包含DarkDAO.splitDAO的程序。这相当于可以无成本(不花gas费)对以太坊发起DoS攻击。

Emin Gün Sirer在他的文章中作了详尽的分析,说如果采用其他手段来修补这个DoS漏洞,只会让事情变得更糟。

社区不得不于6月28日叫停25了分叉。

这充分说明,一种临时性的补救,貌似容易,但会带来新的问题。

从程序设计角度来讲,任何不够干净的设计都会带来技术债务,越是这种设计外的补丁措施,越容易把你带入沟中。

看来,只能硬分叉了?

分叉路口

来一次硬分叉?

6月18日,Slock.it联合创始人Stephan Tual在他的博客中发表“分叉路口”(A Fork in the Road)一文26,指出:

“随后的硬分叉甚至可以将所有的以太坊,包括DAO的extraBalance和被盗的资金,重新回到一个智能合约中。该智能合约将包含一个单一的函数:withdraw()。这将使每个参与DAO的人都有可能提取他们的资金,因为到目前为止,DAO还没有资金发到外部,所以不会有任何损失。”

和前面说的软分叉不一样,硬分叉会将DarkDAO上的以太币强制转到一个新的合约上,这个合约叫“取现DAO”(withdrawDAO),只有一个功能:取现。

这当然是非常规了,相当于出现了一只上帝之手,未经持有人许可,把钱从一个账号转移到另一个账号。

这需要所有的客户端都更新以太坊软件,如果你不升级,你就看不到这个改变。硬分叉的一个显著特征就是:新软件和升级前软件不兼容。

以太坊有过很多次硬分叉,如代号为“君士坦丁堡”、“圣彼得堡”、“柏林”、“伦敦”的硬分叉,但都是为了以太坊的完善和发展,为了拯救一个项目而硬分叉的,仅仅只有这一次,同时也是备受争议的一次。

以太坊方面认为:如果他们什么都不做,以太坊网络就会遭受挫折,可能需要数年时间才能恢复;

反对硬分叉的人则认为:但如果以太坊干预,就树立了一个危险的先例,以后有类似的事还救不救?

一名反对者说,“硬分叉是一个有效的选项,但它应该保留给需要紧急修改以太坊协议本身的情况,而不是用于上面运行的项目。以太坊基金会参与并推广DAO项目是一个错误,它只会篡夺人们对以太坊作为其他项目基础基础设施的信任。”

另一名反对者则说:“以太坊完全按照预期工作,我认为在软件在完全按预期工作时不应该更新。你要承担你投资的风险,如果中心化权威救助这样的事,那就是和加密世界对立。从某种程度上来说,这就是雷曼兄弟被允许失败的原因。交易就是交易,如果你为特定玩家改变规则,所有玩家也会想要特殊待遇。”27

The DAO大概就是金融危机里那些“大到不能倒”的银行。

经历多次激烈争吵(在Reddit、Slack频道、电子邮件和Skype上)和V神公开支持后,大多数以太坊社区支持硬分叉。

7月15日,Christoph Jentzsch在slock.it的博客上,提出了硬分叉设计规范:Hard Fork Specification28,并最终形成了EIP 77929

简单地说,就是把主DAO、所有子DAO,以及额外账户上的ETH,都在以太坊节点软件中以硬编码方式,转移到取现合同WithdrawDAO上。然后让主DAO的token持有者可以拿走自己的投资,对于在子DAO的金额以及额外金额extraBalance账户上的钱,则由主DAO的curator提走,然后再以合适方式归还原投资者。

硬分叉的期限是7月21日,这是黑客能够转移资金的日期,因为DarkDAO筹资期要27天,更新DarkDAO合约或再次split的投票需要7天。

抓紧时间!

以太坊软件的开发团队(包括Geth、Parity、EthereumJ、Eth等)很快实现了这个规范,给出了升级后的版本,是否采用则要看矿工们的行动了。

2016年7月20日,自预先设定好的升级时间(第1920000区块开始)开始,能看到大多数矿工和节点都转移到新版本了,硬分叉顺利实现了。

第二天,Gün回到康奈尔大学校园,和正在这里参加IC3研讨会暨加密训练营的成员们一同庆祝,V神也在这里,Gün带来了两瓶香槟,并在酒瓶上贴上了标签:"祝贺分叉成功”。(Congratulations on the successful fork.)30

V神和Gün庆祝分叉成功

然而,旧的以太坊区块链仍然有支持者,这条链并没有断,以太坊形成了两条链,一条是以太坊官方认可的ETH链,一条为以太坊经典链 (Ethereum Classic),即ETC,这两条链直到今天仍然都活着。只不过ETC的价格要比ETH低很多。

2016年9月,Poloniex和Kraken宣布下架DAO token的交易。31

The DAO就这么结束了。

黑客是谁,他得到了什么?

Tual在“分叉路口”那篇文章里说:

“我估计,这个世界上,能对以太坊和Solidity如此了解,而且能在漏洞发布后如此短时间完成这种攻击,大约只有100人。要知道,这个漏洞是6月9日公开的,而攻击者发起splitDAO攻击,还需要经过投票期7天。”

Tual说,攻击者一定知道,虽然不一定会有硬分叉,但肯定会有软分叉,他永远不会直接享受他的攻击成果。他要么是为了从市场盈利,要么就是坚决反对DAO。

“无论哪种方式,攻击者应该是离安全建议者(security advisories)很近的人,安全建议者绝大多数都是我们信任的朋友、同事和顾问,我很确信,我们至少有一个人一定在过去的会议或聚会上碰见过攻击者。攻击者最好明白,所有交易所都在追溯过去两周内的交易,这意味着他很可能被追踪并被起诉。”

Tual在另一次采访中说:

“此人是以太坊和Solidity语言的绝对专家,他一定是全职Solidity程序员,业余爱好者是搞不出这种攻击的”。

Stephan Tual

在社交媒体上,一些人敏锐地指出,袭击前不久,Bitfinex上出现了300万美元的以太坊空头(short),如果这是攻击者做的,他可以趁此获得近100万美元的利润。很多人认为,既然攻击者这么聪明地实现了攻击,他一定会想到这点。

但我个人感觉,做空应该不是他干的。一些研究认为攻击者是一个团体,我也不这么认为,我直觉认为他是单人作战,而且是男性。

尽管他非常聪明,我想他没有更多时间、精力、资金再去买空,毕竟买空也需要准备和部署,而且更容易暴露出真实身份。

攻击者真不是一般人,在后面几年内,没有人知道他是谁。虽然很多人详尽分析了他所控制的账号和交易32,但并没有发现什么有用的线索。

他用来发起攻击的钱来自ShapeShift,这是一个加密货币兑换网站,在2018年10月1日之前,ShapeShift没有任何用户帐户、注册或注册流程,这使得追踪十分困难。

直到2022年2月,才有人说她找到了攻击者。《福布斯》杂志的记者Laura Shin33,借助她的消息来源以及加密追踪公司Chainalysis的取证工具,指出了她认为最可疑的人:现年36岁奥地利程序设计师Toby Hoenisch,DAO被盗时,Hoenisch在新加坡,他曾是加密借记卡项目TenX的联合创始人和CEO。

Shin猜测,Hoenisch曾对外警告The DAO项目含有安全漏洞,可能是因为被忽视而决定展开攻击。但Hoenisch向Shin否认了此事。

硬分叉以后,在以太坊经典链上,攻击者的战利品(364万ETC)仍然存在。那年夏天,攻击者将他的ETC转移到一个新地址上,10月下旬,他使用ShapeShift将ETC兑换成BTC,由于那时ShapeShift仍然不要求注册,因此即便能看见黑客的活动,也不知道他是谁。

在接下来的两个月里,黑客用此办法获得了282枚BTC。后来,大概是ShapeShift经常阻止他的交易,他放弃了套现,留下了340万以太经典(ETC),当时价值320万美元,

以上是关于惊魂33天:The DAO被盗 | 区块链安全经典案例的主要内容,如果未能解决你的问题,请参考以下文章

原创智能合约安全事故回顾分析:The Dao事件

科普 | 被盗取700万美元的DAO Maker是什么,与DAO有何关系?

SAFEIS安全报告:加密史上十大被盗事件梳理及应对策略

AMA预告6亿美金!被盗金额越来越大,DeFi还安全吗?

blockchain | 区块链安全靶场 The Ethernaut

区块链交易所开发,区块链系统开发公司