五个小时时间,Samczsun 与多名白帽黑客联手完成了一场 3.5 亿美元资产抢救行动。

原文标题:《BitDAO 的 3.5 亿美元险些被盗,白帽黑客讲述一场惊心动魄拯救行动全程》
撰文:Samczsun,区块链投资机构 Paradigm 研究合伙人、著名加密白帽黑客
编译:Kyle

8 月 17 日,区块链投资机构 Paradigm 研究合伙人、著名白帽黑客 Samczsun 撰文披露了 BitDAO 在 SushiSwap IDO 平台 MISO 进行的荷兰拍的智能合约存在安全漏洞,多名白帽黑客联手从众筹资金池中拯救回 10.9 万枚 ETH (约合 3.5 亿美元)的经过。


当构建软件时经常出现的一个常见误解是,如果系统中的每个组件都经过单独验证是安全的,那么系统本身也是安全的。 这种观念在 DeFi 项目中被更多地体现了出来。在 DeFi 项目开发中,可组合性就是开发人员的第二天性。 不幸的是,虽然将两个安全的组件部分组在一起,在大多数情况下可能是安全的,但这也意味着只需要一个漏洞即可对数百甚至数千名无辜用户造成严重的经济损失。 今天,我想告诉你我如何发现并帮助修补了一个严重的漏洞,这个漏洞会使超过 109,000 ETH (按今天的汇率计算约 3.5 亿美元)面临被盗风险。

相遇

上午 9:42

当我在推特上注意到 @ivangbi_和 @bantg 之间关于 SushiSwap 的 MISO 平台上的一个新代币销售项目的讨论时,我正随意浏览 Telegram 上的 LobsterDAO 群聊。 我通常会尽量避免在公共场合做出一些戏剧性的事情,但我忍不住在谷歌上快速搜索了一下,看看到底是怎么回事。 我得到的结果对我来说并不是特别有趣,但我继续往下搜索,因为我觉得如果我继续寻找,就会发现这里有一些有趣的东西。

MISO 平台支持两种类型的代币拍卖模式 : 荷兰式拍卖和批量拍卖。 而今天说的这个代币销售是通过荷兰式拍卖进行的。 当然,我做的第一件事就是在 Etherscan 上打开该项目的合约地址。

上午 9:44

我通过该项目的参与协议快速浏览了这起荷兰式拍卖的合约并检查了每个有意思的函数。该合约的提交函数(commitEth、commitTokens 和 commitTokensFrom)似乎都已正确实现。拍卖管理函数(setDocument、setList 等)也有适当的访问控制。然而,在底部附近,我注意到 initMarket 函数没有访问控制,这非常令人担忧。此外,它调用的 initAuction 函数也不包含访问控制检查。

不过,我真的没想到会发现这么一个漏洞,因为我没想到 Sushi 团队会犯下如此明显的失误。果然,initAccessControls 函数验证了合约尚未初始化。

然而,这时候我又有了另一个发现。在滚动浏览所有文件时,我注意到了 SafeTransfer 和 BoringBatchable 库。我对这两者都很熟悉,并且立即对 BoringBatchable 库的潜在危险感到震惊。

这里先解释一下,BoringBatchable 是一个混合库,旨在轻松地将批处理调用引入任何导入它的合约。 它通过为输入中提供的每个调用数据对当前合约执行委托调用来实现这一点。

function batch(bytes[] calldata calls, bool revertOnFail) external payable returns (bool[] memory successes, bytes[] memory results) {
        successes = new bool[](calls.length);
        results = new bytes[](calls.length);
        for (uint256 i = 0; i < calls.length; i++) {
            (bool success, bytes memory result) = address(this).delegatecall(calls[i]);
            require(success || !revertOnFail,_getRevertMsg(result));
            successes[i] = success;
            results[i] = result;
      }
}

看看上面这个函数,它似乎被正确地实现了。 然而,在我脑海的角落里,有什么东西在提醒着我。 那时我意识到我过去曾看到过非常相似的东西。

发现

上午 9:47

距离今天大约一年多前,我在与 Opyn 团队的 Zoom 视频通话中,试图弄清楚如何在遭受毁灭性黑客攻击后恢复和保护用户资金。 黑客攻击手法本身很简单但很巧妙 : 它使用一次 ETH 支付来行使多个期权,因为 Opyn 合约在循环中使用了 msg.value 变量。 虽然处理代币支付涉及每个循环迭代的单独 transferFrom 调用,但处理 ETH 支付只是检查 msg.value 是否足够。 这允许攻击者多次重复使用相同的 ETH。

回到今天,我意识到我正在看到的是两个完全相同的漏洞,只是形式不同。 在委托调用中,msg.sender 和 msg.value 被持久化。 这意味着我应该能够批量调用 commitEth 并在每个 commitment 中重复使用我的 msg.value,这将允许我在拍卖中能够免费出价。

上午 9:52

我的直觉告诉我这是真实的交易,但我无法在没有实际验证的情况下确定。 我迅速打开 Remix 并编写了一个概念验证。 令我沮丧的是,我的主网分叉环境之前不久被完全损坏了。 我一定是在伦敦硬分叉期间不小心弄坏了它。 有这么多资金正处于风险之中,而我却没有足够的时间。 我很快在命令行上拼凑了一个简陋的主网分叉并测试了我的漏洞。结果跟我想的一样。

