05 月 10 日凌晨,MakerDAO 公開了新版合約。Zeppelin 和 PeckShield 也各自獨立完成了對其新合約的審計,確定新版本修復了該漏洞。本文 Peckshield 將公佈該漏洞的細節與詳細攻擊手法。

原文標題:《MakerDAO 治理合約升級背後的安全風波》

北京時間 2019 年 05 月 07 日,區塊鏈安全公司 Zeppelin 對以太坊上的 DeFi 明星項目 MakerDAO 發出安全預警,宣稱其治理合約存在安全漏洞,希望已鎖倉參與投票的用戶儘快解鎖 MKR 提並出。MakerDAO 的開發者 Maker 公司亦確認了漏洞存在,並上線了新的治理合約,並宣稱漏洞已修復。

該安全威脅曝出後,PeckShield 全程追蹤了 MKR 代幣的轉移情況,並多次向社區發出預警,呼籲 MKR 代幣持有者立即轉移舊合約的 MKR 代幣。截止目前,絕大多數的 MKR 代幣已經完成了轉移,舊治理合約中尚有 2,463 個 MKR 代幣(價值約 128 萬美元)待轉移。

05 月 07 日當天,經 PeckShield 獨立研究發現,確認了該漏洞的存在(我們命名爲 itchy DAO),具體而言:由於該治理合約實現的投票機制(vote(bytes32))存在某種缺陷,允許投票給尚不存在的 slate (但包含有正在投票的提案)。等用戶投票後,攻擊者可以惡意調用 free() 退出,達到減掉有效提案的合法票數,並同時鎖死投票人的 MKR 代幣。

次日 05 月 08 日,PeckShield 緊急和 Maker 公司同步了漏洞細節,05 月 10 日凌晨,MakerDAO 公開了新版合約。Zeppelin 和 PeckShield 也各自獨立完成了對其新合約的審計,確定新版本修復了該漏洞。

PeckShield 深度還原,鎖死 MakerDAO 中 MKR 代幣的漏洞是啥?

在此我們公佈漏洞細節與攻擊手法,也希望有引用此第三方庫合約的其它 DApp 能儘快修復。

細節

在 MakerDAO 的設計裏,用戶是可以通過投票來參與其治理機制,詳情可參照 DAO 的 FAQ。

以下是關於 itchy DAO 的細節,用戶可以通過 lock / free 來將手上的 MKR 鎖定並投票或是取消投票:

PeckShield 深度還原,鎖死 MakerDAO 中 MKR 代幣的漏洞是啥?

在 lock 鎖定 MKR 之後,可以對一個或多個提案 (address 數組) 進行投票:

PeckShield 深度還原,鎖死 MakerDAO 中 MKR 代幣的漏洞是啥?

注意到這裏有兩個 vote 函數,兩者的傳參不一樣(address 數組與 byte32),而 vote(address[ ] yays) 最終亦會調用 vote(bytes32 slate),其大致邏輯如下圖所示:

PeckShield 深度還原,鎖死 MakerDAO 中 MKR 代幣的漏洞是啥?

簡單來說,兩個 vote 殊途同歸,最後調用 addWeight 將鎖住的票投入對應提案:

PeckShield 深度還原,鎖死 MakerDAO 中 MKR 代幣的漏洞是啥?

可惜的是,由於合約設計上失誤,讓攻擊者有機會透過一系列動作,來惡意操控投票結果,甚致讓鎖定的 MKR 無法取出。

這裏我們假設有一個從未投過票的黑客打算開始攻擊:

1、調用 lock() 鎖倉 MKR,此時 deposits[msg.sender] 會存入鎖住的額度。

2、此時黑客可以線下預先算好要攻擊的提案並預先計算好哈希值,拿來做爲步驟 3 的傳參,因爲 slate 其實只是 address 數組的 sha3。

這裏要注意挑選的攻擊目標組合必須還不存在於 slates[ ] 中 (否則攻擊便會失敗),黑客亦可以自己提出一個新提案來加入組合計算,
如此便可以確定這個組合必定不存在。

PeckShield 深度還原,鎖死 MakerDAO 中 MKR 代幣的漏洞是啥?

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)

PeckShield 深度還原,鎖死 MakerDAO 中 MKR 代幣的漏洞是啥?

4、調用 etch() 將目標提案數組傳入。注意 etch() 與兩個 vote() 函數都是 public,所以外部可以隨意調用。這時 slates[hash] 就會存入對應的提案數組。

5、調用 free() 解除鎖倉。這時會分成以下兩步 :

  • deposits[msg.sender] = sub(deposits[msg.sender], wad),解鎖黑客在 1. 的鎖倉
  • subWeight(wad, votes[msg.sender])

從對應提案中扣掉黑客的票數,然而從頭到尾其實攻擊者都沒有真正爲它們投過票。

從上面的分析我們瞭解,黑客能透過這種攻擊造成以下可能影響:

一、惡意操控投票結果
二、因爲黑客預先扣掉部份票數,導致真正的投票者有可能無法解除鎖倉

時間軸

時間 事件
2019.05.07 PeckShield 複查並確認了漏洞存在
2019.05.08 PeckShield 與 Maker 基金會討論並確認了漏洞細節
2019.05.09 Maker 基金會公佈新版 DSChief 合約源碼,PeckShield 披露了漏洞相關細則