原标题:Two Rights Might Make A Wrong

来源:paradigm

作者:samczsun

编译、整理:Chen Zou

许多工程师都认为如果一个系统中的每个组件都被单独验证为安全的,那么这个系统本身也是安全的——这是构建软件的一个常见误区。这个概念在DeFi中得到了很好的诠释,在那里,可组合性是开发者的第二天性。不幸的是,虽然两个组件的组合在大多数情况下可能是安全的,但只需要一个简单的漏洞就可以对数百甚至数千名无辜的用户造成严重的经济损失。今天,我想告诉你们,我是如何发现并帮助修补一个漏洞,而这个漏洞曾使超过10.9万枚以太坊(以今天的汇率计算约3.5亿美元)处于危险之中。

我在不经意间浏览Telegram上的LobsterDAO群时,注意到@ivangbi_和@bantg之间关于SushiSwapMISO平台上的一个新加价的讨论。我通常会尽量避免在公共场合进行讨论,但我忍不住在谷歌上快速搜索,看看这到底是怎么回事。但我并没有得到什么能激起我兴趣的答案,于是我继续顺藤摸瓜,因为我觉得如果我继续寻找的话,最后总能发现一些有趣的东西。

MISO平台运营两种类型的拍卖。荷兰式拍卖和批量拍卖。在这种情况下,加价是通过荷兰式拍卖进行的。自然,我做的第一件事就是在Etherscan上打开该智能合约。

我根据参与协议快速浏览了DutchAuction合约,并检查了每个有趣的功能。提交功能(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)

        result = new bytes[](calls.length)

        for (uint256 i = 0; i < calls.length; i++) {

            (bool success, bytes memory result) = address(this).delegatecall(calls[i]) 

            request (success || !revertOnFail, _getRevertMsg(result)) 

            successes[i] = success;

            results[i] = result

        }

    }

看着这个函数,它似乎也被正确地实现了。然而,我脑海中的某个角落却在唠叨着。这时我意识到我在过去曾见过非常类似的东西。

发掘线索

上午9点47分

一年多以前的今天,我和Opyn团队在Zoom通话,试图找出在一次破坏性的黑客攻击后如何恢复和保护用户资金。那次攻击本身很简单,但却很聪明:因为Opyn合约在一个循环中使用msg.value变量,所以它用一笔以太坊付款行使了多个期权。当处理代币支付时,每个循环迭代都需要单独调用transferFrom,而处理以太坊支付时只需检查msg.value是否足够。这使得攻击者可以多次重复使用同一个以太坊。

我意识到,我看到的是以不同形式存在,但内核完全相同的漏洞。在delegatecall里面,msg.sender和msg.value被永久化了。这意味着我应该可以多次批量调用commitEth,并在每次承诺中重复使用msg.value,从而允许我在拍卖中免费出价。

上午9点52分

我的直觉告诉我这才是真正的问题,但我在没有完全验证它之前依旧无法确定。我迅速打开了Remix,写了一个概念验证。但麻烦又来了,我的主网分叉环境完全被破坏了。我一定是在弄伦敦硬分叉的时候不小心破坏了它。但有这么多的钱处于风险之中,我顾不上修复主网分叉环境了,只能迅速地在命令行上组装了一个丐版主网分叉,并测试了我的漏洞。显然,它成功了。

上午10点13分

我联系了我的同事Georgios Konstantopoulos,希望在报告之前得到第二双眼睛的关注。在等待答复的同时,我又回到合同中,寻找提高严重程度的方法。能够免费参与拍卖是一回事,但如果能够把其他所有的投标也偷走,那就是另一回事了。

在我最初的扫描中,我注意到有一些退款逻辑,但没有想到。现在,这是一个将ETH从合约中取出的方法。我迅速检查了我需要满足哪些条件才能让合同向我提供退款。

令我惊讶(和恐惧)的是,我发现如果发送的ETH超过了拍卖的硬上限,就会被退款。这甚至在达到硬上限后也适用,这意味着合同不是完全拒绝交易,而是简单地退还你所有的ETH。

突然间,我的小漏洞变得大了许多。我面对的不是一个可以让你出价超过其他参与者的漏洞。我看到的是一个价值3.5亿美元的漏洞。

摘要

上午10点38分

在与Georgios核实了这个漏洞后,我让他和Dan Robinson尝试联系Sushi公司的Joseph Delong。几分钟内,Joseph就做出了回应,接着我就和Georgios、Joseph、Mudit、Keno和Omakase连线Zoom。我迅速向与会者汇报了这一漏洞,他们就离开了,以协调回应。整个通话只持续了几分钟。

上午11点10分

Joseph给Georgios和我回了一个Google Meet的房间。我加入时,Georgios正在向Joseph、Mudit、Keno和Omakase,以及Immunefi的Duncan和Mitchell汇报情况。我们很快就讨论了下一步行动。

