BlockSec|探究风险交互导致的 Uniswap v4 Hook 漏洞
2023-12-0411:28
BlockSec
2023-12-04 11:28
BlockSec
2023-12-04 11:28
收藏文章
订阅专栏

我们在上篇文章中提到,Awesome Uniswap v4 Hooks 仓库中超过30%的项目存在漏洞(特指 Uniswap v4 交互中特有的漏洞)。作为本系列文章的第二篇,我们将从以下两个角度深入探究安全的 Hook 交互逻辑:

  • 访问控制缺陷

  • 输入验证不当

针对每个类别,我们将首先进行漏洞分析,并通过相应的 PoC(Proof-of-Concept)来说明潜在的漏洞利用方式,并在最后介绍可以采取的应对策略。

- 1 -
访问控制缺

根据 Hook 是否作为 locker 通过PoolManager获得 lock 来执行在池子中的操作,我们可以将与 Uniswap v4 Hook 相关的交互分为不同类别。在以下两个主要的交互场景中,需要考虑适当的访问控制:

  • Hook-PoolManager 交互:官方回调函数与PoolManager之间的交互。回调函数包括八个池操作回调(即initializemodifyPositionswapdonate)以及锁定回调(即lockAcquired)。

  • Hook 内部交互:在 Hook 合约内部发生的交互(充当 locker 合约)。

Hook-PoolManager 交互 相对简单。Hook 仅仅充当 Hook 合约,接收八个池操作回调函数。Hook 中的逻辑不会影响相关的资金池,也就是说 Hook 与资金池之间没有资金流动。回调函数提供的参数用于修改必要的存储或作为重要的函数参数。这里需要关注的重要因素在于回调参数是否可以被操纵

Hook 内部交互相对较为复杂。在实际操作中,许多 Hook 原型不仅仅充当 Hook 合约,一些开发人员还将 Hook 设定为可以为用户提供资金管理功能。这些功能可能并没有在 Hook 合约中实现,但在这里我们仍然可以将它们统称为 Hook。在这类场景中,Hook 会接收用户资金,并执行流动性管理或兑换等操作。这意味着合约必须从PoolManager获取 lock,将 Hook 转变为 locker。Uniswap 基金会考虑到了这种情况,并在其 Hook 模板中集成了一个函数。具体来说,BaseHook模板提供lockAcquired函数作为 lock 回调函数,如下所示:

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 bytes作为参数,并使用data对自身进行底层调用(low-level call)。data的具体内容取决于 Hook 的业务逻辑,并且可以被用户操纵,这可能带来 lockAcquired 触发的Hook 内部交互引起的安全问题。需要注意的是,Hook 的设计非常灵活,我们无法涵盖所有可能出现的情况。为避免使讨论过于复杂化,我们不深入探讨其他潜在的业务逻辑,在此主要关注的是Hook 获取 lock 以及后续的内部交互

在上述两种场景中,因为这些函数都有明确的交互实体,因此首要任务都是解决可能导致漏洞利用的访问控制缺陷。在接下来的小节中,我们将依次检查每种情形,并讨论必要的访问控制,从而确保交互逻辑的安全性。

    1.1 漏洞分析

访问控制对许多项目来说是一种高效直接的安全解决方案。如果一个函数被设计为只能由特定实体调用,那么它就应该包含访问控制。最知名的访问控制示例是 OpenZeppelin 库中的Ownable合约,它要求特权函数只能由合约所有者调用。显然,我们上面讨论的两种情况都适合采用这种控制方式。

Hook-PoolManager 交互:为了与PoolManager安全地交互,Hook 应在这些回调函数上执行必要的访问控制。具体来说,这些回调应仅能由PoolManager调用,而不能由其他任何账户调用。如果没有这样的访问控制,这些敏感的接口就可能被恶意行为者利用。

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

Hook 内部交互:参与 Hook 内部交互的函数也被设计为只能由特定的调用者调用。如前所述,这种情况包含两个阶段。首先,PoolManager 调用 locker 的lockAcquired函数,这说明函数应指定 PoolManager 作为msg.sender。其次,Hook 根据情况分配函数调用。根据BaseHook的设计,它通过对 Hook 本身进行底层调用来实现。因此,这些函数必须被定义为external,并且调用者必须是 Hook 的地址。

Awesome Uniswap v4 Hooks仓库中的一个 Stop Loss Order 为例 [2]:

止损单(Stop loss orders)直接集成在 Uniswap V4 池中。这些止损单被发布到链上并通过 afterSwap() Hook 执行,无需外部机器人或参与者来保证执行。

