跳至內容
0%

ERC-4337 UserOperation 打包漏洞

作者:Adam Egyed

最後更新:2023年3月21日2 分鐘閱讀

Additional Contributors: fangting@alchemy.com, @drortirosh, @Gooong, @taylorjdawson, @leekt, @livingrockrises

概述

2023 年 3 月 7 日,Alchemy 與開源開發者社群的其他成員,包括 @Gooong、@taylorjdawson、@leekt 和 @livingrockrises,發現了 ERC-4337 EntryPoint 合約與範例 VerifyingPaymaster 合約中的 calldata 解碼問題。

這些合約目前已部署到多條鏈上,並對 user operation 產生雜湊值。此實作方式會依簽署方法的不同而產生不一致的雜湊值,進而可能導致一些二階效應,例如同一個 UserOperation 產生不同雜湊值、或不同的 UserOperation 產生相同雜湊值(碰撞)。

討論由 @drortirosh 主持,並記錄於這個 Github issue中。

詳細分析

以下是受影響程式碼的分析,包含 EntryPoint Packing Vulnerability、VerifyingPaymaster Packing Vulnerability 的說明,以及各自的影響。

受影響的程式碼

問題所在的程式碼片段如下:

‍UserOperation.sol:61-75

VerifyingPaymaster.sol:35-49

作為參考,UserOperation 結構定義如下:

這兩段程式碼都使用組合語言(assembly)將一大段 calldata 複製到記憶體中,目的是擷取 user operation 中要進行雜湊運算的部分。

UserOperationLib 中的 pack 方法,其目的是擷取 user operation 從 sender 到 maxPriorityFeePerGas 的所有欄位,包括可變長度欄位(在 ABI 編碼中稱為動態欄位)initCode、callData 和 paymasterAndData。

VerifyingPaymaster 中的 pack 方法包含除 paymasterAndData 欄位以外的所有這些欄位,因為該欄位當時尚未定義。

為實作此功能,這兩個方法都使用了 Yul 為 calldata 中動態型別提供的便利欄位,名為 .offset。這指的是結構體 ABI 編碼中提供的值,其定義可參見此處的 Solidity 規範。(實際上它指的是 offset 之後的那個記憶體字,但這只是為了方便讀取而已)。

標準的 ABI-encoder 會依照欄位出現的順序,對動態欄位(在 ABI coder 中稱為其 tail)的值進行編碼。

考慮以下可能產生的 user operation calldata 編碼方式:

💡 注意: 為簡潔起見,此範例顯示的是所有動態欄位長度皆不超過一個 word 的 user operation。

💡‍ *‍*注意: 此處的記憶體位址空間僅限於 user operation 結構體本身。在實際的 calldata 中,由於方法選擇器(method selector)和參數 tuple 會佔用空間,其位置會有所不同。

在此範例中,依照 pnd.offset 對 user operation 進行打包,會得到以下這段 calldata 的「片段」:

這正好是我們想要的內容!

然而,使用 ABI 編碼引數的合約並不會驗證欄位定義的順序,甚至不會驗證 offset 是否有效。

使用 signature.offset 或 pnd.offset 會直接從 calldata 中讀取對應的「offset」值。

利用這一點作為邊界,就有可能建構出在 calldata 中有效、但具有異常雜湊特性的 user operation 表示方式。

接下來讓我們分別探討這如何影響 EntryPoint 與 VerifyingPaymaster。

EntryPoint packing vulnerability

要示範此漏洞,我們必須考慮一個與所提供的 SimpleAccount.sol 不同的錢包合約,因為該範例重複使用了 EntryPoint 中存在漏洞的程式碼。

當 EntryPoint 與錢包合約使用不同的雜湊方案,或錢包對非標準的 user operation 編碼進行簽署時,雜湊值不一致的問題就會產生實質影響。

這對 EntryPoint 帶來的風險是:單一 user operation 可以對應到多個「user op hash」,而同一個「user op hash」也可以對應到多個 user operation。

考慮這個帳戶,稱為 ExampleAccount,它有自己的 ExampleAccountFactory。此範例帳戶使用單一簽署者來驗證 user operation。

