臭名昭著的漏洞摘要 #4:通缩代币风险、ERC4626覆盖漏洞与Rust移位溢出

本文是Notorious Bug Digest系列的第四期,主要分析了近期Web3领域出现的一些安全漏洞和事件。

合作者:Ionut-Viorel Gingu & Jainil Vora

目录

介绍

欢迎来到 The Notorious Bug Digest #4——精选的 Web3 最新漏洞和安全事件的见解汇编。我们的安全研究人员在进行审计之余,还会花时间了解最新的安全领域动态,分析审计报告,并剖析链上事件。我们相信这些知识对于更广泛的安全社区来说是宝贵的,它为研究人员提供了一个磨练技能的资源,并帮助新手了解 Web3 安全的世界。加入我们,一起探索这批漏洞!


事件分析:AI 误诊 KRC/BUSD AMM 池中的漏洞利用

Binance Smart Chain 上的一个 KRC/BUSD 流动性池在一次漏洞利用中被耗尽。一个 AI 驱动的区块链交易分析代理 标记了一笔交易 为“涉及闪电贷和重复 skim() 调用的复杂漏洞利用”,并将根本原因归因于“skim/reserve 逻辑中缺少检查,使攻击者能够重复提取剩余的 token”。但这个诊断正确吗?

让我们深入研究 攻击 过程,如图 A 和图 B 所示:

  1. 攻击者闪电贷了 BUSD token,并在池中将其兑换为 KRC token。
  2. 攻击者多次将 KRC token 直接转移到池中,然后调用 pool.skim()skim 函数通过转移池的储备金和 token 余额之间的差额,强制使池的余额与其储备金相匹配,从而允许攻击者检索转移的 KRC token。
  3. 在这些 transfer-skim 操作期间,池的 KRC 余额不断减少。
  4. 当池中只剩下 0.2 个 KRC token 时,攻击者用 2.9 个 KRC token 兑换了几乎所有的 BUSD token,从而确保了他们的利润。

Image A图 AB 图 B

攻击的一个关键方面是为什么池的 KRC 余额在 transfer-skim 操作期间不断减少。

检查 KRC token 的 transfer 实现(图 C 和图 D),我们看到它包含了 通缩 逻辑。具体来说,当池是接收者(即,兑换 BUSD)时,池的余额中会销毁 9% 的转移金额,这不会被池注意到和处理。此外,token 发送者会将转移的 token 的 10% 损失到一个特定的地址。C 图 CD 图 D

通过重复的 transfer-skim 操作,攻击者不断触发通缩逻辑,导致攻击者和池的 KRC 余额以线性速度减少。这种情况一直持续到 KRC token 的价格变得非常高,使攻击者能够用他们剩余的 KRC token 兑换池中几乎所有的 BUSD,从而有效地耗尽它。值得注意的是,为了确保利润超过成本,攻击者必须使用从初始兑换中获得的 KRC token 来进行攻击,而不是使用闪电贷或外部购买的 token。

FIRE token 的案例中,也出现过类似的漏洞利用,token 通缩暂时性地降低了流动性池的储备金。这使得攻击者能够以有利的价格兑换 token,从而慢慢耗尽池。

总而言之,通缩 token 对 AMM 池构成重大风险,尤其是在攻击者可以通过 token 销毁直接操纵池的余额或储备金时。AI 代理的误诊突出了需要对 AMM 特定的机制进行更细致的分析。通过以更细的粒度检查针对典型兑换或套利活动的攻击交易,AI 可以更好地检测到像这里利用的通缩行为这样的异常。


事件分析:重写内部函数允许在 MetaPool 的 Staking 合约中免费铸币

MetaPool 的 mpETH 合约继承了 OpenZeppelin 的 ERC4626Upgradeable 合约,以支持其基于 vault 的 Staking 机制,并在 Staking ETH/WETH 资产时提供 mpETH token 作为份额。值得注意的是,ERC4626Upgradeable 合约的 public mintdeposit 函数都会调用 internal _deposit 函数,其中包含最终的检查,以确保 assets 已从调用者转移到合约(我们称之为“收据检查”)。

mpETH 合约中,ERC4626Upgradeablepublic deposit 函数和 internal _deposit 函数已被重写,并且收据检查已从 _deposit 函数中移出并移入 deposit 函数中。这样做是为了支持新的功能,包括设计变更,比如将 token 转移移到 internal _deposit 函数之外。

然而,开发团队显然没有考虑到,除了 deposit 函数之外,mint 函数也依赖于 _deposit 来进行收据检查。因此,mint 函数没有被重写,并且没有在其中实现收据检查。因此,如果有人调用 mint,则永远不会执行收据检查,并且他们将能够免费铸币。EF虽然该漏洞非常简单,但攻击向量并非如此。新的 _deposit 函数不是直接铸造 mpETH,而是首先使用 MetaPool 的 LiquidUnstakePool - mpETH/ETH 进行兑换,并返回这些 mpETH token。只有当上述池无法提供所需数量的份额时,它才会铸造新的 mpETH token。G为了利用此漏洞,攻击者 执行了以下步骤

  1. 从 Balancer 借出 200 个 WETH 的闪电贷,并在 Staking 合约上调用 depositETH 函数,以存入大量的 ETH,从而完全耗尽 MetaPool 的 mpETH/ETH
  2. 使用大量的资产 - 9701 ETH 调用 mint 函数。由于使用的 _deposit 函数是新的重写函数,并且 mpETH/ETH 池已被耗尽,因此免费为攻击者铸造了新的 mpETH token
  3. 在 MetaPool 的 mpETH/ETH 池上将 mpETH 兑换为 ETH 以偿还闪电贷,随后在 Uniswap V3 池中兑换新铸造的 mpETH token,以产生 8.89 ETH 的净利润

