LOADING...
LOADING...
LOADING...
当前位置: 玩币族首页 > 币圈百科 > BSC闪电贷攻击序曲:详解与复现斯巴达协议漏洞

BSC闪电贷攻击序曲:详解与复现斯巴达协议漏洞

2021-06-03 AmberGroup 来源:区块链网络

本文作者:Amber Group区块链安全专家吴家志

事件摘要:5月1日深夜,与以太坊一样运行着 EVM (虚拟机)兼容智能合约的 BSC(Binance Smart Chain,币安智能链),遭遇了第一次闪电贷攻击。以Amber Group 区块链安全专家吴家志博士为代表的团队复现了这个造成三千万美元损失的攻击事件,但这一事件只是一连串攻击的序曲。

主角:斯巴达协议

该事件的主角是 BSC 上的一个合成资产协议 Spartan Protocol(斯巴达协议),简言之,它使用了Uniswap的代码,同时改造了 UniswapV2 的收费模式,让流动性提供方在流动性稀缺时能获得更多的奖励,相应的,用户在交易较大数额资产时,会被收取较多的手续费。各种 BSC 上的主流代币(如 BNB)都有对应的 Sparta 合成资产,也就是按BNB:Sparta的价值以 ?50:50 创建的 UniswapV2-like 池子(类似于UniswapV2的资金池)。继承了 UniswapV2 的特性,这些池子也允许任意账号注入或移除流通性,例如 Alice 可以放入等值的 BNB 与 Sparta 后调用 addLiquidity() 触发智能合约铸造 SPT1-WBNB 代币作为注入流通性的证明(或称 LP token),这个证明可以在日后用来移除流通性并获取交易手续费的分成。

根源:第385行代码

与大部分使用闪电贷触发的攻击不同,Spartan Protocol 的问题不是因为预言机的价格被操控,而完全是在业务逻辑实现上的一个漏洞被利用所导致的。闪电贷扮演的角色只是把这个漏洞造成的影响放大。具体而言,是在移除流通性的实现里,有一个场景没有考虑到。

从下面的代码我们可以发现,outputBase 与 outputToken 会通过 Util 合约的 calcLiquidityShare() 计算出来,在第244行销毁指定数额(units)的流通性证明后,第245–246行将上面计算出的资产数额打给 member ,也就是流动性提供者。

问题的核心在 Utils 合约的 calcLiquidityShare(),按照 Uniswap 基本原理来理解,移除一个份额的流通性所获得的资产数额应该就是池子里资产总额除以已经铸造出来的流通性证明总额。例如 Alice 注入了 100 BNB + 100 Sparta,换取了 100 个 SPT1-WBNB 代币。此时 Alice 如果要用 10 个 SPT1-WBNB 取出资产,由于 10 SPT1-WBNB 占总量的 10%,所以 10 BNB + 10 Sparta 会被取出。下面代码的第 385 行就是计算了池子里 token 的总量 amount,搭配第 386 行 LP token 的总发行量 totalSupply 以及待销毁的 LP token 份额 units 计算出应发还的资产数额: amount*(units/totalSupply)。

这段看似简单明了,人畜无害的代码怎么就出问题了呢?

开发者疏忽了一点,第 385 行的 amount 是可以被操控的,只要往合约打钱就行。先不管打进去的是谁的钱,当你手上握有若干份额的流通性证明(例如 10%),当池子里的资产增加了,但 LP token 没有增加,你的 10% 自然能换回更多钱。而恰巧,打进去的钱还能无损的取出来,这便形成了黑客获利的闭环。

让我们先看看 Uniswap 是怎么做的,下面的 burn() 是 UniswapV2Pair 合约里用来移除流通性的函数,用户在大部分情况下会通过 router 合约将一定数额的 LP token 打入 Pair 合约,然后调用 burn()。在第144–149行可以看到类似的逻辑,_token0 及 _token1 的数额根据用户销毁的 LP token 份额分配给流动性提供者。

有一个关键的不同点在第153行的 _update() 调用,在下方代码第82–83行可以看到,当前的资产余额 (balance0, balance1) 会被同步到 Pair 合约的储备额里 (reserve0, reserve1)。因此,即使攻击者刻意在 burn() 之前给 Pair 合约打钱,这些资产会被纳入池子里,按比例分配给持有 LP token 的账号。

回到 Spartan 的 Pool 合约,由于 removeLiquidity() 时没有及时将合约的资产余额 (balance) 同步到储备额 (baseAmount, tokenAmount) 里,导致攻击者可以在给 Pool 合约打完钱后,通过 addLiquidity() 得到 LP token,再调用 removeLiquidity() 将这笔钱无损取出。细心的读者应该能发现,在removeLiquidity() 的 243 行,有一个类似 Uniswap 的 _update() 调用,其实是有机会避免这个漏洞被触发的。

可惜的是,_decrementPoolBalances() 并没有检查 BASE 及 TOKEN 的 balances,仅仅将储备额额 (baseAmount, tokenAmount) 减掉。

