古爾丹之手:TheDAO的攻擊手法補全

按:原文發(fā)表于2016年6月19日。

b0c80a8b97424eada7546565d227c60e.jpg

原諒我標題黨了...

這次TheDAO被攻擊的事件中,構(gòu)造攻擊的手法已經(jīng)不是秘密。攻擊者的攻擊的思路很好理解,無非是利用以太坊開發(fā)者社區(qū)最近在討論一個非常容易掉進去的坑,詳細解釋可見朱立剖析TheDAO攻擊手法一文。

但是容易想到,僅僅靠這一點還不足以制造出這個大事件: 以太坊的每個block都有一個隱性的上限, gasLimit,類似比特幣中的區(qū)塊容量。在當前的設置(大約400多萬gas)下,攻擊者可以觸發(fā)的遞歸調(diào)用次數(shù)是非常受限的。而在一次攻擊完成,也就是遞歸調(diào)用結(jié)束之后,他的TheDAO Token balance會歸0,也就無法再發(fā)動下一次攻擊。(調(diào)用必須正常結(jié)束,否則整個事務會回滾,前功盡棄)換句話說,一個split proposal只能被利用一次。想要像這次事件一樣大規(guī)模的盜走以太幣,該怎么辦呢?有了,我準備大量的備用proposal,發(fā)起一次又一次的沖鋒不就行了?

然而攻擊者微微一笑,輕松打臉...

7ee77164bd194508a7f304efdf9fdb9e.png

4672b302d1b64b11812715e942f79d61.png

33aae7c7408c4f58854b05827e741e57.png

截圖為攻擊者地址產(chǎn)生的消息,注意其中的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;

靜靜的觀察一會兒我們可以發(fā)現(xiàn),最后銷毀token的那一行,與方法入口時檢查msg.sender的余額不為0的代碼(onlyTokenHolders modifier),可以構(gòu)成一個經(jīng)典的check-and-set的陷阱,check & set并不是原子性的。翻譯成普通話就是:如果我在遞歸快要結(jié)束的時候,把我的余額先轉(zhuǎn)給別人,會發(fā)生什么呢?銷毀token的一行依然會忠實的執(zhí)行balance = 0的操作,只不過是把本來就是0的記錄再復寫一次0罷了... 真正的token已經(jīng)被轉(zhuǎn)移到了別處。這樣,在一次攻擊完成之后,就可以把別處保管的token再轉(zhuǎn)移回來,重新實施攻擊了。

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轉(zhuǎn)給任意其他人。在每次轉(zhuǎn)賬的時候都會產(chǎn)生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轉(zhuǎn)賬人的地址,topics[2]是TheDAO token收款人的地址??梢钥吹?,由于不停的遞歸,一直在觸發(fā)將攻擊者持有的token歸零的Transfer事件(歸零也就是把持有者的TheDAO token都發(fā)往0地址。注意Transfer event的觸發(fā)是在withdrawRewardFor之前,此時還沒有真正執(zhí)行歸零的操作),但是最后一次Transfer, 卻是把0xdfd3f956d86e775fe個token轉(zhuǎn)移到了0xc0ee9db1a9e07ca63e4ff0d5fb6f86bf68d47b89這個賬戶中!在這個Transfer之后,遞歸中積累的那些等待歸人的歸零操作才開始一遍又一遍自娛自樂的玩耍...

現(xiàn)在我們可以形成一次完整的攻擊了:

  1. 準備工作,提交proposal,準備好作為receiver的惡意合約,等待
  2. 觸發(fā)splitDAO, 遞歸造成TheDAO不停流血
  3. 遞歸在快要觸到block gasLimit的時候結(jié)束,進行收尾工作,把自己的余額轉(zhuǎn)到準備好的另外一個地址,這個地址也受到攻擊者的控制。
  4. 一次攻擊成功結(jié)束。此時再將余額從剛才的隱藏地址轉(zhuǎn)回來。
  5. 回到2,進行下一次攻擊

由于可以反復的利用同一個proposal進行攻擊,這個漏洞的嚴重程度被放大了許多倍,使得攻擊者可以在短時間內(nèi)盜走大量的以太幣。

一些想法

網(wǎng)上最早對于這個攻擊思路的描述似乎是來自這篇6月9號的博客,Solidity的作者Christian接著在6月10號也在以太坊官方博客上發(fā)表了文章提醒大家注意這個問題。6月11號,MakerDAO發(fā)現(xiàn)自己的合約中存在這樣的問題, 所幸處置及時,并未造成什么損失。6月12日,有用戶在TheDAO的論壇上表示TheDAO也存在同樣的問題,TheDAO團隊研究過后作出結(jié)論不會有立即的影響,因為TheDAO還沒有任何的reward產(chǎn)生,而問題代碼是在獲取reward的方法之中。隨后Slock更新了自己的官方博客公布了這個結(jié)論。遺憾的是,攻擊者發(fā)現(xiàn)獲取reward的方法(withdrawRewardFor)也在另一個方法(splitDAO)中被調(diào)用?,F(xiàn)在我們還知道,攻擊者還找到了一個完美的收尾方法,使得攻擊能夠在一個proposal上重復。

這兩天關于TheDAO的討論鋪天蓋地。在技術(shù)層面,我并不認為Slock團隊和進行安全審查的專家有太多的責任。事實并不是如所說的,合約的代碼很爛,漏洞很低級,等等等等。TheDAO的合約經(jīng)過了許多人的審查,包括我自己也在第一時間研究過,代碼很規(guī)范,也很小心,例如一個這么長的合約,內(nèi)部沒有用到過任何的循環(huán),完全規(guī)避了已知的一些漏洞。大多數(shù)攻擊手法,在解釋之后都很好理解,但是在實施之前是很難想到的。何況一次成功的攻擊,還需要組合多個手法構(gòu)造一個完整的方案,再采取各種方式隱藏自己。

從這次攻擊中我們可以學到很多東西。對于合約將要承載的資產(chǎn)容量,我們需要有準確的預計,未來也許可以根據(jù)容量規(guī)劃來進行相應程度的合約審計和試運行。對于call調(diào)用用到的地址,也應該包括在審計之內(nèi),要求其提供合約源代碼。這些東西將成為社區(qū)的寶貴經(jīng)驗。

我們面對的是一個前所未有的世界。在我看來,我們正在進行的探索不亞于火星冒險。遺憾的是,我見到的許多討論沒有意識到這一點。他們既沒有意識到正在進行的事情有著回收火箭一樣高的失敗概率,也沒有意識到回收火箭失敗并不代表冒險者的無能。

附:

攻擊路徑上涉及的地址及合約:

  • 發(fā)起攻擊的地址: 0xf35e2cc8e6523d683ed44870f5b7cc785051a77d
  • proxy: 0xf835a0247b0063c04ef22006ebe57c5f11977cc4
  • TheDAO rewardAccount: 0xd2e16a20dd7b1ae54fb0312209784478d069c7b
  • proxy2: 0x56bcc40e5e76c658fad956ee32e4250bf97468a1
  • 隱藏地址: 0xc0ee9db1a9e07ca63e4ff0d5fb6f86bf68d47b89
  • 轉(zhuǎn)移回token交易的發(fā)起地址: 0x969837498944ae1dc0dcac2d0c65634c88729b2d
  • TheDAO: 0xbb9bc244d798123fde783fcc1c72d3bb8c189413
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容