The DAO的漏洞利用分析

2016-06-21 15:45 来源:巴比特 阅读:10754
我相信大家都已经听说了关于DAO被盗高达$150M的头条新闻是由黑客使用递归以太坊发送漏洞利用。

我相信大家都已经听说了关于DAO被盗高达$150M的头条新闻是由黑客使用递归以太坊发送漏洞利用。

这篇文章将会是第一个涉及这一系列可能是什么,提供通过区块链来对攻击者的行动进行时间轴上的追踪,解构和解释在技术层面上到底是什么出错了。第一篇的发布将会关注于攻击者具体是如何从DAO中偷走了所有的钱。

一个多级的攻击

DAO的这次漏洞利用显然不是微不足道的;具体的编程模式使得DAO的弱点不仅可知,并且由DAO的创建者在早期计划更新框架的代码进行修复。讽刺的是,当他们在写博客发布并庆祝胜利时,黑客正在准备和利用一个漏洞,以他们刚刚修复的的同样功能作为目标吸干了DAO的所有资金。

让我们概览一下这次攻击。进攻者分析了DAO.sol,并且注意到了“splitDAO”功能容易受到攻击去递归的发送我们上文提到的模式:这个功 能最后会更新用户的余额和总额,所以如果我们能在它访问splitDAO 之前再调用这项功能,我们就可以无限递归使用来转移我们想要的不论多少的资金 (代码注释标志为XXXX,你可能需要下滑才能看到):

