Back to Blog

致命整合:鉤子中因危險交互引發的漏洞

Code Auditing
November 20, 2023
9 min read

正如我們在上一篇文章中所強調的,Awesome Uniswap v4 Hooks 儲存庫[1]中超過 30% 的專案存在漏洞。值得注意的是,我們這裡所指的漏洞是針對 Uniswap v4 互動的特定漏洞。因此,在本文中,我們將從以下兩個角度審視安全的 hook 互動邏輯:

  • 存取控制缺陷
  • 不當的輸入驗證

針對每個類別,我們將從漏洞分析開始,並透過提供相應的概念驗證(PoC)來展示其潛在的利用方式。隨後,我們將討論潛在的緩解策略。

存取控制缺陷

一般而言,與 Uniswap v4 的 hook 相關的互動可以根據 hook 是否作為鎖定者(在 PoolManager 中獲取鎖定以執行池操作)進行分類。兩種主要的互動場景需要適當的存取控制:

  • Hook-PoolManager 互動:這涉及官方回呼函式與 PoolManager 之間的互動。回呼函式包括八個池操作回呼(即 initializemodifyPositionswapdonate)以及鎖定回呼(即 lockAcquired)。
  • Hook-Internal 互動:這涉及在 hook 合約內部(作為鎖定者)發生的互動。

Hook-PoolManager 互動相對簡單。在此情境下,hook 純粹作為一個 hook,接受八個池操作回呼。hook 中的邏輯不會影響相關的池,這意味著 hook 與池之間沒有資金流動。回呼函式提供的參數用於修改必要的儲存狀態或作為重要的函式參數。關鍵考量在於回呼參數是否可能被操縱

Hook-Internal 互動則更為複雜。在實際應用中,許多 hook 原型所做的不僅僅是充當純粹的 hook。部分開發者允許 hook 為其使用者提供資金管理功能。這些功能可能並未在 hook 合約中實作,但我們仍可將其統稱為 hook。在這些情況下,hook 接受使用者資金並執行流動性管理或交換等池操作。這意味著合約必須從 PoolManager 獲取鎖定,使 hook 成為鎖定者。 Uniswap 基金會已考慮到這種情況,並將一個函式整合到其 hook 模板 中。具體而言,BaseHook 模板提供了 lockAcquired 函式作為鎖定回呼,如下所示:

    function lockAcquired(bytes calldata data) external virtual poolManagerOnly
returns (bytes memory) {
        (bool success, bytes memory returnData) = address(this).call(data);
        if (success) return returnData;
        if (returnData.length == 0) revert LockFailure();
        // if the call failed, bubble up the reason
        /// @solidity memory-safe-assembly
        assembly {
            revert(add(returnData, 32), mload(returnData))
        }
    }

為了執行自訂邏輯,lockAcquired 接受 data 位元組並使用該 data 對自身進行低階呼叫。data 取決於 hook 的業務邏輯,可能被使用者操縱,這可能導致由 lockAcquired 觸發的 Hook-Internal 互動產生安全問題。請注意,hook 設計非常靈活,我們無法在此情況下涵蓋所有可能的場景。我們在此的主要關注點是 hook 獲取鎖定及其後續的內部互動。深入探討其他潛在的業務邏輯會使討論過於複雜。

在這兩種場景中,首要任務是解決任何可能導致被利用的存取控制缺陷,因為這些函式具有明確的互動實體。在後續的子章節中,我們將依次審視每個場景,並討論確保更安全互動邏輯所需的存取控制。

漏洞分析

存取控制對許多專案而言是高效且直接的安全解決方案。如果一個函式設計為由特定實體呼叫,則應納入存取控制。最廣為人知的存取控制範例是 OpenZeppelin 函式庫的 Ownable 合約,它要求特權函式只能由合約擁有者呼叫。很明顯,我們上面討論的兩種場景是這種控制的適當應用情境。

Hook-PoolManager 互動:為了與 PoolManager 安全互動,hook 應對這些回呼函式執行必要的存取控制。具體而言,這些回呼應只能由 PoolManager 呼叫,而不能由其他任何帳戶呼叫。若未能建立此類控制,這些敏感介面可能會暴露於惡意行為者的潛在利用之下。

除了八個池操作回呼之外,鎖定回呼(即 lockAcquired)在從 PoolManager 獲取鎖定後執行自訂邏輯,同樣需要解決此問題。

Hook-Internal 互動:參與 hook 內部互動的函式也設計為由特定呼叫者呼叫。如我們之前所述,此場景包含兩個階段。首先,鎖定者的 lockAcquired 函式由 PoolManager 呼叫,這表明該函式應要求 msg.sender 為 PoolManager。其次,hook 相應地分派函式呼叫。根據 BaseHook 的設計,這是透過對 hook 自身的低階呼叫實現的。這表明那些函式必須定義為 external,並限制呼叫者必須為 hook 的地址。

