OpenZeppelin 审计月刊 - 那些臭名昭著的错误 第2期

本篇文章为《臭名昭著的错误月刊》第2期,探讨了Web3领域内最近的漏洞与安全事件,详细分析了针对叉沙攻击、zkLend黑客事件的教训以及多个未验证合约的漏洞。通过审计发现的问题,比如属性宏遗漏后续属性和UniswapHook的Hook状态覆盖等,强调了安全性在合约开发中的重要性。

合著者:Ionut-Viorel Gingu 和 Victor Xie

目录

导言

事件分析

审计问题

导言

欢迎来到《臭名昭著的漏洞月刊#2》——这是对最近Web3漏洞和安全事件的精心编排的洞察汇编。当我们的安全研究人员不进行审计时,他们会投入时间来跟上最新的安全动态,分析审计报告,并剖析链上事件。我们相信这些知识对更广泛的安全社区是无价的,为研究人员提供了一个资源,以提高他们的技能,并帮助新人导航Web3安全的世界。让我们一起探索这一批漏洞吧!


事件分析

用转账费用防御三明治攻击?这次不行

在2月7日,0x2d70d62 合约遭到攻击 [1] [2],原因是其 depositBNB 函数在用 WBNB 购买 ADAcash 代币时没有进行滑点保护。

img-1然而,攻击并不简单。ADAcash 是一种转账收费代币,每次转账都向代币合约收取15%的费用。因此,如果攻击者简单地用闪电贷进行三明治攻击,综合费用(买入时的15%,卖出时的15%)就能很容易耗尽任何利润。

img-2 (1)那么,ADAcash 收集到的转账费用都发生了什么?合约会进行交换,但不幸的是,没有任何滑点保护。根据代码,当 from 地址不是 AMM(如卖出 ADAcash 时)且收集到足够的费用时,收集的大部分费用会被交换成 ADA,而约 13% 则转化为 WBNB 以提升 ADAcash/WBNB 池的流动性。

img-3这就是聪明之处:攻击者可以通过对 ADAcash 代币合约本身发动第二次三明治攻击来重新获得这些费用。事情的经过是这样的:

  • 获取一笔 WBNB 的闪电贷。
  • WBNB 购买 ADAcash,抬高其价格(15% 的 ADAcash 成为费用)。
  • WBNB 购买 ADA,推动 ADA 的价格上升。
  • 触发合约 0x2d70d62depositBNB 函数。这是第一次夹击交换,进一步抬高 ADAcash 的价格。
  • 卖出 ADAcash 以从第一次夹击交换中获利。请注意,卖出 ADAcash 也会触发15%的费用。为了最小化费用,攻击者将卖出分为多个交换,因为每次卖出都会利用所有累积的费用进行交换。这些卖出构成了第二次夹击交换,进一步提高了 ADA 的价格。
  • 卖出 ADA,以锁定第二个交换的利润。
  • 偿还 WBNB 的闪电贷并确保利润。

故事的教训是什么?确保始终考虑并设置合理的滑点保护值。否则,交换可能以复杂、意想不到的方式被夹击。


从zkLend黑客事件中汲取的教训

在2月12日,zkLend 借贷协议由于 withdraw 函数中的向下取整错误遭到黑客攻击。SlowMist 发布了 详细的分析 这一漏洞的爆发。事情的经过如下:

  • 空市场设置:攻击者存入1 wei的资产,并以1:1的比率获得1个份额与 lending_accumulator
  • 放大 lending_accumulator:利用闪电贷,他们偿还了额外费用以抬高 lending_accumulator
  • 受害者存入资产:其他用户存入资产。
  • 反复 exploitation:攻击者不断地存入资产并提取大于存入数量的金额。由于大的 lending_accumulator 和由于向下取整所造成的对应精度损失,每次提取操作消耗的份额与存入操作相同。这种存入和提取的资产之间的不一致为攻击者带来了利润。

这种 exploitation 的模式并非偶然,并且在 zkLend 中使用的方法类似于其他借贷协议的攻击:

  • Radiant Capital:攻击了一个空的 USDC 市场。
  • Silo Finance:提高了一个空市场的利用率。
  • Raft Finance: 通过清算引发了向下取整的错误。
  • Wise Finance:在空市场上的一次狡猾的漏洞。它的取整有利于用户,但攻击者用隐形捐赠来操纵份额,然后反转以在市场间创造坏账。

审计人员的收获

向下取整错误只会在能够被放大的情况下排干协议。没有膨胀的话,你只能每次提取 1 wei——远低于交易费用。

为什么是借贷协议?空市场或保险库是膨胀取整错误的首要目标。这些攻击就像是 保险库膨胀漏洞 的增强版。在大多数设置中,一个空的保险库只会影响第一个存款人。但在借贷协议中,一个空市场允许你从其他市场借贷或提取抵押品,从而排干整个系统。

发现这种漏洞的一个关键问题是“攻击者可以在空市场中膨胀哪些变量?”这些不仅仅是取整错误——它们是 取整 + 膨胀 攻击。上述两个漏洞甚至没有使用错误的取整方向:它们都依赖于在空市场中膨胀一个会计变量。