顺便提一下,攻击者 被 MEV 机器人抢先交易,该机器人获得了 45.79 ETH 的利润。

总而言之,必须彻底审查对 internal 函数的重写及其对父合约的依赖性,以防止此类意外后果。


Across Protocol - 使用 Permit2 进行交易时的 DoS 攻击

Permit2 合约 提供了两个关键功能:

  • 基于签名的转移,其中权限在整个交易期间持续有效
  • 基于签名的授权,其中权限在指定的时间段内持续有效

Permit2 通过 ECDSA 签名验证(如果调用者不是智能合约)或通过 ERC-1271 签名验证(如果调用者是智能合约)来授权签名。为了防止签名重放攻击,Permit2 会跟踪每个唯一的(ownertokenspender)组合的 nonce,并确保合约跟踪的 nonce 与签名中或调用者提供的 nonce 相匹配。如果它们不匹配,则不会授予授权,并且交易将被回滚。

Across Protocol 的 SpokePoolPeriphery 合约通过直接 转移、ERC-20 批准 或通过 Permit2 来促进 token 兑换。由于 SpokePoolPeriphery 是一个合约,因此使用的授权方法将是 ERC-1271,并且如果 SpokePoolPeriphery 的 nonce 与 Permit2 的 nonce 相匹配,则将授予授权,从而导致 isValidSignature 调用 通过。

我们 发现了一个漏洞,该漏洞存在于 performSwap 函数 中,该漏洞是由于可以任意指定 exchangerouterCalldata 参数的灵活性而引起的。

H图片 H

如果恶意用户将 exchange 设置为 Permit2 合约,并将 routerCalldata 设置为 invalidateNonces 函数,他们可以人为地增加预期的 nonce,从而有效地解除 Permit2SpokePoolPeriphery 合约之间的 nonce 值的同步。

I图片 I

这种解除同步会导致无法纠正的交易失败,因为 SpokePoolPeriphery 中的 nonce 调整取决于交易的成功完成。

这个问题强调了严格审查基本假设(例如 exchangerouterCalldata 参数的性质)以及彻底检查外部依赖项(即,Permit2.invalidateNonces 函数的存在,该函数超出了该审计的范围)的必要性。


Arbitrum Stylus 库 - Shift 实现中的 Shift 溢出

在 Rust 中,对宽度为 N 的值执行 shift 操作(<<>>)可以接受 >= N 的 shift 值,并且 shift 值将在执行 shift 之前被掩码为模 N。例如,u32 >> 65 等同于 u32 >> 165 % 32 = 1)。此行为在 Rust RFC 560 - Integer Overflow 中定义:

Shift 被指定为屏蔽掉过长 Shift 的位。

在 debug 模式下编译时,此类 Shift 会触发溢出 panic。但是,在 release 模式下,如前所述,shift 值将被屏蔽为模 N。这与 GoSolidity 不同,在 Go 和 Solidity 中,结果将被截断为零。

在审计 Stylus 加密库期间,我们发现了一个与此 shift 溢出行为相关的 问题shr_assignshl_assign 函数实现了 Uint<N> 类型的 shift-and-assign 运算符(<<=>>=),该类型表示具有 N 个 limb 数组的多 limb 无符号整数,每个 limb 都表示为 u64 类型。这些函数迭代所有 limb 以处理位 shift。

以下是 >>= 操作的简化版本:

J图片 J

由于每个 limb 都可以跨 shift 后的边界分成两部分,因此该代码使用两个单独的逻辑路径。第一部分(index1)处理最低有效位,这些位可能以较低下 limb(索引为 index1)的最高有效位显示。第二部分(index2)处理最高有效位,这些位可能以较低下 limb(索引为 index1+1)的最低有效位显示。条件 index_shift < indexindex_shift <= index 可确保 shift 后的位保留在有效的位范围内。

下图说明了对 uint256 整数(表示为 [u64; 4])进行 76 位右移的情况。对于索引 1 处的 limb,最低有效 12 位(index1)超出范围,而最高有效 52 位(index2)成为索引 0 处新 limb 的最低有效 52 位。Captura de pantalla 2025-07-24 a la(s) 1.29.52 p. m.

但是,shift 溢出问题可能会发生在 index1 逻辑中。具体来说,当 limb_shift 为零时,current_limb << (64 - limb_shift) 将变为 current_limb << 64。由于 current_limb 是一个 u64 值,因此 shift 量将回绕为零,从而使 current_limb 保持不变并错误地设置 limbs[index1]。预期的行为是 current_limb 在 shift 后应为零,从而使 limbs[index1] 保持不变

当使用 Stylus 合约库的代码在 release 模式下编译,并将 Uint<N> 变量 shift 64 * x 位时,此问题可能会导致不正确的算术结果。例如:

let num: Uint<4> = Uint::::new(limbs: [\
    0xffffffffffffffff,\
    0xffffffffffffffff,\
    0,\
    0xffffffffffffffff,\
]);

assert_eq!(
    num >> 64, // 内部使用 >>=
    Uint::<4>::new([0xffffffffffffffff, 0, 0xffffffffffffffff, 0])
);

该测试失败,表明索引 1 处的 limb 被错误地设置为左侧索引 2 处的旧 limb 的值(0xFF..FF),而右侧是预期的行为:ult

要解决此问题,可以使用 checked_shlchecked_shr 使用 来缓解可能的溢出。


重要的是要强调,此内容背后的意图不是批评或指责受影响的项目,而是提供客观的概述,作为社区学习的教育材料,并在未来更好地保护项目。

与专家交谈

  • 原文链接: blog.openzeppelin.com/no...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
OpenZeppelin
OpenZeppelin
江湖只有他的大名,没有他的介绍。