~IMN}B0BHJNKT$2W{HJPS_C

基本的思路如下:提出一个split。运行split。当DAO要撤销对你的响应时,在撤销完成之前调用这项功能去执行split。这个功能将会运 行而没有更新你的余额,这个命令行我们在上面标志为“the attacker wants to run more than once”将会运行多次。它会做什么呢?源代码在TokenCreation.sol中,它会将代币从the parent DAO转移到the child DAO中。基本上攻击者就是利用这个来获得比他本应该得到的更多的代币转移到child DAO中。

DAO是怎么决定要转移多少的代币呢?当然是使用平衡数组:

2

因为每次攻击者调用这项功能时p.splitData[0]都是一样的(它是提议p的一个属性,并不是DAO的一般状态),又因为在平衡数组更新之 前攻击者可以从撤销响应中调用这项功能,攻击者可以得到这个代码然后随意的运行无数多次的攻击,这样每次都会有相同数额的资金被转移出。

攻击者为了成功的利用这个漏洞要做的第一个铺垫是需要有DAO的撤回功能,实际运行时,它易受攻击来决定性的递归的发送漏洞。让我们来看下在代码中需要做什么才能让这一切发生(来自DAO.sol):

3

如果黑客可以得到第一个if声明评估为错,这个声明被标志位易受攻击将会运行。当这个声明运行,以下的代码将会被调用:

4

注意被标志的命令行具体是如何成为易受攻击代码,我们链接的描述这个漏洞利用会提到。

这个命令行将会将会从DAO的协议中发送信息到“_recipient”(攻击者)。“_recipient”当然包含了错误的功能,它将会再次调 用splitDAO发送与攻击者第一次调用时产生的参数。记住因为这一切都发生在splitDAO中的 withdrawFor里,在splitDAO中 的代码更新余额并没有运行。所以split将会发送更多的代币到child DAO,接着请求收益将会再次被撤回。它将会再次尝试发送代币到“_recipient”中,在更新平衡数组之前它会再次调用split DAO 。

将会如下运行:

  1. 提出一个split然后等待直达表决期限到期。(DAO.sol, createProposal)

  2. 运行split。(DAO.sol, splitDAO)

  3. 让DAO发送一份额的代币到新DAO(splitDAO -> TokenCreation.sol, createTokenProxy)

  4. 确保DAO在(3)之后在更新你的余额之前尝试发送给你收益。(splitDAO -> withdrawRewardFor -> ManagedAccount.sol, payOut)

  5. 当DAO在步骤(4)时,以与(2)相同的参数再次运行splitDAO 。(payOut -> _recipient.call.value -> _recipient())

  6. DAO将会发送给你更多的子代币,并在更新你的余额之前撤销对你的收益。(DAO.sol, splitDAO)

  7. 返回(5)!

  8. 让DAO更新你的余额。因为(7)返回到(5),所以这将不会发生。

(边注:以太坊的燃气技术在这里并没有帮助。Call.value会默认的传递交易所需的燃气,并不像发送功能那样。所以只要攻击者支付的话代码就会运行,这被认为是一个低级的漏洞利用意味着不稳定)

有了以上这些,我们可以一步步的反追踪DAO是如何一步步被掏空。

 

第1步:提出split

 

第一步只是简单的提出一个常规的split,就像我们之前提到的那样。

攻击者在这一步骤中在区块链中在DAO中提出#59,命名为“Lonely, so Lonely”。

因为这一个命令行:5
他不得不等待一个礼拜让这个提议获得批准。不过没关系,这只是一个和其他一样的简单的split提议!没有人会过度的关注它的,对吧?

 

第2步:得到收益

这在 slock.it’s previous posts on the matter中得到完整的解释,在DAO中仍没有分发收益!(因为并没有产生收益)。

就像我们在概述中提到的,这个关键的命令行需要在这里运行:

6

如果黑客能得到第一个被标志的命令行去运行,那么第二个被标志的命令行将会运行他选择的默认功能(这称为返回splitDAO就像我们之前描述的那样)。
让我们解构第一个if声明:

7

余额的功能被定义在 Token.sol中,当然它具体是这样:

8

rewardAccount.accumulatedInput()命令行是从ManagedAccount.sol中的代码评估出来的:

9

幸运的是accumulatedInput 的操作很简单。只需要使用收益账户的默认功能!

10

不仅如此,因为减少accumulatedInput到任何地方是没有逻辑可言(它沿着账户在所有交易中的输入轨迹),攻击者要做的就是发送一些Wei到收益账户,我们的初始条件将不仅会评估为错误,且它的成分值在每次调用时都会评估为相同:11

记住因为balanceOf 指代余额,它从没有被更新,因为在splitDAO中的代码从没有真正执行, paidOut和 totalSupply 也是没有被跟新的,攻击者便声称他们的这微小份额的收益是没有问题的。因为他们可以认领这一份额的收益,他们可以运行默认功能并 重新返回到splitDAO。

但他们真的需要包含一个收益吗?让我们再次看下这一个命令行:

12
如果收益账户的余额为0会怎样?那么我们可以得到:

13

如果没有被支付,这将会一直被评估为错误且永不停止!为什么呢?因为初始命令行是等值的,在从双方减去paidOut之后,得到:

14

第一部分到底被支付了多少。所以支票事实上是这样的:

15

但如果amountToBePaid是0,DAO无论怎样都会支付你。这对我来说没有太大意义–为什么要用这种方式浪费燃气?我想这就是为什么许多 人认为攻击者需要一个收益账户的余额来进行攻击,这个事实上他们并不需要的东西。不论是一个空的收益账户还是满的账户攻击都是一样运行的。

让我们看一下DAO的收益地址。从Slockit pegs中DAO的账户文件,这个地址是0xd2e16a20dd7b1ae54fb0312209784478d069c7b0。检查这个账户的交易你会看到这个模式:200页的.00000002以太币交易到0xf835a0247b0063c04ef22006ebe57c5f11977cc4 和0xc0ee9db1a9e07ca63e4ff0d5fb6f86bf68d47b89,攻击者的两个恶意的协议(我们之后会谈到)。这个交易中每一递归调用withdrawRewardFor,我们上面有提到。所以在这次攻击中确实有一个收益账户的余额,攻击者并没有应用到它。

 

第3步:一个巨大的短缺

 

在社交媒体上有一些完全未经证实的指控指出在攻击之前在bitfinex上有3百万美元的以太币短缺,声称这次短缺将近有1百万美元的利润。

不论是谁构件和分析这次攻击都很清楚DAO的某些性能(尤其是任何的split都必须像初始DAO一样运行同样的代码)要求攻击者在撤回恶意的 split中的货币都必须等待通过child DAO的创建期间(27天)。这给了社区时间来对偷窃做出反应,可以通过软叉冻结攻击者账户或者通过硬叉撤回整个协议。

任何有经济动机的攻击者尝试利用测试网络上的漏洞时,都会想要确保利润,不管是潜在的回滚或者是叉卖空底层的令牌,由恶意split触发的智能协议 导致在几分钟之内的价格猛跌,提供了一个绝好的利润机会,但并没有证据表明攻击者利用了这次机会,我们至少可以得出这样的结论在这方面他们是愚蠢的。

 

第3a步:防止退出(抵抗是无效的)

 

另外的一个可能性攻击者必须要考虑的是在攻击者掏空DAO之前,一个DAO的split出现了。这样子的话,由于有另一个用户作为了单独的管理者,攻击者就没有机会接触到DAO的资金。

不幸的是攻击者很聪明:有证据表明攻击者的的所有split提议都来自于他自己的条款,确保他对于任何的DAO split持有一些许可。我们在这篇文章的后面会讲到由于DAO的性质,我们这里讲述的DAO的split对于同样的攻击是脆弱的。攻击者要做的就是通过 创建期之后,发送一些以太币到收益账户,自己提出和执行一个split远离新的DAO。如果他能在新的DAO的管理者更新代码消除脆弱性之前执行,他就能 成功的将不属于他的以太币从DAO中提出来。

由这里的时间轴上可以注意到攻击者在开始恶意的split时就开始这一切,几乎是同时。我将这一切认为更多的是对DAO没必要的羞辱胜过是经济上可 行的进攻:事实上已经掏空了整个DAO,通过这一努力来拾取桌上可能剩余的硬币可能是想要尝试着瓦解持有者的无所作为。许多人得出了结论,并且我同意这一 次的进攻攻击者的动机是完全摧毁DAO而不是获得利益。虽然没有人知道真相,我建议大家自己去判断。

有趣的是,这次攻击发生在区块链之后由Emin Gün Sirer描述了,但公众之前并没有注意到。

 

第4步:运行split

 

我们费力的描述完这次攻击的所有无聊的饿技术方面后,让我们步入有趣的一部分,行动:执行恶意的split。在split之后进行交易的账户是:0xf35e2cc8e6523d683ed44870f5b7cc785051a77d

他们将资金送入的child DAO是0x304a554a310c7e546dfe434669c62820b7d83490。创建和发起提议的账户

 0xb656b2a9c3b2416437a811e07466ca712f5a5b5a(你可以在区块链的历史看到对创建的提议的调用)。

解构构造的参数创建了child DAO带领我们走向了管理者0xda4a4626d3e16e094de3225a751aab7128e96526。这个智能协议只是一个普通的多重签名钱包,它过去大多数的交易只是添加/去除拥有者和其他钱吧的管理任务。并没有什么有趣的。

Johannes Pfeffer 在媒体上有一个交易中绝佳的基于区块链的重建涉及到了child DAO.既然他已经做出了这么出色的工作,我就不花费太多的时间对这一区块链进行分析。我强烈建议对其感兴趣的人重这篇文章开始。

在这一系列的下一篇文章中,我们将会关注恶意的协议本身的代码(包括漏洞利用在递归攻击中的实际发行)。为了方便发布,我们还没有完成完整的分析。

 

第4a步:扩展split

 

这一步骤是跟新初始更新,并掩盖了攻击者是如何能将a-30x放大攻击(因为以太坊堆栈的大小限制在128以内)转变为一个几乎无限榨干的账户。

精明的读者在上面可能会注意到,即使在压倒性的堆栈和执行比需要的更多的恶意split之后,通过代码在splitDAO结束后黑客对他们的余额仍然是零输出:

16

所以攻击者要怎么绕过这个限制?由于DAO代币有转账的功能,攻击者就没必要这么做!他所需要做的就只是调用在堆栈顶端的的DAO的帮助转化功能,来自他的这一恶意功能:

17

将代币转账到一个代理账户,在splitDAO结束时原始账户将会是零输出(注意如果A将所有钱转账到B,A的账户在可通过splitDAO零输出 前就已经通过转账变为零输出了)。接着攻击者可以将代理账户的资金返回到原始账户并且重新开始整个过程。即使在splitDAO 的 totalSupply丢失了,鉴于 p.totalSupply[0]是用于计算支付,它是原始提议的一个性质并且只有有攻击发生之前才能具现化。 所以尽管在每一次迭代中DAO中的以太币减少了。攻击的强度可以保持不变。

在区块链上两个恶意协议调用withdrawRewardFor的迹象表明攻击者的代理账户也是一个可用于攻击的合同,与攻击者的原始账户简单进行交替。这一优化使得每一个进攻周期中节省了一次交易,但这看上去是没有必要的。

 

1.1是容易受攻击的吗?

 

因为withdrawRewardFor是易受攻击的,自然就会有人问DAO1.1经过更新功能后是不是任然容易受到同样的攻击。答案是:是的。
检验更新的功能(尤其是被标志的命令行):

18

注意在实际支付之前paidOut现在是如何更新的。所以这又是如何影响我们的漏洞利用? getRewardFor第二次被调用时,恶意的第二次调用splitDAO,这一命令行:

19

将会得到0.调用支出将会有 call _recipient.call.value(0)(),这是对这一功能的默认值,使得它等值得调用:

20

因为在攻击者在发送他的恶意split进行交易时支付了大量的燃气,递归攻击被允许能继续进行。
意识到他们在1.1之后需要6天才有1.2,设计一个保证安全的代码需要数年,这可能就是为什么DAO的傀儡主人想要放手了。

 

一个重要的结论

 

我认为1.1的敏感性对于这次攻击是很有趣的:即使withdrawRewardFor本身是不易受攻击的,即使没有 withdrawRewardFor的splitDAO也是不容易受攻击的,但结合起来就是致命的。这大概就是为什么这么多人这么多次的检测都忽略了这个 漏洞:检测员习惯一次检测一个功能,并假定调用安全子例程将会安全的如预期的操作。

就以太坊的例子来说,即使安全功能涉及到发送资金也会致使你的原始功能脆弱的可重新进入。不管他们是来自默认固体库的功能还是你自己在脑中形成的安全功能。在任何状态更新之后都应特别注意检查是不是以太坊代码都能确保任何功能运行值都能实现,否则这些状态值将会需要重入。

 

接下去会怎样?

 

我在这里不会涉及交叉的辩论或者是以太坊和DAO接下来将会怎样。这个话题已经在每一个社交平台上被预测的谁都不想谈了。

在我们一系列的公布,下一步是在测试网络上用DAO1.0的代码重建这个漏洞,并论证漏洞利用背后的代码和攻击原理。请注意如果有人在这些上击败了我,我保留限制在一个系列的长度的权利。

 

更多的信息

 

在本文中提供的信息只是提供一个广泛的概览和攻击的时间轴,同时也是分析开始的点。

如果你有区块链数据或分析、协议源代码或二进制代码与这个话题描述相关的话,请分享到我的邮箱 phil linuxcom. 我会很乐意添加到这篇文章中并致谢,并对这一事件进行全面的重新创建在最后的这24小时之内。


声明:此文出于传递更多信息之目的,并不意味着赞同其观点或证实其描述。本网站所提供的信息,只供参考之用。

点击阅读全文