兩大智慧合約簽名驗證漏洞分析
可重入(Reentrancy)或整數溢位漏洞,是大多數開發人員知道或者至少聽說過的,關於智慧合約當中容易出現的安全問題。另一方面,在考慮智慧合約的安全性時,你可能不會立即想到針對密碼簽名實現的攻擊方式。它們通常是與網路協議相關聯的。例如,簽名重放攻擊(signature replay attacks),一個惡意使用者可竊聽包含有效簽名的協議序列,並針對目標進行重放攻擊,以期獲得益處。本文將解釋智慧合約處理DAPP生成簽名時可能存在的兩種型別的漏洞。我們將通過Diligence團隊在今年早些時候完成的現例項子審計結果進行分析。此外,我們將討論如何設計智慧合約,以避免這類漏洞的出現。
協議層
簽名是以太坊網路中的基礎,傳送至網路的每筆交易都必須具有有效的簽名。下圖顯示了這種交易的一個例子。除了交易標準屬性,例如 from、to、gas、value 或input在全域性名稱空間中可用,並且經常出現在智慧合約程式碼中,欄位v,r以及s共同組成了交易簽名。
以太坊網路確保只有具有有效簽名的交易可被納入新的區塊當中。這為交易提供了以下安全屬性:
身份驗證:以太坊節點使用簽名來驗證交易簽名者是否具有與公共地址相關聯的私鑰。開發者因此可以信任這個msg.sender是真實的;
完整性:交易在簽名後不會發生更改,否則簽名就是無效的;
不可否認性:交易是由from欄位中公共地址對應的私鑰簽名的,這是不可否認的,並且擁有私鑰的簽名方已經進行了任何狀態更改。
合約層
協議層並不是簽名發揮作用的唯一場地。簽名也越來越多地被用於智慧合約本身。隨著gas價格的上漲,而擴容解決方案仍在程序當中,則避免鏈上(on-chain)交易便凸顯出了越來越多的重要性。當談到鏈外的交易時,簽名也是非常有用的,EIP-191以及EIP-712,都是有關於如何處理智慧合約中籤名資料的通證標準。而後者旨在改善鏈外訊息簽名的可用性。那麼,為什麼它是有用的,以及它是如何節省鏈上交易的?
讓我們來檢視一個簡單的例子。愛麗絲為鮑伯建立了一個命題,她將其編碼成了一條訊息。她還用自己的私鑰建立了訊息的簽名,並通過協商好的通道傳送給鮑伯。鮑伯可以驗證愛麗絲是否簽署了該訊息,如果鮑伯認為該命題是合適的,那麼他可以建立新的交易,將他自己的訊息、愛麗絲的訊息及簽名共同納入到一個智慧合約當中。通過資料,這個智慧合約可以證實:
鮑伯已簽署了自己的資訊(或者在這種情況下,交易會是更具體的)。而網路保證了身份驗證、完整性以及不可否認性。
整個過程只需要一筆鏈上交易,其可提供明顯更好的使用者體驗,同時可節省gas。需要注意的是,智慧合約需要確保愛麗絲髮送給鮑伯的訊息,能夠保持所有三個安全屬性的完整性。
讓我們分析現實世界中存在的兩種簽名驗證漏洞,並探討如何修復它們。
缺少針對簽名重放攻擊的保護(SWC-121)
第一個例子,是由Consensys的Diligence部門在審計去中心化新聞應用Civil時發現的一個漏洞例子,與此案例相關的系統的第一部分,被Civil稱之為Newsroom(新聞編輯室),而內容編輯可以把自己的文章釋出到這個Newsroom,他們還可以為自己的記憶體創作進行加密簽名,以此證明內容實際上是由他們創造的。pushRevision()函式對現有內容進行更新或修訂。引數內容雜湊、內容URI、時間戳以及簽名,為內容建立新的修訂。之後,verifiyRevisionSignature()函式會呼叫提議修訂,以及最初建立第一個簽名修訂的內容作者。根據設計,新修訂的簽署者,只能是建立初始簽名內容版本的作者。
verifiyRevisionSignature() 函式會根據DApp生成的內容雜湊,以及Newsroom合約的地址,建立一個已簽名的訊息雜湊。然後,呼叫recover()函式(來自OpenZeppelin 的ECRecovery庫)。隨後,呼叫ecrecover()函式,並驗證作者是否真正簽署了訊息。已討論過的兩個函式程式碼是沒有問題的,因為只有最初建立內容的作者才能為它建立新的版本,所以實際上它們不存在什麼安全問題。
問題在於,合約是不會跟蹤內容雜湊的,因此,已提交的一個內容雜湊及其使用者簽名,實際是有可能被提交多次的。而惡意的內容作者就可以利用這個漏洞,從其他作者那裡獲取有效的簽名和內容雜湊,並在他們不知情的情況下為他們建立新的有效修訂。
Civil 已通過跟蹤這些內容雜湊,並拒絕已是先前修訂部分的雜湊,來解決這個問題。
缺乏正確的簽名驗證(SWC-122)
在上一次審計去中心化協議0x的過程當中,Diligence發現了這種漏洞型別的一個例項。以下解釋,是這次審計報告當中3-2節內容中描述的問題總結。0x協議具有不同簽名型別的各種簽名驗證器,包括Web3以及EIP712。另一個存在的驗證器稱為SignatureType.Caller,如果order.makerAddress等於msg.sender(order.makerAddress是建立order的使用者),則允許order有效。如果設定了SignatureType.Caller,則沒有實際簽名驗證是由交易合約執行的。現在還不清楚為什麼這會導致漏洞,因為已經證實msg.sender以及order的建立者是相同的,至少從理論上看是這樣的。
除了交易合約之外,0x系統還有另一部分稱為Forwarder的合約,有了這個合約,使用者可以簡單地傳送以太幣,以及他們想要填寫的 order,而這個Forwarder合約會在同一筆交易中執行所有的order;
想要用以太幣交易其他通證的使用者,可以向其他使用者傳送order,而Forwarder合約將代表他們進行交易。這個交易合約會驗證每個order,以確保order簽名的有效性,並確保其他使用者已實際簽署了order。讓我們再次檢視上面的圖,並重新評估以下假設:如果order.makerAddress等於msg.sender,則我們不需要在這個交易合約當中進行適當的簽名驗證,因為傳送交易的使用者也是order的建立者。如果使用者直接向交易合約傳送order,則該假設成立。但是,如果我們通過Forwarder合約傳送這個order,將order.makerAddress設定為 Forwarder合約的地址,並使用SignatureType.Caller簽名驗證器呢?
在交易執行處理結算個別order的過程中,Forwarder合約會呼叫這個交易合約。這個交易合約會驗證這個order.makerAddress中的地址,就是msg.sender,在這種情況下,可以將其設定為Forwarder地址。由於合約在交易雙方之間起到了中介作用,所以order.takerAddress通常被設定為Forwarder地址。因此,惡意使用者可以使用Forwarder處理order,其中合約會與其本身進行交易,因為它既是接受者又是製造者。這是因為以下的原因:
在 Forwarder當中,沒有邏輯可以阻止合約成為一個 order的製造者;
用於transferFrom((address _from, address _to, uint256 _value) )的ERC20規範,不會阻止使用者進行“空傳輸”。而 _from和_to可以是相同的地址;
這個交易合約允許基於以下事實來處理order:如果使用者實際上已經簽署了order,則msg.sender沒有傳送order。
在交易合約解決了order之後,這個Forwarder合約將得到完全相同的 balance,並且Forwarder合約將takerAmount轉移給自己,而把makerAmount轉移給一個惡意使用者,而惡意使用者可以使用這個場景,來建立“惡意order”,以便用1 Wei(以太幣最低單位)的價格從 Forwarder合約中換取到所有的ZRX通證;綜上所述,假設訊息的傳送者也是其建立者,而不去驗證其簽名,這可能是不安全的,尤其是在通過代理轉發交易的情況下。在合約處理訊息簽名的任何時候,都需要執行正確的簽名恢復及驗證。0x通過刪除了 SignatureType.Caller簽名驗證器修復了這個問題。
總結
鏈外訊息簽名的方式,對於節省 gas以及改善使用者體驗方面,的確是一個好的方法。但從安全性的角度來看,這無疑增加了複雜性,並使得智慧合約在處理已簽名訊息的情況下成為了一個更具挑戰性的任務。如果你對針對基於簽名的攻擊,或其它智慧合約漏洞示例感興趣,你可以檢視SWC-registry在Github的內容,裡面擁有大量易受攻擊的合約示例,此外還有關於智慧合約弱點分類(SWC)計劃的更多資訊,我們一直在與社會各界合作。如果你想了解更多關於SWC的資訊,或者有其他好的想法,那麼歡迎你在ethereum/EIPs以及Ethereum Magicians裡面參加關於EIP-1470的討論;
也歡迎大家隨時瀏覽我們的Discord,告訴我們你希望通過安全分析工具檢測到哪些漏洞… 但,請不要說出全部的內容。
作者:Gerhard Wagner
編譯:灑脫喜
稿源(譯):巴位元資訊(http://www.8btc.com/signature-verification-bugs-in-ethereum)