要授予執行 user operation 的權限,會針對 user operation 中除簽章本身以外的所有欄位產生一個雜湊值並加以簽署。

validateUserOp 方法的定義如下:

這是一個相對簡單的簽章驗證實作,因為它會檢查 userOp 的所有欄位,連同 entrypoint 位址與 chain id 一併檢查。

作為帳戶抽象化(account abstraction)目標之一,validateUserOp 方法可以包含任意邏輯(雖然會受到儲存存取的限制),因為此方法代表 user operation 得以發起的條件。

對於此範例帳戶,user operation 是由擁有者的簽章發起的。但更廣泛而言,user operation 可以來自任意條件:鏈上狀態、多重簽章,或應用程式特定的簽章——這正是帳戶抽象化的一項特性。

為示範此漏洞,讓我們建構惡意 calldata 傳入 EntryPoint.handleOps,使得 EntryPoint 所發出的 UserOperationEvent 帶有非預期的值。

在定義一個範例 UserOperation memory uo 結構之後,以下是我們如何建構 calldata:

rightPadBytes 是一個輔助函式,用於將 bytes 型別對齊到最接近的完整 word 長度。

其定義如下:

現在,當呼叫 handleOps 時,所發出的事件與 EntryPoint.getUserOpHash\(\) 的結果將會不同。

影響

惡意的 bundler,或呼叫 EntryPoint.handleOps 的非 bundler EOA,可以修改 calldata 中 UserOp 的表示方式,藉此改變在發出事件中的 UO hash。這可能破壞與所發出事件整合的鏈下系統,因為這些事件現在被揭露對於同一個 UO 並非確定性的。

此外,bundler 在讀取 EntryPoint 合約所發出的 userOpHashes 時,也必須處理非確定性的問題。要判斷 EntryPoint 所發出的 UserOperationEvent 是否對應到 bundler 本地 mempool 中的某個 user operation,僅比對雜湊值已不再足夠,因為傳入 handleOps 的 calldata 可被修改以改變雜湊值。

取而代之,bundler 必須查詢交易收據、取得傳送至 handleOps 的 calldata、解碼該 calldata,然後透過標準 ABI coder 重新編碼並呼叫 EntryPoint.getUserOpHash\(...\) 來取得「標準」雜湊值。這是判斷本地 mempool 中的 user operation 是否已被打包所必需的步驟。此外,由於對 EntryPoint.handleOps 的呼叫可能發生在其他合約呼叫內部,解碼工作可能會深入呼叫堆疊之中。

此雜湊不一致的問題也會影響 bundler RPC 方法的實作,因為 user op hash 在 eth\_getUserOperationByHash 和 eth\_getUserOperationReceipt 中被用於識別。

Bundler 將需要進行昂貴的搜尋、剖析與解碼傳入 EntryPoint.handleOps\(...\) 的 calldata,才能將事件中發出的雜湊值轉換為來自 EntryPoint.getUserOpHash\(...\) 的「標準」雜湊值。

💡 注意: 此漏洞與惡意 SCW 可重複使用 user operation 這件事是不同的問題。重複使用的 user operation,以及更廣泛而言所有的 user operation,都應該具有確定性的雜湊值。其他建構在 ERC-4337 之上的應用程式與服務,若此問題尚未解決,就必須自行實作對應的緩解措施。

由於 ERC-4337 整體上仍處於採用初期,很難描述此漏洞對更廣泛生態系統的潛在影響。影響範圍取決於 bundler、user operation 瀏覽器、索引器及其他鏈下服務的實作方式。

在最低程度上,這會造成令人困惑的使用者體驗,因為 user operation hash(類似於 transaction hash)可能在提交與納入區塊的時間點之間發生變化,因此某些錢包可能未考量到此差異,導致無法向使用者顯示正確的更新。

在中度風險的情況下,錢包可以被設計成刻意規避索引,方法是讓其所有的 user op hash 都相同(參見 @leekt 提供的範例)。

在高風險的情況下,監控 user operation 是否被納入區塊的鏈下服務,可能會錯過某個 user operation 已被納入的事實,進而嘗試重新傳送或以其他方式錯誤處理資料與金鑰。