Awesome Uniswap v4 Hooks 儲存庫列出的其中一個範例為例,即 Stop Loss Order[2]:

直接整合到 Uniswap V4 池中,止損訂單在鏈上發布並透過 afterSwap() hook 執行。無需外部機器人或行為者來保證執行。

讓我們來檢視其 afterSwap 回呼函式:

圖 1:Stop Loss Order 的 afterSwap 函式
圖 1:Stop Loss Order 的 afterSwap 函式

顯然,上述函式被設計用於執行敏感操作。然而,由於存取控制缺陷,惡意行為者可能透過操縱參數(例如 keyparams)來利用它,導致意外行為。例如,afterSwap 回呼可能在假設交換已在 PoolManager 中發生的前提下運作。隨後,它可能啟動操作以記錄重要的狀態資訊,例如當前價格或收取的交換費用。然而,如果 afterSwap 未嚴格限制其呼叫來源僅限於 PoolManager,惡意行為者可能會偽造 params 參數,導致記錄的狀態出現偏差。

漏洞利用與 PoC

為了簡單起見,我們將使用一個基本的 PoC 來說明此存取控制問題。一般而言,hook 的 beforeInitialize 接受一個 PoolKey 類型的參數,該參數的 hooks 欄位中必須包含此 hook 地址(因為 PoolManager 將使用此欄位來確定要呼叫的 hook 地址)。

截圖提供了一個 PoC,展示了對具有存取控制缺陷的 hook 的利用,如 DiamondHookPoC [3] 所示。 在 beforeInitialize 回呼函式沒有存取限制的情況下,惡意行為者可以向此函式提供任意的 poolKey。hook 未驗證此 poolKey 的 hook 是否與當前 hook 地址相符。

圖 2:PoolKey.hooks 可以被設定為零地址 beforeInitialize_poolKey_no_hooks_validation.webp

雖然值得注意的是,在此場景中的漏洞利用可能不會造成 hook 的財務損失,但它仍然深刻地突顯了 hook 的狀態如何可以透過未受保護的回呼函式被操縱。

如何緩解

為了確保 Hook-PoolManager 互動的安全性,hook 回呼和鎖定回呼都應將其存取限制為僅限 PoolManager

幸運的是,Uniswap v4 透過其 v4-periphery 儲存庫[4]中的 BaseHook 提供了最佳實踐。 BaseHook 提供了 poolManagerOnly 修飾符,以嚴格限制來自 PoolManager 的呼叫:

    /// @dev Only the pool manager may call this function
    modifier poolManagerOnly() {
        if (msg.sender != address(poolManager)) revert NotPoolManager();
        _;
    }

此修飾符可有效地用於對敏感的 hook 和鎖定回呼執行適當的存取控制。

另一方面,Hook-Internal 互動的存在要求透過 BaseHook 所規定的 lockAcquired 回呼呼叫的任何重要狀態變更函式都不應被任意呼叫。

為了滿足此要求,BaseHook 提供了 selfOnly 修飾符。此修飾符將所宣告函式的存取限制為 hook 本身,禁止外部合約直接呼叫這些敏感函式以達到惡意目的。

    /// @dev Only this address may call this function
    modifier selfOnly() {
        if (msg.sender != address(this)) revert NotSelf();
        _;
    }

總而言之,透過繼承 BaseHook,自訂 hook 可以利用這些內建的存取控制修飾符和回呼來執行適當的存取控制。

不當的輸入驗證

v4-periphery[4] 中的 BaseHook 為更安全的互動邏輯提供了解決方案,hook 開發者可以加以利用。然而,我們持續觀察到不當使用的情況,這為現有 hook 中的攻擊向量開闢了新的可能性。

預設情況下,hook 允許任何池透過 PoolManager 中的 initialize 函式進行註冊。然而,如果 hook 未能驗證正在註冊的池中的底層資產,惡意使用者可能會註冊包含偽造代幣的池,使他們能夠透過代幣的 transfer 函式重入 hook。

這個漏洞非常隱蔽,因為 hook 本身可能不會執行惡意邏輯。然而,當 hook 呼叫 PoolManager 時,PoolManager 與惡意池的底層資產之間的互動可能透過 PoolManager 中的 take 函式將控制流移交給攻擊者。

    /// @inheritdoc IPoolManager
    function take(Currency currency, address to, uint256 amount) external override 
noDelegateCall onlyByLocker {
        _accountDelta(currency, amount.toInt128());
        reservesOf[currency] -= amount;
        currency.transfer(to, amount);
    }

本質上,此漏洞源於對 hook 使用者計劃互動的已註冊池的驗證不當。我們將透過一個具體範例深入探討此漏洞,並討論潛在的緩解策略。

漏洞分析

Take Profits Hook[5] 是 Awesome Uniswap v4 Hooks 列出的一個 hook:

在此範例中,我們構建了一個允許使用者設置「止盈」倉位的 hook。例如,在 ETH/DAI 池中,如果當前 1 ETH = 1500 DAI,您可以設置一個止盈訂單「當 1 ETH = 2000 DAI 時賣出我所有的 ETH」,該訂單將自動執行。

讓我們來看看這個 hook 中的 _handleSwap 函式。此函式在獲取鎖定後執行交換以完成止盈訂單。

圖 3:Take Profits Hook 的 _handleSwap 函式[5]
圖 3:Take Profits Hook 的 _handleSwap 函式[5]

您可能注意到此函式未受任何存取控制修飾符保護。然而,第 250 行有效地限制了存取,使得此函式只能在從 PoolManager 獲取鎖定後才能被呼叫。否則,poolManager.swap 將會失敗,因為操作者不會是最新的鎖定者。換言之,只要已註冊的池經過驗證,_handleSwap 必須按特定順序呼叫。不幸的是,hook 未實作此類驗證。

由於此缺陷實作,hook 容易受到重入攻擊。此漏洞可能允許攻擊者使用使用者存入的資金強制執行任意交換。

漏洞利用與 PoC

具體而言,攻擊可以透過以下步驟發動:

  1. 攻擊者使用偽造代幣註冊一個惡意池,並將 Take Profits Hook 指定為池的 hook。
  2. 攻擊者透過 hook 在惡意池中設置一個止盈訂單。
  3. 攻擊者在惡意池中執行交換,觸發 afterSwap 回呼中的 fillOrder 以完成攻擊者的止盈訂單。
  4. hook 呼叫 PoolManagerlock 函式以請求鎖定,並在 lockAcquired 回呼中呼叫 _handleSwap 函式。
  5. _handleSwap 函式中,代幣的轉移觸發了偽造代幣合約中的惡意邏輯,該邏輯重入 _handleSwap 函式。這是可能的,因為 _handleSwap 是一個沒有任何存取限制的外部函式。由於鎖定已被獲取,攻擊者可以強制 hook 在任何池上執行任意交換,只要 hook 持有足夠的底層資產。攻擊者隨後可以對這些交換進行三明治攻擊,以犧牲其他使用者為代價獲取利潤。

以下詳細圖示說明了攻擊的流程。

圖 4:攻擊流程
圖 4:攻擊流程

如前所述,hook 本身並未呼叫惡意邏輯。唯一的錯誤在於 hook 未阻止不受信任的代幣池在 PoolManager 合約中進行註冊。間接地,偽造代幣合約中的惡意邏輯是透過代幣轉移操作被呼叫的,這也是一種不受信任的外部呼叫。

如何緩解

有三種可行的方法來緩解由不當輸入驗證導致的潛在攻擊:

  • 適當的存取控制。透過利用 BaseHook 的構建模組,hook 可以嚴格管理函式的存取權限。這可以防止任意帳戶呼叫敏感函式。

  • 重入鎖。在上述攻擊場景中,此方法無疑可以防止惡意代幣邏輯重入敏感函式。然而,在某些情況下,hook 設計要求 hook 本身是可重入的。 具體而言,當 hook 需要執行某些池操作時,它應允許 PoolManager 重入其回呼以完成這些操作。重入鎖可能會破壞此預期功能。

  • 白名單方法。這需要特權管理員在 hook 中將已核准的池列入白名單。管理員確保白名單中的池不會引入潛在風險。然而,其限制在於 hook 使用者只能透過 hook 在有限數量的管理員核准池上執行操作。 雖然白名單方法提高了安全性,但它嚴重限制了 hook 的功能。

找到一個在安全性和可用性之間取得完美平衡的解決方案對 hook 而言是具有挑戰性的。雖然我們討論了幾種緩解方法,但開發者需要在其 hook 設計中深思熟慮地考慮各種取捨。目標應是在保留預期功能的同時,盡可能地降低潛在風險。此外,我們的討論僅涵蓋可能存在於 Uniswap v4 特定功能互動中的漏洞。實際應用無疑會更加全面。請務必確保您理解合約中的每一行程式碼,並保持安全(SAFU)!

結論

在本文中,我們探討了 hook 互動邏輯中出現的漏洞,具體集中在兩個場景:存取控制缺陷和不當的輸入驗證。我們提供了詳細的漏洞分析,說明了潛在的漏洞利用方式及其 PoC,並討論了潛在的緩解策略。我們相信這些見解能夠有助於 hook 的安全開發和使用,並為未來的漏洞檢測工作提供指引。

參考資料

[1] Awesome Uniswap v4 Hooks

[2] Stop Loss Order

[3] DiamondHookPoC

[4] v4-periphery

[5] Take Profits

閱讀本系列的其他文章

Sign up for the latest updates

Best Security Auditor for Web3

Validate design, code, and business logic before launch. Aligned with the highest industry security standards.

BlockSec Audit