因此,在 addLiquidity() 时,稍早打入的 BASE和TOKEN 资产,通过当前余额与 (baseAmount, tokenAmount) 的差被计算出来,并且当作新增的流通性。下面的代码可以看到第 224–225 行计算出了 (_actualInputBase, _actualInputToken),这两个数字在第 226 行被用来计算 LP token 的数额 liquidityUnits。

以上述 224 行调用的 _getAddedBaseAmount() 为例,第 274 行取出了当前的 BASE 代币余额,在第 276 行扣掉储备额 baseAmount 后返回。

复现:“掏空”资金池

要利用这个漏洞获利,只需要 4 个简单的步骤: 1) 注入流通性; 2) 给 Pool 合约打钱,放大步骤#1取得的 LP token 能兑换的资产数额; 3) 取出流通性,此时能取出的资产会比步骤#1注入的多; 4) 将步骤 #2 打入合约的资产当作流通性注入,并立即取出。

上面的步骤有一个关键词“放大”,直接的联想就是闪电贷了,如果能在步骤 #1 之前先弄到一大笔钱,就能把步骤 #2 做得更好,获利更多,我们仿照攻击者使用 PancakeSwap 的 flash-swap 作为闪电贷,如下:

在第 37, 39 行可以看到,我们直接借出受害的 Spartan Pool 三倍的资产(例如 WBNB),通过 swap() 调用,我们可以在 pancakeCall() 回调中实现上述的漏洞利用步骤。新手黑客要注意一点,swap() 的第三个参数 “brrrr” 必须写入任何不为空的字符串,否则 pancakeCall() 回调不会触发,详细可以参考 UniswapV2Pair 合约。

在获得大量的 WBNB (即 victimAsset )后,第一件要做的事就是购入 sparta 代币。吴家志团队刚开始分析这个攻击时不解的是,为何要多次小量购入 sparta 而不是一次买到足够的量,随后发现是前面说到的手续费机制所导致。如果一次兑换的量过大,被收取较多的手续费,就会抵消掉活力。因此,可以看到 pancakeCall() 第 46–52 分了 5 次购入 pool 内 1/3 的 sparta 代币。接下来,在第 59–61 行将流通性注入。

下一步,给合约打钱。基于节省手续费的考虑,第 67–70 行分 10 批购入 sparta,搭配对应的 WBNB 直接 transfer 给 Pool(第 77–78 行):

而后,在第 81 行将 LP token 全数打入 Pool 合约,第 82 行的 removeLiquidity() 调用即可取出放大过的 wbnb + spartan:

最后的步骤,将用来放大的资产当流通性注入后(第85行),立即取出(第86–87行):

此时,Pool 合约的大半 WBNB 已经被掏空,攻击者手上剩余的 sparta 代币也直接原地变卖成 WBNB:

最后,把最早借的闪电贷归还,PancakeSwap 的利率是 0.25%,可以看到下面将需要归还的数额 (retAmount) 计算出来,在第 106 行确认有获利的情况下,归还贷款,结束这一轮的攻击。这也是 EVM 类智能合约的特性,在没有获利的情况下,可以回滚整个交易,发起者只损失了手续费。

由于不能一次将 Pool 掏空,吴家志团队重复了上述流程 20 次(一个交易 4 次,连续触发 5 个交易),差不多能取出 Pool 90%+ 的 WBNB。

序曲:风险仍然存在

上一段最后一张图可以看到,吴家志团队模拟攻击的区块高度是 7,115,815(eth-brownie 的 forked-chain 会自动 +2),对照 BscScan 的时间是 May-04–2021 01:07:27 AM +UTC:

但这攻击不是5月1日晚上发生的吗?没错,这个漏洞并没有被立即修补,吴家志团队在北京时间5月3日下午复现完攻击后发现,包括 WBNB, BUSD, CREAM, ETH, BURGER, XRP, DOT, LINK, RAVEN 等多个池子价值超过10万美元的资产还存在风险。北京时间5月3日傍晚 5:57,吴家志团队联系到 Spartan Protocol 项目方告知风险,下午6:16项目方发出这个交易更新了 Utils 合约,阻断了可能发生的后续攻击,但这个更新也同时让流动性提供方无法取出流通性。

北京时间5月4日上午8:28,Amber Group区块链安全团队的监控系统发现 Utils 合约再度被更新,而且是回滚到有问题的版本,也就是说这些资金再度陷入风险,约 40 分钟后在区块高度 7,115,816,正确的补丁才被补上,原因不明。这也是为何上述的攻击最晚能够触发的区块高度是 7,115,815。

后续与项目方的沟通中,Amber Group区块链安全专家吴家志博士的团队协助搭建了复现攻击的环境,并提供攻击代码协助确认最终的 Utils 合约已无法被攻击。由于后续的赔偿需要基于攻击发生前的快照发出项目方保留的代币,吴家志团队也协助了项目方连接 BSC archive node,使用正确的状态生成快照,并于5月24日受到了Spartan项目方通过网络公告的公开致谢。

—-

编译者/作者:AmberGroup

玩币族申明:玩币族作为开放的资讯翻译/分享平台,所提供的所有资讯仅代表作者个人观点,与玩币族平台立场无关,且不构成任何投资理财建议。文章版权归原作者所有。

LOADING...
LOADING...