Certora宣布重新启动Capture the Funds - Endless CTF,这是一个DeFi黑客挑战赛,旨在提供一个真实的环境,供安全研究人员探索、利用并展示他们的技术技能。比赛提供了一个相互关联的DeFi生态系统,参赛者需要通过攻击协议来窃取资金。比赛的目标是鼓励更多人学习Web3安全知识,并为超过目标分数的参与者提供奖金。
我们的第一个 DeFi 黑客挑战 — Capture The Funds,Certora 的下一代 CTF — 于 2025 年底举行。在这次成功的基础上,我们宣布重新启动该比赛,并进行了一些调整:Capture the Funds - Endless CTF!
Capture the Funds 旨在为经验丰富的安全研究人员提供一个真实的环境来探索、利用和展示深厚的技术技能。参赛者不是寻找孤立的错误,而是试图从我们为他们准备的 DeFi 生态系统中窃取价值。这更接近于现实世界的安全分析,结果不仅仅是什么是 bug,而是你的利用有多干净、你能把它推到多远以及一个弱点如何渗透到整个生态系统中?
在我们的联合赞助商 Ethereum Foundation、Coinbase、Aave 和 Lido Finance 的帮助下,为期四周的比赛取得了成功,赢得了参与者的高度赞扬,并获得了一些我们很高兴在此基础上继续发展的经验教训。阅读到最后,了解今天重新启动挑战赛的信息。
安全 CTF 是很好的教育资源,也是吸引那些在棘手的实际问题上表现出色的人才的方式。鉴于我们在 web3 安全方面的经验,我们认为我们有能力在这里做出一些新的贡献。
从一开始,我们就有一个目标:避免一系列不相连的线性挑战。相反,我们想构建一个连贯的 DeFi“世界”,让道德黑客可以自由探索,选择他们在协议和攻击面上的路径。我们想要一种开放世界的感受,你必须推理一个实时系统,形成假设,并通过主动利用来验证它们,而不是仅仅遵循代码中的面包屑。
我们还希望评分能够反映媒介。评分将使用以太坊的原生货币,而不是二进制的 bug 清单,这自然地捕捉了成功的程度。两个参赛者可能会发现相同的弱点,但根据他们的利用效率以及他们如何将操作链接在一起,他们可能会获得非常不同的 ETH 数量。
最后,我们想要真实感。大多数漏洞的灵感来自我们在实际审计中看到的模式,以及一些基于 EVM 和 Solidity 怪癖的漏洞,我们认为这些值得了解。我们的目标不是为了棘手或神秘而棘手或神秘,而是要务实、有指导意义和引人入胜。
组织这样的比赛提出了几个技术和概念上的挑战:
我们用超过 6 个月的时间逐渐回答了第一个问题。大部分时间都花在了调整生态系统上,这样协议和漏洞都有意义。这意味着协议中呈现的任何机制都必须有内在的理由,并且所有协议必须以某种有意义的方式互连(例如,Auction 协议将锁定的资产投资于 Lending 协议)。此外,在许多 CTF 中(不是指责任何人),协议往往包含一些特性,除了允许某些漏洞存在之外,没有任何其他目的。虽然这样的协议肯定具有教育价值,但我们利用我们丰富的协议审计经验来超越人为的例子。我们选择了你可能在实际中遇到的协议和机制(真正的漏洞潜伏在黑暗中)。
创建一种接受和验证提交的机制主要是一个设计挑战。与常规 CTF 不同,提交的内容不是“flag”字符串,而是从给定的初始状态开始的逻辑操作序列,这使得验证变得更加困难。一个简单的解决方案是为每个参与者创建一个单独的区块链,可以通过 Web UI 访问。虽然这有一些优点(例如,容易跟踪评分和参与情况),但它会过于复杂并且计算成本太高。相反,我们决定采用一种组合方法:每个参与者运行区块链的本地实例并提交一个“replay”文件。我们提供了一个本地 NodeJS 服务器,可以轻松与本地区块链进行交互。每次用户测试攻击合约时,它都会被写入 replay 文件,在本地区块链上执行,并且他们可以看到对其 ETH 余额的影响。一旦用户对本地抢劫感到满意,他们就可以将 replay 文件提交到 CTF 服务器,该服务器以初始状态启动区块链的 ad hoc 实例,重新执行攻击,并相应地对提交进行评分。
最后,重要的是参与者从一开始就享受体验,因为据报道 web3 安全研究人员更喜欢乐趣 [需要引用]。问题是,利用几乎总是与乐趣相反。因此,我们设计了本地 NodeJS 服务器 Web UI 的许多功能来减轻痛苦:攻击合约的便捷模板、Hardhat 控制台和事件日志、每个协议状态的可视化以及使用自由铸造的资产而不是闪电贷来测试利用步骤的探索模式。
比赛持续了四个星期,而大多数 CTF 仅持续几天。最初的几天非常繁忙——在一个协议中发现了一个意外的漏洞,破坏了 CTF 的一些内部逻辑。幸运的是,我们发现得足够早,影响很小。一周后,我们发现一些提交以一种意想不到的方式利用了我们的一个 bug,使其比我们预期的更有利可图。由于这是一个很好的利用,我们决定保持原样。这两起事件让我们吸取了一个重要的教训:编写安全代码真的很难,尤其是如果你故意使其不安全。
比赛的其余部分进行得很顺利。有趣的是,在最后几天,提交的数量急剧增加,这暗示着很多人延迟提交,以避免向竞争对手提供有价值的信息(即,可以实现一定的分数)。在最后的 48 小时内,提交蜂拥而至,这导致了排行榜的剧烈变化。在比赛结束前 30 分钟,获胜的解决方案以超过 100 分的优势超过了之前的领先者。稍后会介绍有关获胜者的更多信息。
以下撰写的内容有意不完整:它不包括挑战集中每个问题的完整端到端利用,并且某些协议部分仅在根本原因分析和攻击方向的级别上进行介绍。将其视为构建漏洞的指南,而不是一组交钥匙的解决方案。
剧透警告! 如果你想自己找到这些漏洞,请跳到下面的“获胜”部分。
在 Lottery 中,购买彩票会产生一个随机的基础奖金和一个可选的“技能奖励”,需要解决 $x^2 \equiv \text{magic} \pmod N$,这是一个标准的数论任务(例如,参见 Alpertron's QUADMOD)。
真正令人担忧的是围绕技能挑战的不必要的样板代码:数十个名为 solveMulmodXXXXX 的公共入口点,以及“审计修复”注释,暗示早期版本包含后门。Lottery 与 LotteryExtension 一起部署,任何与主合约中的函数不匹配的调用都会通过 fallback 和 delegatecall 转发到扩展合约。因此,用户会被引诱调用高收益的 solveMulmodXXXXX 扩展合约中的函数。
“审计员”忽略的是,Solidity 调度纯粹是基于选择器的:选择器只有 4 个字节(keccak256("<signature>") 的前 4 个字节),并且主合约在考虑 fallback 之前解析选择器。由于函数名称在很大的空间内变化(5 位数的后缀给出了 $\approx 10^5$ 个候选对象),因此找到冲突是可行的(例如,Birthday Attack)。当扩展函数的选择器与直接在 Lottery 中实现的任何函数的选择器冲突时,该调用永远不会到达扩展合约——它会被路由到主合约。
这正是最有利可图的“技能”函数所发生的事情。例如,LotteryExtension 中的 solveMulmod99781、solveMulmod99715、solveMulmod99437 和 solveMulmod98752 都与主 Lottery 合约中的函数选择器冲突。尝试解决这些顶级挑战之一的用户会被静默地调度到 Lottery 中的另一个 solveMulmod 验证器(具有不同的参数),因此即使是针对预期方程式的正确解决方案也无法通过验证,并且技能奖励也会丢失。实际上,最高奖励的扩展难题是无法访问的;最佳的可访问高奖励调用是 solveMulmod93740、solveMulmod90174 和 solveMulmod89443。
在 Auction 中,用户可以通过常规(英式)拍卖或 荷兰式拍卖 拍卖 NFT (ERC721)。竞标者不会直接从他们的钱包付款。相反,他们首先将选择的 ERC20 存入 AuctionVault 并收到一个 AuctionToken,该 AuctionToken 代表对该 vault 余额的按比例索取权(并且是在竞标期间锁定/解锁的单位)。
核心 bug 是一个 类型混淆,由 ERC20 和 ERC721 之间的 ABI 重叠启用。两者不仅都公开了 transferFrom(address,address,uint256)(将 uint256 解释为金额与 tokenId),而且两者都公开了 approve(address,uint256)(同样,允许金额与 tokenId 批准)。Auction 依赖于接口类型(IERC20 与 IERC721),但不强制提供的 token 合约实际实现预期的标准。因此,攻击者可以传递一个 ERC721,而协议期望一个 ERC20(或反之亦然),并且仍然满足协议的批准和转移流程,同时将 uint256 参数的含义从金额更改为 tokeId(或反之亦然)。
在第一个方向(ERC721 被视为 ERC20)中,攻击者注册一个 NFT 合约作为新 AuctionToken 的“底层”资产,然后使用 depositERC20/withdrawERC20。在底层,协议调用 “ERC20” 上的 transferFrom(),因此存入/提取实际上会移动其 tokenId 等于提供的 amount 的 NFT。由于 vault 必须已批准 AuctionManager 用于相关的 tokenId,因此对于通过 荷兰式拍卖 放置在 vault 中的 NFT 来说,可以特别利用这一点,其中管理器在拍卖创建时获得批准。然后,攻击者可以直接通过 ERC20 提取路径提取特定的 tokenId。
在第二个方向(ERC20 被视为 ERC721)中,攻击者创建一个“NFT 拍卖”,其中 nftContract 实际上是一个 ERC20 token 合约。createAuction 路径将该 ERC20 的 tokenId 单位转移到 vault 中(因为 transferFrom 存在),但没有铸造相应的 AuctionToken。因此,vault 报告的底层资产增加,而 AuctionToken 供应量没有增加。由于 AuctionToken 余额是根据 vault 的总底层资产进行缩放的,因此这会暂时膨胀现有 AuctionToken 的价值。持有 AuctionToken 的攻击者可以在通货膨胀窗口期间提取更多的底层资产,然后通过让拍卖结算(回到自己),从而收回注入的 ERC20,从而使 vault 净耗尽。
Exchange 协议允许用户通过 unlock 流程在 ERC20 token 之间进行交换:在解锁窗口期间,调用者可以提取 vault 持有的任何资产(通过 sendTo)并运行任意逻辑,只要在该调用的结束时,每个 token 的余额都“结算”回零。这有效地启用了免手续费的闪电流动性:你可以在 unlock 中借用资产,用它们做任何你想做的事情,并在瞬态上下文关闭之前偿还。
漏洞在于 SafeCast.toInt256(uint256 value)。该例程旨在拒绝不适合 signed int256 的值,但它错误地允许 value = 2^255(即,当解释为 int256 时为 INT_MIN)而不是恢复。因此,使用 amount = 2^255 调用 sendTo 不会像预期的那样记录巨大的正债务;它记录了一个巨大的负增量(信用/盈余),因为强制转换返回 -2^255。通常,此调用仍然会失败,因为 sendTo 会立即尝试 token.transfer(to, amount),并且在实践中不可能转移 2^255 个 token。
关键在于,其中一个 ERC20,NISC,具有节省 gas 的优化:当 from == to 时,transfer 会短路(即,自转移会返回成功,而无需进行余额检查或状态更新)。通过选择 to = address(ExchangeVault),攻击者可以执行 sendTo(NISC, address(this), 2^255) 来铸造巨大的“信用”增量,而无需发生任何实际转移。然后,他们使用该信用从 vault 中提取所有的真实 NISC 余额,并通过正常的 sendTo(NISC, attacker, vaultBalance),最后通过对剩余差额(2^255 - vaultBalance)执行一次自转移来将增量恢复到完全为零。由于所有三个调用都发生在单个 unlock 内部,因此结算检查通过,并且攻击者带着 ExchangeVault 持有的所有 NISC 离开。
在 LendingPool 中,闪电贷通过一个特权的 FlashLoaner 合约进行路由,并且预计成本很高 (10% 的费用)。预期不变的很简单:FlashLoaner.flashloan() 在进入时快照其 token 余额,将 amount 转移给接收方,运行接收方的回调,然后检查其回调后的余额是否至少为 initialBalance + fee。然后,它偿还从池中提取流动性的资金,并将任何剩余盈余视为费用收入。
bug 是该记账是基于余额的,并且 flashloan() 是可重入的。在嵌套的闪电贷期间,FlashLoaner 余额已经包含外部调用的本金;由于偿还条件不跟踪每个贷款的负债,因此外部本金与“偿还盈余”无法区分,并且实际上被计为内部调用所需的费用(反之亦然,在解开时)。换句话说,在递归调用中,协议隐式地使用来自其他活动帧的未偿还本金来满足 initialBalance + fee 检查。
利用方式是以几何方式增加数量(大致按 $\frac{1+r}{r}$ 缩放(对于 10% 的费用,约为 11 倍))递归调用 flashloan(),并且仅在最深级别偿还一次。当调用堆栈解开时,每个帧都通过其偿还检查,因为合约仍然持有来自其他帧的本金,因此“费用”是使用暂时借入的资金而不是攻击者自己的资金支付的。攻击者最终以协议的费用获得(几乎)免手续费的闪电流动性。
在 Community Insurance 中,流动性提供者因在 Lending 协议中提供不良债务的保护而获得奖励。奖励由外部 RewardDistributor 跟踪,并且保险份额(一种类似 ERC20 的 token)在每次份额转移/铸造/销毁时调用到分配器中,以使用其转移前的自由份额余额(以及当前的自由总供应量)来“结算”相关方的奖励。
关键的设计决策是通过包装在 try/catch 中的外部调用来执行这些奖励更新,并使用空的 catch 块,以便发生故障的分配器不会阻止正常的份额操作。这会将奖励记账变成尽力而为的副作用:如果调用失败,份额转移仍然成功,但分配器的全局索引(lastUpdateTime、rewardPerTokenStored)和/或用户的检查点(userRewardPerTokenPaid)不会更新。
恶意用户可以通过gas 悲伤可靠地强制执行此失败:提交转移/铸造/销毁交易,其 gas 刚好足够,以便外部 updateReward 调用根据 EIP-150 的 63/64 gas 规则 接收太少的 gas,从而导致被调用者中出现 out-of-gas 错误,而调用者继续,并且 catch {} 吞下该错误。一旦攻击者可以选择性地跳过奖励更新,他们就可以将分配器的时间/索引记账与份额移动取消同步,然后以攻击者选择的条件(例如,在操纵自由供应之后)触发更新/索取,提取与预期的“持续的、按时间的比例分配”模型不一致的奖励。
当参赛者争先恐后地完成以提高他们在比赛排行榜上的排名时,评分系统按设计执行。最伟大的灰帽超越了伟大和优秀的人。他们所有的攻击都值得称赞,并且最好的 3 名甚至获得了现金奖励。其中两位获奖者慷慨地撰写了他们的工作,你可能需要仔细研究这些内容:Bill(第一名)在 这篇文章 中讲述了他如何获胜,并分享了他的获胜解决方案,而 SpicyMeatball(第三名)在 他的解决方案存储库 中解释了他的每个攻击和完整的攻击。
现在,让我们看看谁能做得更好。
宣布推出 Capture the Funds - Endless CTF!
为了鼓励 web3 安全领域的更多人学习 Capture the Funds 中的课程,我们再次接受提交,对于任何比目标分数高出至少 25 ETH 的人,奖励 1000 美元(因为我们正在寻找超越 gas 优化的创新)。发生这种情况时,门槛会提高,并且奖励再次可用。就这样一直持续下去!
请务必探索并攻击 比赛网站 上链接的最新代码。它是原始比赛的 DeFi 生态系统,但……一些漏洞已被修补,其他漏洞仍有待发现,并且有一个新的排行榜,其中包含基于迄今为止发布的漏洞的 Certora 目标分数。阅读最新的奖品和资格规则,并加入我们的 Discord 服务器 以在 #capture-the-funds 频道中进行讨论。
我们的第一次 Capture the Funds 比赛提供了一个真实且引人入胜的 DeFi 安全挑战。通过制作一组相互关联的协议,其漏洞的灵感来自现实世界的审计,我们超越了孤立的 bug 寻找,以促进更深入的安全分析。
随着 Capture the Funds - Endless CTF 的推出,我们期待看到更多的研究人员探索并为 Web3 安全的集体知识做出贡献。因此,我们邀请你学习最佳利用方式,并尝试进一步推进,因为就像追求保护 DeFi 一样……挑战是无止境的。
- 原文链接: certora.com/blog/capture...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!