对于 zkLend,审计人员可以标记 lending_accumulator 作为可膨胀的。与有利于用户的取整相结合,这就成为了排干的食谱。检查取整方向会显得尤为重要。

这个问题还会挖掘出非取整错误。请考虑 Silo Finance漏洞——它利用捐赠在空保险库中膨胀一个变量,无需任何取整技巧。在 Raft Finance漏洞中,一个有利于用户的取整错误被报告为低严重性问题,并在修复审查中没有得到解决。对一个错误如何被放大的进一步分析可能揭示了关键,并避免这次黑客事件的发生。


未经验证合约中的漏洞

本月,由于薄弱的访问控制、不足的输入验证或未初始化的状态,多份未经验证的智能合约遭到攻击。以下是事件的总结。

合约 0xeffca1:通过未检查的回调调用损失20 ETH [1] [2]

下图高亮显示了一个关键问题:不足的访问控制。受害者是一个 MEV 机器人,未能验证其 uniswapV2 函数的调用者。然而,对反编译代码和 msg.data 的深入分析揭示了更多细节:

  • 不存在 uniswapV2 函数——该调用实际上触及了机器人的回调逻辑。
  • delegatecall 的目标地址 (0xcc85e)、参数(例如,对不存在的 0x24ec8approve 调用)以及后续的 WETH转移都由 msg.data 控制。
  • 值得注意的是,delegatecall 的目标在 explo时之前几小时被 列入白名单——这是一个特权行为。

这引发了一个问题:该攻击是自动化的吗?一个熟练的攻击者可以绕过 0x24ec8,直接批准 WETH,并通过 swapIn 回调提取它。考虑到反编译代码的复杂性,黑客可能已经变异历史 msg.data,注入自己的地址、资产细节和一个有利可图的返回值。这仍然是推测,但可以信服。

img 4合约 0x378c6:通过假池交换盗取 151 BNB [1] [2]

这一漏洞相对简单。如图所示,0xb9d384fa 函数缺乏访问控制,使得在未经验证的 Uniswap V3 池上进行代币交换成为可能。攻击者用最少的 WBNB 和大量假代币创建了一个池,然后触发 0xb9d384fa 来交换 WBNB 与毫无价值的代币。随后,他们撤回流动性,从而在 WBNB 中获利。一个关键细节是:滑点保护参数 sqrtPriceLimitX96(不受攻击者控制)被设置为 MIN_SQRT_RATIO,确保以低 WBNB 价格成功交换。

img-5合约 0xd4f1a:由于未初始化状态损失23 BNB [1] [2]

这一案例同样明了。受害者是一个可初始化的合约,部署时并没有调用 _disableInitializersinitialize 部署,导致其未初始化。在两天累积费用后,攻击者调用 initialize 来声称所有权,并调用 withdrawFees 来 siphon资金。有趣的是,在攻击后,费用仍持续流入,而攻击者则将这些费用重定向 到 Tornado.cash


审计问题

Stellar Contracts 库 - 属性宏省略后续属性

这个漏洞是在我们的 Rust 审计中发现的。它突显了过程宏在 Rust 中是如何工作的。过程宏是一个 编译时代码生成工具:当编译器看到 function doAction 被标记为 macro 时,它会查找 macro 的定义,并根据该定义增加、删除或替换 doAction 函数中的代码。

这为开发像 Solidity 的修饰符这样的功能提供了强大的工具。我们可以创建一个 when_not_paused 过程宏,当它写在一个函数上方时,将复制函数代码并在开头添加额外的逻辑。如果 paused 变量被设置为 true,则额外逻辑将会失败。

同样,客户端打算创建一个 when_not_paused 过程宏。该宏将函数代码(连同所有注解)解析为 token,并可以通过以下方式访问:

  • fn_vis(函数可见性:例如 pub
  • fn_sig(函数签名:例如 fun doAction(number: u32) -> u32
  • fn_block(函数主体:例如 return number;

在编译时,编译器复制 函数可见性、函数签名和函数主体,并在函数开头添加 openzeppelin_pausable::when_paused(#env_arg); 调用,如下所示:

img-6 (1)有什么东西缺失了。但是什么呢?其余的注解!如果我们的函数还被其他过程宏注解(想想 only_owner),则 when_not_paused 宏将改变代码,从而使得 only_owner 在此过程中丢失。此问题的修复将是 也复制其他注解,因此结果如下:

img-7 (1)


Uniswap Hooks - 非显式多池支持允许覆盖Hook状态

在我们对 Uniswap Hooks 库的审计中,我们发现了 BaseCustomAccounting hook 的一个关键漏洞。这个Hook旨在完全控制其关联池的流动性,确保所有流动性修改都通过它。因此,它必须永久链接到单一池。否则,流动性可能会变得不可访问。

然而,由于池-Hook意识的不对称,任何有效的Hook地址都可以附加到任何池,除非被Hook自身明确拒绝。BaseCustomAccounting Hook缺乏防止重新初始化的保护,允许攻击者将其从受害者的池重定向到恶意创建的 Uniswap 池。这实际上锁定了受害者池的流动性,阻止了对资金的访问。

修复确保了,一旦定向到池,一个继承自 BaseCustomAccounting 的Hook将无法被重新初始化并指向另一个池。


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

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

0 条评论

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