致命的整合:钩子(Hooks)中因风险交互而存在的漏洞

在本文中,我们探讨了在钩子交互逻辑中出现的漏洞,特别关注了两种情况:有缺陷的访问控制和不当的输入验证。

致命的整合:钩子(Hooks)中因风险交互而存在的漏洞

正如我们在上一篇文章中所强调的,Awesome Uniswap v4 Hooks 仓库[1] 中超过 30% 的项目存在漏洞。值得注意的是,我们这里提到的漏洞是Uniswap v4交互特有的。因此,在本文中,我们将从以下两个角度来剖析安全的钩子交互逻辑:

  • 错误的访问控制
  • 不当的输入验证

对于每个类别,我们将首先分析漏洞,并通过提供相应的概念验证(PoC)来演示其潜在的利用方法。之后,我们将讨论潜在的缓解策略。

错误的访问控制

通常,与Uniswap v4钩子相关的交互可以根据钩子是否充当锁定者,在PoolManager中获取锁以在池中执行操作来进行分类。两种主要的交互场景需要适当的访问控制:

  • 钩子-PoolManager 交互:这涉及官方回调函数与PoolManager之间的交互。回调函数包括八个池操作回调(即initializemodifyPositionswapdonate)以及锁定回调(即lockAcquired)。
  • 钩子内部 交互:这指的是在钩子合约(充当锁定者)内部发生的交互。

钩子-PoolManager 交互相对简单。在这里,钩子纯粹充当钩子,接收八个池操作回调。钩子中的逻辑不影响相关池,这意味着钩子和池之间没有资金流。回调函数提供的参数用于修改必要的存储或作为重要的函数参数。关键的考虑因素是回调参数是否可以被操纵

钩子内部 交互要复杂一些。实际上,许多钩子原型所做的不仅仅是充当纯粹的钩子。一些开发者允许钩子为其用户提供资金管理功能。这些功能可能未在钩子合约中实现,但在此上下文中我们仍可将它们统称为钩子。在这种情况下,钩子接受用户资金并执行池操作,如流动性管理或兑换。这意味着合约必须从PoolManager获取锁,将钩子转变为锁定者。 Uniswap基金会已经考虑了这种情况,并在其钩子模板中集成了一个函数。具体来说,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取决于钩子的业务逻辑,并且可以由用户操纵,这可能由于lockAcquired触发的钩子内部交互而导致安全问题。请注意,钩子设计非常灵活,我们无法涵盖这种情况下的所有可能场景。我们在这里主要关注钩子获取锁及其后续的内部交互。深入研究其他潜在的业务逻辑会使情况过于复杂,不适合本次讨论。

在这两种情况下,鉴于这些函数具有明确的交互实体,优先处理可能导致利用的任何错误的访问控制。在接下来的小节中,我们将依次检查每种场景,并讨论确保更安全的交互逻辑所必需的访问控制。

漏洞分析

访问控制是许多项目非常高效且直接的安全解决方案。如果一个函数被设计为由特定实体调用,它应该包含访问控制。最著名的访问控制示例是OpenZeppelin库的Ownable合约,它要求特权函数只能由合约所有者调用。显然,我们上面讨论的两种场景是这种控制的合适案例。

钩子-PoolManager 交互:为了与PoolManager进行安全交互,钩子应该对这些回调函数强制执行必要的访问控制。具体来说,这些回调应该只能由PoolManager调用,而不能由任何其他账户调用。未能建立此类控制可能会使这些敏感接口暴露给恶意攻击者利用。

除了八个池操作回调之外,锁定回调(即lockAcquired),它在从PoolManager获取锁后执行自定义逻辑,也需要解决这个问题。

钩子内部 交互:钩子内部交互涉及的函数也被设计为由特定调用者调用。如前所述,这种情况包含两个阶段。首先,锁定者的lockAcquired函数由PoolManager调用,这表明该函数应要求msg.senderPoolManager。其次,钩子相应地分派函数调用。基于BaseHook的设计,它通过低级调用钩子自身来实现。这表明这些函数必须定义为external,并限制调用者必须是钩子的地址。

Awesome Uniswap v4 Hooks仓库列出的一个示例,即Stop Loss Order为例[2]:

止损订单直接集成到Uniswap V4池中,在链上发布并通过afterSwap()钩子执行。不需要外部机器人或参与者来保证执行。

让我们看一下其afterSwap回调函数:

图1:止损订单的afterSwap函数[2]

显然,上述函数被设计用于执行敏感操作。然而,由于错误的访问控制,它可能被恶意攻击者通过操纵参数(例如,keyparams)来利用,导致意外行为。例如,afterSwap回调可能假设交易已在PoolManager中完成。之后,它可以执行记录关键状态信息的操作,例如当前价格或收集的交易费用。但是,如果afterSwap不严格限制其只能由PoolManager调用,恶意攻击者可能会伪造params参数,导致记录状态失真。

攻击与PoC

为简单起见,我们将使用一个基本的PoC来说明这个访问控制问题。通常,钩子的beforeInitialize接受一个PoolKey类型的参数,该参数必须在其hooks字段中包含此钩子地址(因为PoolManager将使用此字段来确定要调用的钩子地址)。

截图提供了一个PoC,演示了具有错误访问控制的钩子的利用,如DiamondHookPoC [3]中所见。 在beforeInitialize回调函数上没有访问限制的情况下,恶意攻击者可以向该函数提供任意的poolKey。钩子不验证此poolKey的钩子是否匹配当前钩子地址。

图2:PoolKey.hooks可以设置为零地址

虽然需要注意的是,此场景中的利用可能不会对钩子造成经济损失,但它仍然极大地突显了如何通过未受保护的回调函数来操纵钩子的状态。

如何缓解