让我们来看一下它的afterSwap回调函数:


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

显然,上述函数旨在执行敏感操作。但是,由于访问控制存在缺陷,就可能被攻击者通过操纵参数(例如keyparams)的方式进行利用,导致意外行为。例如,afterSwap回调可能在假设交换已在PoolManager中发生的情况下进行操作。随后,它可能会记录关键的状态信息,如当前价格或已收取的兑换费用。但是,如果afterSwap未严格限制只能通过PoolManager调用,攻击者就能够伪造params参数,导致记录的状态出现偏差。

    1.2 漏洞利用分析及 PoC

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

下图的 PoC 演示了对于访问控制存在缺陷的 Hook 的漏洞利用,如 DiamondHookPoC[3]所示。在没有对beforeInitialize回调函数进行访问限制的情况下,恶意行为者可以向此函数提供任意的poolKey。Hook 并未验证该poolKey的 Hook 是否与当前的 Hook 地址匹配。

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

尽管该 PoC 可能不会给 Hook 造成经济损失,但该案例清楚地说明了可以如何通过未受保护的回调函数对 Hook 的状态进行操控。

    1.3 如何降低风险

为了确保Hook 与 PoolManager 交互的安全性,Hook 的回调函数以及锁定回调都应该仅限PoolManager访问

幸运的是,Uniswap v4 通过 v4-periphery 仓库[4]中的BaseHook提供了最佳实践。BaseHook提供了poolManagerOnly修饰符,严格限制只能由PoolManager进行调用:

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

这个修饰符可以用于对敏感的 Hook 和锁定回调执行适当的访问控制。

另一方面,Hook 内部交互则要求任何通过lockAcquired回调调用的(由BaseHook指定),改变状态的重要函数,都不能被任意调用。

为了满足这个要求,BaseHook提供了一个selfOnly修饰符。这个修饰符限制了只能由 Hook 访问声明函数,禁止外部合约直接调用这些敏感函数以进行恶意操作。

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

简而言之,自定义的 Hook 可以通过继承BaseHook,利用其内置的访问控制修饰符和回调来执行适当的访问控制。


- 2 -
输入验证不当

如前所述,v4-periphery 中的BaseHook提供了一种更安全的交互逻辑解决方案,Hook 的开发人员可以对此加以利用。然而,我们还是注意到了一些不当使用的情况,这为现有 Hook 创造了新的攻击路径。

默认情况下,Hook 允许任何池子通过PoolManager中的initialize函数进行注册。然而,如果 Hook 未能验证注册池中的底层资产,恶意用户就可以注册包含伪造代币的资金池,从而通过代币的transfer函数反复调用 Hook。

这种漏洞的微妙之处在于Hook 本身可能并不执行恶意逻辑。然而,当 Hook 调用PoolManager时,PoolManager和恶意池底层资产之间的交互可能通过PoolManager中的take函数将控制流移交给攻击者。

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

从根本上讲,这个漏洞源于那些未提供合适输入校验的池子,进而导致在 Hook 用户与之交互时产生问题。我们将用一个具体的例子深入分析该漏洞,并探讨可能的应对策略。

    2.1 漏洞分析

Take Profits Hook[5]是在Awesome Uniswap v4 Hooks上给出的一个 Hook 案例:

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

让我们来看一下这个 Hook 中的_handleSwap函数。这个函数在获取 lock 后,执行兑换操作来完成止盈单。

图 3:Take Profits Hook[5]的_handleSwap 函数

表面看来,该函数没有受到任何访问控制修饰符的保护。但事实上,第 250 行的代码有效地限制了访问权限,使得该函数只能在从PoolManager获得 lock 之后调用。否则,poolManager.swap将失败,因为操作者不会是最近的 locker。换句话说,在已注册的流动性池经过验证后,_handleSwap必须按照特定的顺序调用。然而,这个 Hook 并没有实现这些验证。由于这个实现缺陷,Hook 容易受到重入攻击。这个漏洞使得攻击者能够使用用户存入的资金进行任意兑换操作。

    2.2 漏洞利用分析及 PoC