上午 10:13

在对外报告这个漏洞风险之前,我给我的同事 Georgios Konstantopoulos 打了电话,让他​​们再看一遍。在等待回应的同时,我又回到合约中寻找确定严重性的方法。在这种情况下,能够免费参加拍卖是一回事,但能够窃取所有其他参与者的出价则是另一回事。

我注意到在我最初的扫描过程中有一些退款逻辑,但当时我并未多想。现在,这已是一种让 ETH 退出合约的方法。我很快检查了我需要满足哪些条件才能让合约为我提供退款。

令我惊讶(和恐惧)的是,我发现发送的任何超过拍卖硬上限的 ETH 都会获得退款。即使达到硬上限,这也适用,这意味着合约不会完全拒绝交易,而是简单地退还您的所有 ETH。

突然间,我发现的这个漏洞变得巨大。我不是在处理一个让你出价可以超过其他参与者的漏洞。我在看的是一个价值 3.5 亿美元的漏洞。

披露

上午 10:38

在与 Georgios 确认这个漏洞后,我让他和 Dan Robinson 尝试联系 Sushi CTO Joseph Delong。 几分钟后,Joseph 做出了回应,然后我与 Georgios、Joseph、Mudit、Keno 和 Omakase 一起进行了 Zoom 通话。 我就漏洞向其他参与者进行了快速汇报,然后他们开始四处协调响应。 整个通话只持续了几分钟。

准备

上午 11:26

在救援行动室里,Mudit、Keno、Georgios 和我正在忙着写一份简单的救援合约。 我们决定最干净的做法是发起一笔闪电贷,直接购买到硬上限,结束拍卖,然后使用拍卖本身的收益偿还闪电贷。 这种方法不需要前期准备资金,效果非常好。

下午 1:36

当我们完成救援合约的工作时,我们讨论了批量拍卖的后续步骤。 Mudit 指出,即使在拍卖进行时也可以设置一个积分列表,并且在每次 ETH commitment 期间都会调用它。我们立即意识到这可能是我们正在寻找的暂停功能。

我们集思广益,想出了不同的方法来使用这个方法。立即还原是一个显而易见的解决方案,但我们想要更好的方案。我考虑添加一个检查,每个源只能为每个区块做出一个 commitment,但我们注意到该函数被标记为视图,这意味着 Solidity 编译器将使用静态调用操作码。我们的方式不允许进行任何状态修改。

经过一番思考,我意识到我们可以使用积分列表来验证拍卖合约是否有足够的 ETH 来匹配所做的 commitment。换句话说,如果有人试图利用这个漏洞,那么 commitment 会比 ETH 多。我们可以很容易地检测到这一点并还原交易。 Mudit 和 Keno 开始编写测试以进行验证。

救援

下午 2:01

通信突围团队与救援突围团队合并工作以同步进度。 他们已经与执行拍卖的团队(BitDAO)取得了联系,但该团队希望手动完成拍卖。 我们讨论了风险并认为某个自动化机器人注意到这笔交易或能够对其采取任何行动的可能性很小。

下午 2:44

执行拍卖的团队完成了拍卖,消除了直接威胁。 我们互相祝贺成功,然后各自解散。 这次批量拍卖将在当天晚些时候悄悄结束。 不知情的人恐怕不知道刚刚避免了一场多么严重的灾难。

反思

下午 4:03

过去的几个小时让人感觉很模糊,时间好像静止一样。我从相遇这个项目到发现漏洞只用了半个多小时,20 分钟内进行了披露,另外 30 分钟内作战室,三个小时内修复漏洞。总而言之,只用了五个小时就保护了 3.5 亿美元不落入坏人之手。

即使没有金钱上的损失,我相信所有参与其中的人都更愿意一开始就没有经历过这个过程。针对这次事件,我有两个主要的要点给你。

首先,在复杂系统中使用 msg.value 很困难。它是一个全局变量,您无法更改并在委托调用中保持不变。如果您使用 msg.value 来检查是否已收到付款,则绝对不能将该逻辑置于循环中。随着代码库复杂性的增加,很容易忘记发生的位置并意外地在错误的位置循环某些内容。虽然封装和释放 ETH 很麻烦并且引入了额外的步骤,但如果想要避免这样的事情,那么 WETH 和其他 ERC20 代币之间的统一接口可能值得一试。

其次,两个安全组件组合在一起,可能就会得到不安全的东西。我之前曾在可组合性和 DeFi 协议的背景下声明过这一点,但这次事件表明,即使是安全的合约级组件也可能以产生不安全的合约级行为的方式混合。这里没有像「检查-效果-交互」这样的包罗万象的建议,所以你只需要了解新组件引入的额外交互。

我要感谢 Sushi 的贡献者,Joseph、Mudit、Keno 和 Omakase 对这个问题的快速响应,以及我的同事 Georgios、Dan 和 Jim 在整个过程中提供的帮助,包括审阅了这篇文章。

来源链接:www.8btc.com