为了确保钩子-PoolManager交互的安全,钩子回调和锁定回调都应将其可访问性严格限制给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();
        _;
    }

此修饰符可有效用于对敏感钩子和锁定回调强制执行适当的访问控制。

另一方面,钩子内部交互的存在要求通过lockAcquired回调(如BaseHook所指定)调用的任何重要的状态更改函数不应被任意调用。

为了满足此要求,BaseHook提供了selfOnly修饰符。此修饰符将声明的函数的访问权限限制为钩子本身,从而阻止外部合约出于恶意目的直接调用这些敏感函数。

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

总之,通过继承BaseHook,自定义钩子可以利用这些内置的访问控制修饰符和回调来强制执行适当的访问控制。

不当的输入验证

v4-periphery[4]中的BaseHook为更安全的交互逻辑提供了一个解决方案,钩子开发者可以利用这一点。然而,我们仍然观察到不当使用的情况,为现有钩子中的攻击向量打开了新的可能性。

默认情况下,钩子允许任何池通过PoolManager中的initialize函数进行注册。然而,如果钩子未能验证注册池中的底层资产,恶意用户就可以注册一个包含假冒代币的池,从而使他们能够通过代币的transfer函数重新进入钩子。

这种漏洞很微妙,因为钩子本身可能不会执行恶意逻辑。然而,当钩子调用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);
    }

本质上,漏洞源于对钩子用户计划交互的已注册池的不当验证。我们将通过一个具体的例子来深入探讨这个漏洞,并讨论潜在的缓解策略。

漏洞分析

Take Profits Hook[5]是Awesome Uniswap v4 Hooks列出的一个钩子:

在这个例子中,我们构建了一个允许用户设置“止盈”头寸的钩子。例如,在一个ETH/DAI池中,如果当前1 ETH = 1500 DAI,你可以设置一个止盈订单,例如“当1 ETH = 2000 DAI时卖出我所有的ETH”,该订单将自动执行。

让我们看一下这个钩子中的_handleSwap函数。该函数在获得锁后执行交易以填补止盈订单。

图3:止盈钩子的_handleSwap函数[5]

您可能会注意到,此函数没有受到任何访问控制修饰符的保护。然而,第250行有效地限制了访问,使得此函数只能在从PoolManager获得锁后调用。否则,poolManager.swap将会失败,因为操作者不是最新的锁定者。换句话说,_handleSwap必须按特定顺序调用,前提是已注册的池经过验证。不幸的是,钩子没有实现这种验证。

由于这种错误的实现,钩子容易受到重入攻击。此漏洞可能允许攻击者使用用户存入的资金强制执行任意交易。

攻击与PoC

具体来说,攻击可以按以下步骤进行:

  1. 攻击者使用假冒代币注册一个恶意池,并将止盈钩子指定为该池的钩子。
  2. 攻击者通过钩子在恶意池中下达一个止盈订单。
  3. 攻击者在恶意池中执行一次交易,触发afterSwap回调中的fillOrder来填补攻击者的止盈订单。
  4. 钩子调用PoolManagerlock函数请求锁,并在lockAcquired回调中调用_handleSwap函数。
  5. _handleSwap函数中,代币的转移触发了假冒代币合约中的恶意逻辑,该逻辑重新进入_handleSwap函数。这是因为_handleSwap是一个没有访问限制的外部函数。由于锁已经被获得,攻击者可以强制钩子在任何池上执行任意交易,只要钩子持有足够的底层资产。然后,攻击者可以夹带交易,以牺牲其他用户的利益来获利。

下图详细说明了攻击流程。

图4:攻击流程

如前所述,钩子本身并不执行恶意逻辑。唯一的错误是钩子没有阻止不受信任的代币池在PoolManager合约中注册。间接而言,假冒代币合约中的恶意逻辑通过代币转移操作被调用,这也是一种不受信任的外部调用。

如何缓解

有三种可行的方法可以缓解由于不当输入验证而引起的潜在攻击:

  • 适当的访问控制。通过利用BaseHook的构建块,钩子可以严格管理函数的可访问性。这可以防止任意账户调用敏感函数。

  • 重入锁。在上述攻击场景中,这种方法无疑可以防止恶意代币逻辑重新进入敏感函数。然而,在某些情况下,钩子设计要求钩子本身可重入。 具体来说,当钩子需要执行某些池操作时,它应该允许PoolManager重入其回调以完成这些操作。重入锁可能会破坏这种预期的功能。

  • 白名单方法。这将需要一个特权管理员在钩子中将批准的池列入白名单。管理员确保被列入白名单的池不会引入潜在风险。然而,限制是钩子用户只能通过钩子在有限数量的管理员批准的池上执行操作。 虽然白名单方法提高了安全性,但它严重限制了钩子的功能。

在平衡钩子的安全性和可用性方面,很难找到一个完美的解决方案。虽然我们讨论了几种缓解方法,但开发人员需要仔细考虑其钩子设计中的权衡。目标应该是尽可能地缓解潜在风险,同时保留预期的功能。此外,我们的讨论仅涵盖可能存在于Uniswap v4特性相关交互中的漏洞。实际应用无疑将更加全面。始终确保您理解合同的每一行,并保持SAFUE!

结论

在本文中,我们探讨了在钩子交互逻辑中出现的漏洞,特别是专注于两个场景:错误的访问控制和不当的输入验证。我们提供了详细的漏洞分析,展示了潜在的利用及其PoC,并讨论了潜在的缓解策略。我们相信这些见解有助于钩子的安全开发和使用,并指导未来漏洞检测的努力。

参考

[1] Awesome Uniswap v4 Hooks

[2] Stop Loss Order

[3] DiamondHookPoC

[4] v4-periphery

[5] Take Profits

阅读本系列的另一篇文章

Sign up for the latest updates