臭名昭著的漏洞摘要 #4

本文是 Notorious Bug Digest 系列的第四期,汇总了近期 Web3 的漏洞和安全事件。

合作作者:Ionut-Viorel Gingu & Jainil Vora

介绍

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


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

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

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

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

Image AImage AB Image B

攻击的一个关键方面是为什么在转移-skim 操作期间池的 KRC 余额持续减少。

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

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

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

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


事件分析:覆盖内部函数允许在 MetaPool 的质押合约中免费铸造代币

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

mpETH 合约中,ERC4626Upgradeablepublic``deposit 函数和 internal``_deposit 函数已被覆盖,收据检查已从 _deposit 函数移到 deposit 函数中。这样做是为了支持包括设计变更在内的新功能,例如将代币转账移到 internal``_deposit 函数之外。

然而,开发团队显然未能考虑到,除了 deposit 函数之外,mint 函数也依赖于 _deposit 进行收据检查。因此,mint 函数未被覆盖,并且未在其中实现收据检查。因此,如果有人调用 mint,则永远不会执行收据检查,并且他们将能够免费铸造代币。E

IMAGE F

虽然该漏洞非常简单,但攻击方式并非如此。新的 _deposit 函数,不是直接铸造 mpETH,而是首先使用 MetaPool 的 LiquidUnstakePool - mpETH/ETH执行兑换,并返回那些 mpETH 代币。它仅在上述池无法提供所需数量的份额时才铸造新的 mpETH 代币。G为了利用此漏洞,攻击者执行了以下步骤

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

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

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


Across 协议 - 使用 Permit2 进行兑换时发生 DoS

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

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

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

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

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

HImage H

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

IImage I

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

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


Arbitrum Stylus 库 - 移位实现中的移位溢出

在 Rust 中,宽度为 N 的值的移位操作(<<>>)可以接受大于等于 N 的移位值,并且移位值将在移位发生之前被屏蔽为模 N。例如,u32 >> 65 等效于 u32 >> 165 % 32 = 1)。此行为在 Rust RFC 560 - Integer Overflow 中定义:

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

在调试模式下编译时,此类移位会触发溢出 panic。然而,在发布模式下,移位值将被屏蔽为模 N,如上所述。这与 GoSolidity 不同,在 Go 和 Solidity 中,结果将被截断为零。

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

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

JImage J

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

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

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

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

// 示例代码,说明该问题

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

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


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

与专家交谈

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

0 条评论

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