我们有三个选择。

1.不管这个漏洞,希望没有人注意到

2.使用漏洞拯救资金,可能使用Flashbots来隐藏交易

3.通过购买剩余的分配,并立即完成拍卖来拯救资金,这需要管理员的权限。

经过一些快速的辩论,我们决定选项3是最干净的方法。我们分成不同的房间,以便分别进行通信和行动的工作。

准备工作

上午11点26分

在行动室里,Mudit、Keno、Georgios和我正忙着写一份简单的救援合同。我们决定,最干净的做法是采取闪电贷款,购买到硬上限,最后完成拍卖,然后用拍卖本身的收益来偿还闪电贷款。这就不需要预付资金,这非常好。

上午12点36分

我们遇到了一个问题。原本应该是一个简单的救援行动,现在却变成了一颗无法拆除的定时炸弹,因为还有另一个活跃的拍卖。这是一次批量拍卖,这意味着我们不能只买到硬上限,因为根本就没有硬上限。幸运的是,没有硬上限也意味着没有办法从合同中抽走以太坊,因为该限制下没有退款渠道。

我们迅速讨论了对第一份合约进行白帽救援的利弊,并最终决定,即使批量拍卖有800万美元的承诺,这800万美元也没有风险,而原始荷兰拍卖中的3.5亿美元仍有很大风险。即使有人因为我们强制停止荷兰拍卖而被影响,并在批量拍卖中发现了错误,我们仍然可以保住大部分的资金。于是团队选择继续进行救援。

下午1点36分

当我们结束了救援合同的工作时,我们讨论了批量拍卖的下一步工作。Mudit指出,有一个积分列表,即使在拍卖过程中也可以设置,并且在每个ETH承诺期间都会调用。我们立即意识到这可能是我们正在寻找的暂停功能。

我们集思广益,用不同的方法来利用这个钩子。立即恢复是一个明显的解决方案,但我们想要更好的东西。我考虑添加一个检查点,即每个原点在每个块中只能做一个承诺,但我们注意到该函数被标记为视图,这意味着Solidity编译器会使用静态调用操作码。我们的钩子将不会被允许做任何状态修改。

经过一番思考,我意识到我们可以使用积分列表来验证拍卖合约是否有足够的以太坊来匹配所做的承诺。换句话说,如果有人试图利用这个错误,那么他就需要更多以太坊来进行验证。我们可以很容易地检测到这一点并恢复交易。Mudit和Keno开始着手写一个测试来验证。

拯救行动

下午2点01分

通讯小组与行动小组合并,以同步进展。他们已经与执行加价的团队取得了联系,但该团队想手动完成拍卖。我们讨论了风险,并同意自动机器人注意到该交易或能够采取任何行动的可能性很小。

下午2点44分

执行加价的团队最终完成了拍卖,解除了眼前的威胁。我们互相祝贺,然后各奔东西。这批拍卖会将在当天晚些时候完成,没有什么大张旗鼓宣传。圈外的人都不知道我们刚刚避免了怎样的一场危机。

反思

下午4点03分

过去的几个小时感觉很模糊,几乎就像没有时间过去一样。我从偶遇该项目到发现问题只用了半个多小时,披露用了20分钟,组建行动室又用了30分钟,修复漏洞用了三个小时。总而言之,只用了五个小时就保护了3.5亿美元不落入坏人手中。

尽管没有金钱上的损失,但我相信每个人都希望一开始就不要经历这个过程。为此,我有两个主要的启示给你们。

首先,在复杂的系统中使用msg.value是困难的。它是一个全局变量,你不能改变,而且在不同的委托调用中都会持续存在。如果您使用msg.value来检查是否收到付款,您绝对不能把这个逻辑放在一个循环中。随着代码库的复杂度增加,很容易就会它发生的位置,并意外地在错误的地方进行循环。虽然对以太坊的打包和解包很繁琐,并引入了额外的步骤,但如果WETH和其他ERC20代币之间的统一接口能避免这样的事情发生,那么这个成本可能是非常值得的。

第二,安全的组件可以组合在一起,形成不安全的东西。我以前曾在可组合性和DeFi协议的背景下宣扬过这一点,但这次事件表明,即使是安全的合约级组件也可以混合在一起,产生不安全的合约级漏洞。这里没有像 "检查-效果-互动 "那样的万能建议,所以你必须要认识到新的组件会引入,将会给智能合约带来哪些额外的互动。

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

本文来自比推Bitpush.News,转载需注明出处

文章来自:https://www.bitpush.news/archives/1725019?from=listen

更新日期: 2021-08-19 01:10:07
文章标签: ,,
文章链接: 正正可以得负 —— MISO 漏洞反映出的组件叠加问题  [复制链接]
站方声明: 除特别标注, 本站所有文章均为原创, 互联分享, 尊重版权, 转载请注明.