古尔丹之手:TheDAO的攻击手法补全

按:原文发表于2016年6月19日。

b0c80a8b97424eada7546565d227c60e.jpg

原谅我标题党了...

这次TheDAO被攻击的事件中,构造攻击的手法已经不是秘密。攻击者的攻击的思路很好理解,无非是利用以太坊开发者社区最近在讨论一个非常容易掉进去的坑,详细解释可见朱立剖析TheDAO攻击手法一文。

但是容易想到,仅仅靠这一点还不足以制造出这个大事件: 以太坊的每个block都有一个隐性的上限, gasLimit,类似比特币中的区块容量。在当前的设置(大约400多万gas)下,攻击者可以触发的递归调用次数是非常受限的。而在一次攻击完成,也就是递归调用结束之后,他的TheDAO Token balance会归0,也就无法再发动下一次攻击。(调用必须正常结束,否则整个事务会回滚,前功尽弃)换句话说,一个split proposal只能被利用一次。想要像这次事件一样大规模的盗走以太币,该怎么办呢?有了,我准备大量的备用proposal,发起一次又一次的冲锋不就行了?

然而攻击者微微一笑,轻松打脸...

7ee77164bd194508a7f304efdf9fdb9e.png

4672b302d1b64b11812715e942f79d61.png

33aae7c7408c4f58854b05827e741e57.png

截图为攻击者地址产生的消息,注意其中的block number一直在变化,也就是说,攻击者只用了一个proposal/token holder address, 就成功的进行了多次攻击。-_-!!

那他是怎么做到的呢?

思路

还是回到历史上有名的splitDAO方法:

// Burn DAO Tokens
Transfer(msg.sender, 0, balances[msg.sender]);
withdrawRewardFor(msg.sender); // be nice, and get his rewards
totalSupply -= balances[msg.sender];
balances[msg.sender] = 0; // <---------- 看我看我
paidOut[msg.sender] = 0;
return true;

静静的观察一会儿我们可以发现,最后销毁token的那一行,与方法入口时检查msg.sender的余额不为0的代码(onlyTokenHolders modifier),可以构成一个经典的check-and-set的陷阱,check & set并不是原子性的。翻译成普通话就是:如果我在递归快要结束的时候,把我的余额先转给别人,会发生什么呢?销毁token的一行依然会忠实的执行balance = 0的操作,只不过是把本来就是0的记录再复写一次0罢了... 真正的token已经被转移到了别处。这样,在一次攻击完成之后,就可以把别处保管的token再转移回来,重新实施攻击了。

TheDAO中包含了一个标准的token合约,transfer的代码如下:

function transfer(address _to, uint256 _amount) noEther returns (bool success) {
    if (balances[msg.sender] >= _amount && _amount > 0) {
        balances[msg.sender] -= _amount;
        balances[_to] += _amount;
        Transfer(msg.sender, _to, _amount); // <----------- 看我看我
        return true;
    } else {
        return false;
    }
}

transfer的作用是让TheDAO token的持有者可以将自己的token转给任意其他人。在每次转账的时候都会产生Transfer的事件,我们可以从这里入手进行验证。

验证

有了想法就可以求证。打开geth console, 通过filter把所有和攻击者地址0xf835a0247b0063c04ef22006ebe57c5f11977cc4相关的Transfer event都找出来:

// address是TheDAO的地址
// topics[0]是Transfer event的id
// topics[1]是攻击者的proxy合约地址
> eth.filter({fromBlock: '0x19f0a0', address: '0xbb9bc244d798123fde783fcc1c72d3bb8c189413', topics: ['0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef', '0x000000000000000000000000f835a0247b0063c04ef22006ebe57c5f11977cc4']})

...

 {
    address: "0xbb9bc244d798123fde783fcc1c72d3bb8c189413",
    blockHash: "0x5965c954a9f9c60ade7bd81d1dc39e695f2be26239169315d74f1240c364baaa",
    blockNumber: 1720245,
    data: "0x00000000000000000000000000000000000000000000000dfd3f956d86e775fe",
    logIndex: 55,
    topics: ["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", "0x000000000000000000000000f835a0247b0063c04ef22006ebe57c5f11977cc4", "0x0000000000000000000000000000000000000000000000000000000000000000"],
    transactionHash: "0xa348da60799bff3ca804b3e49c96edebea44c5728a97f64bec3e21056d42f6e3",
    transactionIndex: 0
}, {
    address: "0xbb9bc244d798123fde783fcc1c72d3bb8c189413",
    blockHash: "0x5965c954a9f9c60ade7bd81d1dc39e695f2be26239169315d74f1240c364baaa",
    blockNumber: 1720245,
    data: "0x00000000000000000000000000000000000000000000000dfd3f956d86e775fe",
    logIndex: 57,
    topics: ["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", "0x000000000000000000000000f835a0247b0063c04ef22006ebe57c5f11977cc4", "0x0000000000000000000000000000000000000000000000000000000000000000"], // 这里的收款人地址为0
    transactionHash: "0xa348da60799bff3ca804b3e49c96edebea44c5728a97f64bec3e21056d42f6e3",
    transactionIndex: 0
}, {
    address: "0xbb9bc244d798123fde783fcc1c72d3bb8c189413",
    blockHash: "0x5965c954a9f9c60ade7bd81d1dc39e695f2be26239169315d74f1240c364baaa",
    blockNumber: 1720245,
    data: "0x00000000000000000000000000000000000000000000000dfd3f956d86e775fe",
    logIndex: 58,
    topics: ["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", "0x000000000000000000000000f835a0247b0063c04ef22006ebe57c5f11977cc4", "0x000000000000000000000000c0ee9db1a9e07ca63e4ff0d5fb6f86bf68d47b89"], // 这里的收款人地址不是0
    transactionHash: "0xa348da60799bff3ca804b3e49c96edebea44c5728a97f64bec3e21056d42f6e3",
    transactionIndex: 0
}
]

