我们在上篇文章中提到,Awesome Uniswap v4 Hooks 仓库中超过30%的项目存在漏洞(特指Uniswap v4交互中特有的漏洞)。作为本系列文章的第二篇,我们将从以下两个角度深入探究安全的Hook交互逻辑:
-
访问控制缺陷
-
输入验证不当
针对每个类别,我们将首先进行漏洞分析,并通过相应的PoC(Proof-of-Concept)来说明潜在的漏洞利用方式,并在最后介绍可以采取的应对策略。
根据Hook是否作为locker通过PoolManager获得lock来执行在池子中的操作,我们可以将与Uniswap v4 Hook相关的交互分为不同类别。在以下两个主要的交互场景中,需要考虑适当的访问控制:
-
Hook-PoolManager 交互:官方回调函数与PoolManager之间的交互。回调函数包括八个池操作回调(即initialize、modifyPosition、swap和donate)以及锁定回调(即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回调函数:
显然,上述函数旨在执行敏感操作。但是,由于访问控制存在缺陷,就可能被攻击者通过操纵参数(例如key和params)的方式进行利用,导致意外行为。例如,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 function
modifier poolManagerOnly() {
if (msg.sender != address(poolManager)) revert NotPoolManager();
_;
}
这个修饰符可以用于对敏感的Hook和锁定回调执行适当的访问控制。
另一方面,Hook内部交互则要求任何通过lockAcquired回调调用的(由BaseHook指定),改变状态的重要函数,都不能被任意调用。
为了满足这个要求,BaseHook提供了一个selfOnly修饰符。这个修饰符限制了只能由Hook访问声明函数,禁止外部合约直接调用这些敏感函数以进行恶意操作。
/// @dev Only this address may call this function
modifier 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 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用户与之交互时产生问题。我们将用一个具体的例子深入分析该漏洞,并探讨可能的应对策略。
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
具体来说,可以通过以下步骤发起攻击:
攻击者注册一个包含虚假代币的恶意池,并指定止盈Hook作为该池的Hook。
攻击者通过Hook在恶意池中下止盈单。
攻击者在恶意池中执行交易,触发afterSwap回调中的fillOrder来完成攻击者的止盈单。
Hook调用PoolManager的lock函数获取lock,并在lockAcquired回调中调用_handleSwap函数。
在_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!
在本文中,我们探讨了在Hook交互逻辑中出现的漏洞,主要讨论了两种情况:访问控制缺陷和输入验证不当。我们先进行了详细的漏洞分析,阐述了潜在的漏洞利用方式及其PoC,并讨论了可能的应对策略。相信这些见解将有助于Hook的安全开发和使用,以及为未来的漏洞检测工作提供指引。
参考资料
Twitter:https://twitter.com/BlockSecTeam
Phalcon: https://phalcon.xyz/
MetaSleuth: https://metasleuth.io/
MetaDock: https://blocksec.com/metadock
原文始发于微信公众号(BlockSec):BlockSec|探究风险交互导致的 Uniswap v4 Hook 漏洞