次日05月08日,PeckShield 緊急和 Maker 公司同步了漏洞細節,05月10日凌晨,MakerDAO 公開了新版合約。Zeppelin 和 PeckShield 也各自獨立完成了對其新合約的審計,確定新版本修復了該漏洞。
在此我們公佈漏洞細節與攻擊手法,也希望有引用此第三方庫合約的其它 DApp 能儘快修復。
細節
在 MakerDAO 的設計裡,使用者是可以透過投票來參與其治理機制,詳情可參照 DAO 的 FAQ。
以下是關於 itchy DAO 的細節,使用者可以透過 lock / free 來將手上的 MKR 鎖定並投票或是取消投票:
在 lock 鎖定 MKR 之後,可以對一個或多個提案 (address 陣列) 進行投票:
注意到這裡有兩個 vote 函式,兩者的傳參不一樣 (address 陣列與 byte32),
而 vote(address[] yays) 最終亦會呼叫 vote(bytes32 slate),其大致邏輯如下圖所示:
簡單來說,兩個 vote 殊途同歸,最後呼叫 addWeight 將鎖住的票投入對應提案:
可惜的是,由於合約設計上失誤,讓攻擊者有機會透過一系列動作,來惡意操控投票結果,甚致讓鎖定的 MKR 無法取出。
這裡我們假設有一個從未投過票的駭客打算開始攻擊:
1. 呼叫 lock() 鎖倉 MKR,此時 deposits[msg.sender] 會存入鎖住的額度。
2. 此時駭客可以線下預先算好要攻擊的提案並預先計算好雜湊值,拿來做為步驟 3 的傳參,因為 slate 其實只是 address 陣列的 sha3。
這裡要注意挑選的攻擊目標組合必須還不存在於 slates[] 中 (否則攻擊便會失敗),駭客亦可以自己提出一個新提案來加入組合計算,如此便可以確定這個組合必定不存在。
3. 呼叫 vote(bytes32 slate),因為 slate 其實只是 address 陣列的 sha3,駭客可以線下預先算好要攻擊的提案後傳入。
這時因為 votes[msg.sender] 還未賦值,所以 subWeight() 會直接返回。接下來駭客傳入的 sha3(slate) 會存入 votes[msg.sender],之後呼叫 addWeight()。從上方的程式碼我們可以看到,addWeight() 是透過 slates[slate] 取得提案陣列,此時 slates[slate] 獲取到的一樣是未賦值的初始陣列,所以 for 迴圈不會執行(由於 yays.length = 0)
4. 呼叫 etch() 將目標提案陣列傳入。注意 etch() 與兩個 vote() 函式都是 public,所以外部可以隨意呼叫。這時 slates[hash] 就會存入對應的提案陣列。
5. 呼叫 free() 解除鎖倉。這時會分成以下兩步:
· deposits[msg.sender] = sub(deposits[msg.sender], wad)
解鎖駭客在 1. 的鎖倉
· subWeight(wad, votes[msg.sender])
從對應提案中扣掉駭客的票數,然而從頭到尾其實攻擊者都沒有真正為它們投過票
從上面的分析我們瞭解,駭客能透過這種攻擊造成以下可能影響:
一、惡意操控投票結果
二、因為駭客預先扣掉部份票數,導致真正的投票者有可能無法解除鎖倉
時間軸