这里贴出了最后一部分Transfer事件,其中topics[0]代表了Transfer这个名字, topics[1]是TheDAO token转账人的地址,topics[2]是TheDAO token收款人的地址。可以看到,由于不停的递归,一直在触发将攻击者持有的token归零的Transfer事件(归零也就是把持有者的TheDAO token都发往0地址。注意Transfer event的触发是在withdrawRewardFor之前,此时还没有真正执行归零的操作),但是最后一次Transfer, 却是把0xdfd3f956d86e775fe个token转移到了0xc0ee9db1a9e07ca63e4ff0d5fb6f86bf68d47b89这个账户中!在这个Transfer之后,递归中积累的那些等待归人的归零操作才开始一遍又一遍自娱自乐的玩耍...

现在我们可以形成一次完整的攻击了:

  1. 准备工作,提交proposal,准备好作为receiver的恶意合约,等待
  2. 触发splitDAO, 递归造成TheDAO不停流血
  3. 递归在快要触到block gasLimit的时候结束,进行收尾工作,把自己的余额转到准备好的另外一个地址,这个地址也受到攻击者的控制。
  4. 一次攻击成功结束。此时再将余额从刚才的隐藏地址转回来。
  5. 回到2,进行下一次攻击

由于可以反复的利用同一个proposal进行攻击,这个漏洞的严重程度被放大了许多倍,使得攻击者可以在短时间内盗走大量的以太币。

一些想法

网上最早对于这个攻击思路的描述似乎是来自这篇6月9号的博客,Solidity的作者Christian接着在6月10号也在以太坊官方博客上发表了文章提醒大家注意这个问题。6月11号,MakerDAO发现自己的合约中存在这样的问题, 所幸处置及时,并未造成什么损失。6月12日,有用户在TheDAO的论坛上表示TheDAO也存在同样的问题,TheDAO团队研究过后作出结论不会有立即的影响,因为TheDAO还没有任何的reward产生,而问题代码是在获取reward的方法之中。随后Slock更新了自己的官方博客公布了这个结论。遗憾的是,攻击者发现获取reward的方法(withdrawRewardFor)也在另一个方法(splitDAO)中被调用。现在我们还知道,攻击者还找到了一个完美的收尾方法,使得攻击能够在一个proposal上重复。

这两天关于TheDAO的讨论铺天盖地。在技术层面,我并不认为Slock团队和进行安全审查的专家有太多的责任。事实并不是如所说的,合约的代码很烂,漏洞很低级,等等等等。TheDAO的合约经过了许多人的审查,包括我自己也在第一时间研究过,代码很规范,也很小心,例如一个这么长的合约,内部没有用到过任何的循环,完全规避了已知的一些漏洞。大多数攻击手法,在解释之后都很好理解,但是在实施之前是很难想到的。何况一次成功的攻击,还需要组合多个手法构造一个完整的方案,再采取各种方式隐藏自己。

从这次攻击中我们可以学到很多东西。对于合约将要承载的资产容量,我们需要有准确的预计,未来也许可以根据容量规划来进行相应程度的合约审计和试运行。对于call调用用到的地址,也应该包括在审计之内,要求其提供合约源代码。这些东西将成为社区的宝贵经验。

我们面对的是一个前所未有的世界。在我看来,我们正在进行的探索不亚于火星冒险。遗憾的是,我见到的许多讨论没有意识到这一点。他们既没有意识到正在进行的事情有着回收火箭一样高的失败概率,也没有意识到回收火箭失败并不代表冒险者的无能。

附:

攻击路径上涉及的地址及合约:

  • 发起攻击的地址: 0xf35e2cc8e6523d683ed44870f5b7cc785051a77d
  • proxy: 0xf835a0247b0063c04ef22006ebe57c5f11977cc4
  • TheDAO rewardAccount: 0xd2e16a20dd7b1ae54fb0312209784478d069c7b
  • proxy2: 0x56bcc40e5e76c658fad956ee32e4250bf97468a1
  • 隐藏地址: 0xc0ee9db1a9e07ca63e4ff0d5fb6f86bf68d47b89
  • 转移回token交易的发起地址: 0x969837498944ae1dc0dcac2d0c65634c88729b2d
  • TheDAO: 0xbb9bc244d798123fde783fcc1c72d3bb8c189413
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,911评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,014评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 142,129评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,283评论 1 264
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,159评论 4 357
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,161评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,565评论 3 382
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,251评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,531评论 1 292
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,619评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,383评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,255评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,624评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,916评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,199评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,553评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,756评论 2 335

推荐阅读更多精彩内容