概念驗證

完整的概念驗證請見此 repo。

VerifyingPaymaster packing vulnerability

VerifyingPaymaster 所帶來的風險是,user operation 在簽署時與鏈上納入時,內容可能有所不同。當兩個不同的 user operation 從 VerifyingPaymaster.getHash\(UserOperation userOp, uint48 validUntil, uint48 validAfter\) 得到相同的雜湊值時,就可能發生這種情況。

讓我們針對此函式建構 calldata,以示範這種情況如何發生:

Calldata 1

請注意,此編碼方式改變了動態欄位的順序,但其餘部分仍然有效——offset 指向正確的位置,長度也都有效。但由於 paymasterAndData 的 offset 是一個非預期的值,我們從 pack\(\) 得到的片段將會是以下內容:

可以看到 initCode 和 callData 都被排除在該片段之外!

現在讓我們建構第二筆輸入 calldata,這次惡意地同時修改這兩個欄位:

  • initCode 會從 1517c0de 變成 1517c0de02
  • callData 會從 ca11dada1 變成 ca11data02

這會得到以下結果:

Calldata 2

如果我們對這筆不同的 calldata 執行相同的 pack 操作,會得到與先前相同的片段!我們可以透過以下方式驗證兩者的雜湊值是相同的:

在 foundry 的測試環境中執行此程式碼,會發現這兩個 user op 的雜湊值相同:0x736f86d224bab46a95ae119947e172efa694379d9ac682d4ca780b7640a89b06

完整的 POC 請見此測試檔案。

影響

在此漏洞中,雜湊值可被修改為只涵蓋比預期更少的元素,使得 initCode、callData 以及可能的其他靜態欄位被排除在雜湊之外,因而在簽署與實際使用之間產生差異。這可能導致 paymaster 的贊助簽章被用於原本不打算允許的用途。

舉例來說,錢包合約的部署工廠(deployer factory)以及對錢包 execute 函式的呼叫都可能被替換。如果某個 paymaster 原本只想贊助其錢包的使用者,且僅在使用者鑄造特定 NFT 時才提供贊助,這些規則都有可能被繞過。

要繞過這些規則,發送者可以在取得簽章之後修改 userOp.initCode 和 userOp.callData。如此一來,paymaster 的原生代幣(ETH 或其他)就會被用於其原本無 gas NFT 鑄造意圖以外的其他用途。

以 ABI 編碼格式接收 user operation 進行簽署的鏈下簽署者,或是擁有合約整合以準備待簽署資料的簽署者,都容易受到此問題影響。這個影響範圍有限,因為本質上他們是在「自我攻擊」,但這對於營運 paymaster 服務仍構成風險。

針對此問題的防禦措施包括部署更新版本的 VerifyingPaymaster,或自行處理從使用者輸入進行 ABI 編碼的流程。

結論

在與生態系成員進行多次精彩的討論後,@drortirosh 合併了一個經過最佳化且易讀的修補程式,套用於 Entrypoint 合約,以解決此漏洞。重新部署後,Entrypoint 合約將不再出現上述行為。

此外,也有一項提案要在 Entrypoint 中對 nonce 支援進行抽象化,藉此進一步強化此系統。由於此問題對 paymaster 造成的風險有限,目前並未針對上游做出正式修補,paymaster 營運者可依其實作需求自行決定如何處理此問題。

我們要感謝 4337 社群,包括 @drortirosh、@Gooong、@taylorjdawson、@leekt 和 @livingrockrises,與我們一同解決此漏洞。

有任何問題或想討論的主題嗎?

歡迎透過 account-abstraction@alchemy.com 與我們聯繫。

Alchemy 電子報

搶先掌握最新發布消息

訂閱我們的電子報

取得 Alchemy 最新產品更新與資源

A
O
D
+
超過 80,000 名訂閱者

輸入您的電子郵件地址,即表示您同意接收我們的行銷通訊與產品更新。您了解 Alchemy 會依照我們的隱私權聲明處理所收到的資訊。您可以隨時取消訂閱。