跟随the DAO攻击者的尾迹

2016-06-24 10:21 来源:巴比特 阅读:5921
上文中我主要关注的是the DAO的withdrawRewardFor漏洞的递归发送,这也是最近今天媒体和研究员所关注的焦点。上文中,我描述了一种攻击的放大性,攻击者可以让攻击放大30倍,而且可以无限循环重复。

images

之前我的一篇博文说明了对the DAO 漏洞利用的原理和时间线,这篇文章是这篇文章的后续:http://hackingdistributed.com/2016/06/18/analysis-of-the-dao-exploit/ (http://www.8btc.com/thedao-expolit-analysis)

二次入侵

上文中我主要关注的是the DAO的withdrawRewardFor漏洞的递归发送,这也是最近今天媒体和研究员所关注的焦点。上文中,我描述了一种攻击的放大性,攻击者可以让攻击放大30倍,而且可以无限循环重复。

起初我认为实施细节不重要,而 Joey Krug 和Martin Köppelmann努力说明了这些细节的重要影响。就像Martin指出的那样,确实存在领个独立的漏洞利用, 它们促成了第三种更强的漏洞。

  1. 在splitDAO上,来自withdrawRewardFor函数的递归攻击,可以让你划走你应当访问的DAO代币的30倍数量,但是只能实施一次。

  2. 可以在进入,但,它是非递归攻击,在splitDAO上,通过withdrawRewardFor转账,每次分裂,你可以得到双倍于你的代币。

  3. (1)和(2)的组合,允许你划转30倍的你应该得到的代币,你想实施几次就可以进入几次。

攻击者实施了第三种,我们之前已经讨论过了,但是,我们来看看第二个,我们创建一个合约,这合约的第一和第三漏洞已经修复,我们来看看 withdrawRewardFor 完美的一个合约,它没有循环进入漏洞,我们回忆一下之前我发的帖子,withdrawRewardFor的脆弱让DAO 1.1中splitDAO有可重入性,即使它可能已被修复,然而在该函数第一行依然存在问题:

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

reward = rewardAccount.balance < reward ? rewardAccount.balance : reward;

paidOut[_account] += reward;
if (!rewardAccount.payOut(_account, reward))
throw;

return true;
}

记住这样的事情,即使在奖励账户(rewards account)中没有余额,支付给用户的钱为0,payOut 的调用依然运行,依然在接受者的合约中调用任意代码:

function payOut(address _recipient, uint _amount) returns (bool) {
if (msg.sender != owner || msg.value > 0 || (payOwnerOnly && _recipient != owner))
throw;
if (_recipient.call.value(_amount)()) { // vulnerable

}

getRewardFor的第一次运行,它将支付合法的奖励。第二次支付变为0,第三次依然为0,第四次依旧……它还会允许用户返回splitDAO中,30倍的放大他们应该得到的。

要注意这,即使是DAO 1.1被修复的withdrawRewardFor ,依然存在重入性弱点。这种重入性不会威胁到奖励账户中的余额,因为你第二次(第三次、第四次)运行这个函数时它支付的是0.然而它依然容易被重入性攻击。

如果我们在DAO 1.2中修复这个函数,我们把withdrawRewardFor 函数写的完美,它们就不会受到重入的威胁,这个函数应该是这样的:

function withdrawRewardFor(address _account) noEther internal returns (bool _success) {
if ((balanceOf(_account) * rewardAccount.accumulatedInput()) / totalSupply <= paidOut[_account])
return false; // Stop all splitDAO calls from failing, orthogonal change

仅仅修改了涂有红色那一处,简单的把 < 改为 ≤ 。为什么?第二次运行这个函数,paidOut[_account] 的值就会和左边表达式的值相等。在重入被触发前已经设置好代码:

uint reward =
(balanceOf(_account) * rewardAccount.accumulatedInput()) / totalSupply – paidOut[_account];
paidOut[_account] += reward;

所以这是DAO 1.1的withdrawFor 应该的样子,它没有重入的弱点,这是经典反模型的最好实践。这代码简单且易读。

这样一来,像我在上篇文章中描述的splitDAO的无限放大的弱点就不存在了,攻击者就不会这样做:因为在余额为零的时候不会再运行payOut 调用,重复运行这个函数没有一点意义。

然而就像 Joey Krug所指出的那样,这不影响利用withdrawRewardFunction用同样的漏洞利用来榨干一个DAO1.2。关键是可以在新的案例上用withdrawRewardFor调用一次任意代码,一次就足够了。

函数它本身放大了这个漏洞:当你的奖励在取出的时候调用DAO的转账函数,你的余额在代币划到你的子DAO中后变为零,你的代币会存在一个新的账 户。代码详解可以在上一篇文章中找到,主要涉及到splitDAO 和转账函数,它们都被非原子级别(non-atomically)的修改了相同的余额数组。

所以,任何非原子级别(non-atomically )影响一个过程的逻辑都会造成一个漏洞,如果这个过程被一个调用中断去执行任意合约,这就会被攻击。所以除了“写不被重入的函数”,还要记着:不要调用外部的合约,否则你就无法预测你的程序流或状态。

这个漏洞利用有一些很有意思的地方,首先,它很慢,本质上来说,你在你每次调动代币的时候把它们加倍,然而这不影响攻击的成功:这种速度已经足够了,足够的意思是,没人能够阻止你。

第二,在我们已经修复过的 DAO 1.1代码中,消除了重入性的弱点,因为它需要奖励账户中有余额。如果这个rewardAccount.accumulatedInput() 返回值为0,用户就永远不会被支付,我们修改函数将一直返回false,永远不会执行潜在的恶意代码。在公布的the DAO1.1代码中,依然存在withdrawRewardFor漏洞,奖励账户依然不需要有余额就可执行withdrawRewardFor。

在实际应用中,这些都没关系,奖励账户可以被打进资金,有意的或者无意的。这意味着攻击者可以每次转移它的奖励的一部分,可以使用withdrawRewardFor函数再次入侵,甚至修复过的the DAO1.2都无法避免。

Solidity的缺陷

如果我所列的漏洞中第一条没有存在的话,那么社区的人肯定会遗漏第二条。第一条是反模型的实例,我们会在这个点上尽力去防范它,第二条是更加精妙的反模型实例:没有原始函数的重入调用,仅仅是原始合约的重入。

总结如下:

  1. 如果你用Solidity 的调用结构调用外部合约,或者在你修改过的合约中有任何外部调用的函数,那么当你的合约运行外部调用的时候,你就不能预算出这之后的状态了。

  2. 在当前DAO的漏洞被利用情况下,上面这条不是一个已知的编程实践。你可以看一下Solidity的调用文 档:https://github.com/ethereum/wiki/wiki/Solidity-Features#generic-call- method 它忽视了很多安全漏洞,当开发者使用这种结构的时候,使它们进入一种错误的安全感知。

  3. 就像前面所建议的,不要使用Solidity的调用架构在你的合约中调用外部合约代码,如果你能避免这么做,那就永远不要这样做。如果你做不到,你应当明白,你将在那时失去对这个程序流的所有保证。

上面的这些对于我来说不仅仅意味着the DAO合约自身的缺陷和漏洞,它是:从技术来讲,这些是以太坊虚拟机本身设计的功能,然而Solidity在合约中造成了安全漏洞,这些不仅被社区忽视了,还被这个语言的设计者忽视了。

在我看来这个漏洞50%的责任应该落在Solidity语言设计者的头上,这样可能会让这类案例的纠正措施得到改善。我不同意把责任单独归于合约代码写的糟糕,即使是合约代码完全的按照这个语言文档来编写,同样会有这样的漏洞。

一个奇怪的帮助请求

在写本文时,我无意中发现了一个关于DAO奖励账户有意思的地方:为什么攻击者要收集奖励,谁向奖励账户打钱的?为什么withdrawRewardFor 函数在DAO遭受攻击的阶段一直在运行?

让我们看下the DAO的奖励地址,The DAO的会计资料来自Slockit ,地址是: 0xd2e16a20dd7b1ae54fb0312209784478d069c7b0(https://github.com/slockit /DAO/wiki/Understanding-the-DAO-accounting),看一下它的交易记录:https: //etherscan.io/txsInternal?a=0xd2e16a20dd7b1ae54fb0312209784478d069c7b0&&zero=true&p=220 你可以看到有200页的0.00000002的以太币转进 0xf835a0247b0063c04ef22006ebe57c5f11977cc4 和0xc0ee9db1a9e07ca63e4ff0d5fb6f86bf68d47b89 ,这是攻击者的恶意合约。

但是,看下这些币是从哪里来的,最后一页的账户交易记录:一个单独的打钱交易记录,从地址 0xe76563eb8413ede9b4a1a3c1f3280a95c4b60a33发送了0.001以太坊,时间是在the DAO创建末期。

相同的地址后来还投资了DAO,并持有DigixDAO 代币。

我的理论是:他可能是想要投资买进the DAO众筹的新人,试着想程序化的买入,然而失败了。但怎么会弄错主DAO的地址呢?它们在daohub.org的主页上啊,而奖励地址出现在DAO维基 的偏僻角落。所以为什么打钱进这个地址?只是有人想和the DAO架构开玩笑看看他们的反应?

更有意思的是搜索这个账户,可以在一个以太币论坛上找到帖子,他寻求 Javascript API发送以太币的帮助。这个账户发完这个帖子之后再没有登录过,这是他们唯一的帖子https://forum.ethereum.org /discussion/7109/web3-eth-gettransaction-returned-null 。

这可能会成为区块链历史上有意思的一瞥,我们永远也不知道答案。如果你是这个账户的持有者,我很想直到你当时的想法。

攻击者并不孤单

看一下我之前的帖子,我之所以会这么快的分析出攻击者的路径,是因为之前我自己也在构建这样的攻击。

一周前我都到一份Emin Gün Sirer发来的邮件:

2016年6月11号星期六17:42:37 Emin G Sirer <> 撰写
嘿伙计,
我知道怎么倒空the DAO了。

我花了几个小时来分析the DAO的这个漏洞。在周末,我花了几个小时来排除了V1.1中的几个潜在漏洞,Sirer replied回复我:

2016年6月12日 13:34:09 Emin G Sirer <> 撰写
很奇怪,我是最早发现这个潜在问题的人之一……
我仍然认为splitDAO 存在漏洞。balances[]不被清零直到这个函数被调用,这违反了取款模式。所以我认为它可能通过移动rewardTokens多次分裂DAO。这发生在DAO.sol的640行到666行。我错了吗?
不管怎样,这确实是一个静态可分析的漏洞。我会问一下Andrew Miller是否静态分析过这个问题。

所以我花了几个小时分析了splitDAO,我推算出没有触发递归发送这个漏洞的可能性。我简单的回答这不太可能。我引用了时间问题,不管是从 splitDAO内部还是创建TokenProxy中,都不可能触发递归发送。我证明了一些可能的漏洞利用实施方案,然后就去忙自己的分内工作了。

Sirer小组中有其他人关注这个问题,当我们往深处探究时,这挺有意思的,攻击者并不是唯一知道怎样触发这个漏洞的人。

DAO的这次事故是不可避免的。我在这写这系列文章只是因为好奇而没有 去试图证明它,它给了我一点对公开审查力量的希望,我们对此并不太惊讶。

短暂的离开

谢谢阅读我这个系列的帖子,我希望它们有点启发。
由于工作原因,我到周末这段时间不关注theDAO了,你可能会在我的推特上看到些相关消息https://twitter.com/phildaian

还有很多我们要做的:我们仍然需要重构一个关于这次攻击的完整区块链图,包括黑客的代币最终到了哪里,和这么做的原因。我们需要搜索公共测试网络,看看黑客是否现在那里进行过攻击测试。我们需要认真的反编译和重组他的原始Solidity代码,以便记录在案和分析。

我们应该总结经验教训,学习使用最佳实施方法(一点想法:慢慢放大这些合约,停止在Solidity内使用调用结构)。作为一个社区,我们要制定一个前进路线,决定是否要分叉。

最后我想分享的是当前人们对智能合约技术的兴趣比任何时候都强,这是一个大好机会,来展示一个比上周更强、更有指导性、更有秩序和原则的社区。

我在这里谢谢这个黑客,花了不知多少小时的时间来开发和测试这个漏洞。向你致敬,这次你击败了我们。我想下次你不会这么幸运了。


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

点击阅读全文