具体来说,可以通过以下步骤发起攻击:

  1. 攻击者注册一个包含虚假代币的恶意池,并指定止盈 Hook作为该池的 Hook。

  2. 攻击者通过 Hook 在恶意池中下止盈单。

  3. 攻击者在恶意池中执行交易,触发afterSwap回调中的fillOrder来完成攻击者的止盈单。

  4. Hook 调用PoolManagerlock函数获取 lock,并在lockAcquired回调中调用_handleSwap函数。

  5. _handleSwap函数中,代币的转移触发虚假代币合约中的恶意逻辑,导致重新调用_handleSwap函数。这是因为_handleSwap是一个没有任何访问限制的外部函数。由于已经获取了 lock,只要 Hook 持有足够的底层资产,攻击者可以强制 Hook 在任何池子中执行任意兑换操作。然后,攻击者可以通过夹击攻击,牺牲其他用户的利益为自己谋利。

下图详细展示了攻击流程。

图 4:攻击流程

如前所述,Hook 本身并不调用恶意逻辑。唯一的错误在于Hook 没有阻止不受信任的代币池在PoolManager合约中注册。虚假代币合约中的恶意逻辑通过代币转账操作这种间接的方式被调用,这也是一种不受信任的外部调用。

    2.3 如何降低风险

针对输入验证不当导致的潜在攻击,有三种可行的方法来减轻风险:

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

  • 重入锁。在上述攻击情景中,这种方法可以确保防止恶意代币逻辑重入敏感函数。然而,在某些情况下,Hook 的设计需要 Hook 本身是可重入的。具体来说,当 Hook 需要执行一些池操作时,应该允许PoolManager重入回调函数以完成这些操作。重入锁可能会破坏这种预设功能的实现。

  • 白名单法。特权管理员需要在 Hook 中将经过批准的池子列入白名单。管理员确保白名单中的池子不会带来潜在风险。然而,这种方法的局限性在于,Hook 用户只能通过 Hook 在管理员批准的有限数量的池中执行操作。这种方法虽然提高了安全性,却也严重限制了 Hook 的功能。

在 Hook 设计中,很难找到一个解决方案,能够在安全性和可用性之间达到完美的平衡。尽管我们讨论了几种可能的应对方法,开发者在设计 Hook 时仍需要仔细权衡利弊,在保留预期功能的同时,尽可能减少潜在风险。此外,我们的讨论只涵盖了与 Uniswap v4 特定功能交互时可能出现的漏洞。在实际应用中,情况无疑会更加复杂。请务必确保了解合约的每一行代码。Stay SAFU!

- 3 -
结语

在本文中,我们探讨了在 Hook 交互逻辑中出现的漏洞,主要讨论了两种情况:访问控制缺陷输入验证不当。我们先进行了详细的漏洞分析,阐述了潜在的漏洞利用方式及其 PoC,并讨论了可能的应对策略。相信这些见解将有助于 Hook 的安全开发和使用,以及为未来的漏洞检测工作提供指引。

参考资料

[1] Awesome Uniswap v4 Hooks
https://github.com/fewwwww/awesome-uniswap-hooks
[2] Stop Loss Order
https://github.com/saucepoint/v4-stoploss
[3] DiamondHookPoC
https://github.com/ArrakisFinance/minimize-lvr-hook-poc
[4] v4-periphery
https://github.com/Uniswap/v4-periphery
[5] Take Profits
https://github.dev/LearnWeb3DAO/uniswap-v4-take-profits-hook/

     关于 BlockSec
BlockSec 是全球领先的区块链安全公司,于 2021 年由多位安全行业的知名专家联合创立。公司致力于为 Web3 世界提升安全性和易用性,以推进 Web3 的大规模采用。为此,BlockSec 提供智能合约和 EVM 链的安全审计服务,面向项目方的安全开发、测试及黑客拦截系统 Phalcon,资金追踪调查平台 MetaSleuth,以及 web3 builder 的效率插件 MetaDock 等。
目前公司已服务超 300 家客户,包括 MetaMask、Compound、Uniswap Foundation、Forta、PancakeSwap 等知名项目方,并获得来自绿洲资本、经纬创投、分布式资本等多家投资机构共计逾千万美元的两轮融资。
官网:www.blocksec.com
Twitter:https://twitter.com/BlockSecTeam
Phalcon: https://phalcon.xyz/
MetaSleuth: https://metasleuth.io/
MetaDock: https://blocksec.com/metadock

【免责声明】市场有风险,投资需谨慎。本文不构成投资建议,用户应考虑本文中的任何意见、观点或结论是否符合其特定状况。据此投资,责任自负。

专栏文章
查看更多
数据请求中

推荐专栏

数据请求中

一起「遇见」未来

DOWNLOAD FORESIGHT NEWS APP

Download QR Code