Cream Finance何以损失1800万美元?区块链安全专家深入解析重入攻击_链圈子

在区块链短短的历史上发生过跟智能合约相关的攻击事件中,重入攻击无疑是最广为人知的一种类型,在以太坊草创时期,2016 年7 月的TheDAO 事件甚至直接造成了以太坊硬分叉为以太经典(ETC) 及现在大部分人熟知的以太坊(ETH)。在TheDAO 事件给以太坊重击之后,开发者也多了一些方法来防范重入攻击,例如,Checks-Effects-Interactions 以及 Reentrancy Guard。然而,许多重入攻击仍然持续发生,攻击的形式也从通过 fallback 函数重入同函数转变成通过不同的外部函数进入智能合约,造成合约状态混乱以达成有效攻击。本文将介绍及复现发生于 2020 年 4 月 UniswapV1 的重入攻击,2021 年 7 月发生在 BSC 上 DeFiPIE 项目的重入攻击,以及近期发生于 C.R.E.A.M. 项目的 AMP 代币重入攻击。

在进入案例分析之前,我们先介绍下重入攻击的基本概念。下面是 Solidity 网站上面介绍重入攻击给的简单案例,事实上这个 Fund 合约,就是简化过的 TheDAO 合约,在 withdraw() 函数裡我们可以看到 shares[msg.sender] 数量的 ETH 会通过 msg.sender.send() 发给 msg.sender,也就是 Fund.withdraw() 的 caller。其中 shares[] 裡头存的是每个 user 存入合约的 ETH 额度,因此在 user 成功取出 ETH 之后,shares[msg.sender] 会被清零,这个程序逻辑看起来没有任何问题。

Cream Finance何以损失1800万美元?区块链安全专家深入解析重入攻击_链圈子

然而,上述的caller (msg.sender) 可以是个恶意合约地址,如果恶意合约里写了fallback function ,则Fund.withdraw() 里的msg.sender.send() call 就可以被hijack,在这个fallback function 里如果再次调用了Fund.withdraw() 则shares[msg.sender] 数量的ETH 就会在被清零之前被多次发送给msg.sender,下面是一个示意图:

Cream Finance何以损失1800万美元?区块链安全专家深入解析重入攻击_链圈子

攻击者部署一个Evil 合约,在Evil.receive()(即fallback function)检查Fund 的ETH 足够的情况下连续调用Fund.withdraw() ,即可将Fund 合约的ETH 抽光,直到最后一次调用,shares[msg.sender] 才会被真正的清零。

这个简单的重入攻击案例有一个关键点:「清零」发生在「转帐」之后。虽然这样的写法比较符合人类的逻辑,即「确认转帐成功了,再把纪录清掉」,但是在EVM 的世界里有点不同。其实先清零再转帐也没有什么问题的,如果转帐失败了,清零的操作会自动回滚(revert)。而且把转帐放到清零之后,反而可以避免重入攻击,也就是Checks-Effects-Interactions pattern。shares[msg.sender] 是effects,msg.sender.send() 是interactions,只要所有的interactions 都在effects 之后,即使重入了Fund.withdraw() 也不会造成什么影响。

接下来,我们将介绍一个类似的案例,只是漏洞利用方式稍微复杂一点。2020 年4 月18 日下午,Twitter 上开始出现了关于Uniswap imBTC pool 被攻击的消息:

 Cream Finance何以损失1800万美元?区块链安全专家深入解析重入攻击_链圈子

Uniswap 的创始人 Hayden Adams 提到了 UniswapV1 不支持 ERC-777 并且附上了一个 ConsenSys Diligence blog 的链接。事实上,这次攻击符合 ConsenSys Diligence blog 裡的描述,而且这篇 blog 是差不多刚好一年之前写的 (2019-4-20)。

关键点在UniswapV1 的tokenToEthInput() 函数与ERC-777 token 的兼容性问题,从下面程序代码片段可以看到,tokenToEthInput() 函数基本上是符合Checks-Effects-Interactions 的写法,第208 行合约给用户发送ETH,第209 行用户给合约发送token,都在函数的最后面执行,如果从UniswapV1 本身来看是没有任何问题的。然而,DeFi 世界就像是一个金融乐高游乐场,209 行发送的token 本身也是一个智能合约,在这个合约肚子里存在一个effects after interactions 的场景。

Cream Finance何以损失1800万美元?区块链安全专家深入解析重入攻击_链圈子

下面是某个ERC-777 token contract 的transferFrom() 函数底层实现,第866 行有一个callback interface 可以用来通知holder ,只要holder 是一个合约地址,并且按照ERC-1820 注册了tokensToSend() 函数。而第868 行的_move() 才是真正更新token balances 的地方。因此,如果攻击者在_callTokensToSend() 时重入了UniswapV1 的tokenToEthInput(),可以造成UniswapV1 pool 本身token balance 增加之前,多次兑换成ETH。即第204 行的token_reserve 永远不变。

Cream Finance何以损失1800万美元?区块链安全专家深入解析重入攻击_链圈子

简单的说,在重入攻击发生的情况下,第204 取出的token_reserve 可能跟上一层调用是一样的,在Uniswap xy=k 的设定下,如果可以用同样的token_reserve 多次交易,等于是可以持续用较高的价格卖出token 把流通性提供方(LP) 的代币消耗殆尽。

下面是我们利用eth-brownie 回到案发之前的2020-2-15 区块高度9488451 复现这次攻击的程序代码:

Cream Finance何以损失1800万美元?区块链安全专家深入解析重入攻击_链圈子

首先是通过ERC1820 合约注册tokensToSend() callback function,注册完成之后所有兼容ERC-777 的token transfer 发生时,如果目标地址是攻击合约本身,则合约的tokensToSend() external function 会被调用。接下来介绍攻击发起函数trigger():

Cream Finance何以损失1800万美元?区块链安全专家深入解析重入攻击_链圈子

上面这短短10 行代码只做了四件事,第38 行将ETH 换成token,第39 行将上一步换出来的token 又换回ETH,第40 行将一部分ETH 换成token,第43-44 行将所有的ETH 及token 转给owner,也就是攻击者钱包地址。其中,第39 行有一个比较特别的点,只有1/32 的token 被换回ETH,按照这样的写法肯定是会亏钱的。其实另外的31/32 置换是在上述的callback function 里头完成,程序代码如下:

Cream Finance何以损失1800万美元?区块链安全专家深入解析重入攻击_链圈子

从上面的程序代码可以看到entry 会计算现在是第几次进入tokensToSend() 然后在第57 行完成另外31 次兑换,每次也是1/32 的token balance。通过这31 次重入,攻击者可以用较好的价钱卖出token 并且破坏UniswapV1 pool 里的平衡状态,即xy=k 的k 值改变,因此最终pool 里的ETH 会变得很少token 很多,ETH 相对于token 的价值极高,所以上面trigger() 函数的第40 行,攻击者可以用很少的ETH 把大部分pool 里的token 买回来。下面是攻击代码执行的结果:

Cream Finance何以损失1800万美元?区块链安全专家深入解析重入攻击_链圈子

原本pool 里头有718 ETH + 19.59 imBTC,攻击完成之后只剩下0.013 ETH + 0.019 imBTC,几乎是掏空了pool。

上述UniswapV1 + ERC-777 的例子其实跟TheDAO 的案例类似,都属于同一个函数的重入,下面介绍一个多函数参与的案例,是近期发生在BSC 上的DeFiPIE 攻击事件。在第一眼看到DeFiPIE 代码时,有一种熟悉感,与老牌DeFi 项目Compound 有87% 的相似度,直觉联想起了2020-4-19 的Lendf.Me $25M Better future事件,仔细分析之后发现,问题的根源确实如出一辙,都是通过重入攻击造成内部记帐错误,达成获利。

Cream Finance何以损失1800万美元?区块链安全专家深入解析重入攻击_链圈子

从上面DeFiPIE 的PToken 合约程序代码片段中可以看到,borrowFresh() 函数会在把资产发给borrower 之后才将因为这次借款造成的状态改变写入合约的storage,所以又是一个effects after interactions 的案例。由于借款的上限取决于抵押资产的价值,正常情况下,某一次借款把额度用完之后,在归还借款之前应该就借不出任何资产了。但由于上述情况数据没有及时更新,重入后的第二次借款仍然可以使用跟第一次借款发生前一样的额度,因此理论上是可以无限嵌套,多次利用有限额度,最终攻击者通过清算自己以较低成本创造的负债获利。

在Lendf.Me 事件中,攻击者是通过imBTC 的ERC-777 内建机制拦截transferFrom() 完成重入攻击。在DeFiPIE ,对于token 本身并没有任何限制,可以随意创建token 合约纳入借贷体系。如上图所示,任何人都可以创建一个恶意的EvilToken 并且人工制造一个拦截transfer() 的机制以达成重入攻击,下面介绍我们如何reproduce 针对DeFiPIE 的攻击,由于这个攻击比较复杂,我们会依序从各个模块介绍,最后介绍如何组装使用。

Cream Finance何以损失1800万美元?区块链安全专家深入解析重入攻击_链圈子

先从恶意token contract 开始,现在要写一个ERC20 合约基本上只要继承OpenZeppelin的template自行修改 token name 以及 symbol 就行。在上面的X token contract 里可以看到,第233 行的transfer() 我们加入了一个开关optIn,在开关打开的情况下(optIn == true),Lib.shellcode() 会被调用执行重入攻击任务,这就是上面说到的人工创建拦截transfer() 的机制。其他如mint(), setup(), start() 就是一些方便使用的外部函数。

Cream Finance何以损失1800万美元?区块链安全专家深入解析重入攻击_链圈子

第二个模块是Lib.shellcode() 函数,也就是上述transfer() 被拦截后发起重入攻击的地方,在这次模拟中,我们嵌套了三层,依序调用了自行创建的PToken (pX[1], pX[2]) 并且在第三层从pBUSD 真正的借出了21,000 BUSD,在这过程中实现了「三个坛子一个盖」。

Cream Finance何以损失1800万美元?区块链安全专家深入解析重入攻击_链圈子

第三个模块是获利的关键,清算者(Liquidator)。在上面的Liquidator.trigger() 函数可以看到,清算者使用x 代币调用pX 合约的liquidateBorrow() 获取质押品colleteral(即pCAKE),随后在第66-67 行将pCAKE 换成CAKE 并转给owner(即Lib 合约)。mint() 函数的作用是提供足够的x 给pX 合约,让上述Lib 合约能够调用pX.borrow() 借出资产。

Cream Finance何以损失1800万美元?区块链安全专家深入解析重入攻击_链圈子

接下来就是组装上面三个模块搭配闪电贷取得获利,首先是创建三个X tokens 及Lib 合约。Lib 合约的constructor 创建了Liquidator 合约。第272-278 行铸造了X tokens 给Liquidator 及Lib,第280-284 行将X tokens 与Lib 互相关联上。第285 行触发Lib 合约启动后续流程,最后在第288 行将获利的WBNB 转给owner(即攻击者钱包地址)。

Cream Finance何以损失1800万美元?区块链安全专家深入解析重入攻击_链圈子

Lib.trigger() 实际上就做了一个两层的PancakeSwap 闪电贷,第116 行可以看到154.5 WBNB 被借出,在回调函数pancakeCall() 里又借了2,900 CAKE。主要的攻击流程在pancakeCall() 的后半段。

Cream Finance何以损失1800万美元?区块链安全专家深入解析重入攻击_链圈子

在进入第二层pancakeCall() 时,就是真正攻击流程的开始,首先是使用x[0], x[1], x[2] 这三个X tokens 创建三个pToken (pX[0], pX[1], pX[2])。要创建pToken 需要预先在Uniswap 创建交易对并且注入流通性(第136-142 行),pX[i] 创建完毕后,即可取出流通性(第149 行)以方便重复使用前面借出的WBNB,最后触发Liquidator 存入足够的x[i] 让pX[i] 能够被borrow()(第152 行)。

Cream Finance何以损失1800万美元?区块链安全专家深入解析重入攻击_链圈子

第二步是触发pX.borrow() 前的准备工作,第156-162 行调用了Controller.enterMarkets() 将pX[0], pX[1], pX[2], pCAKE 等pToken 纳入DeFiPIE 体系,以便后续操作。第166 行将前面闪电贷借出的2,900 CAKE 全数注入pCAKE 合约充当后续借贷的抵押品。

Cream Finance何以损失1800万美元?区块链安全专家深入解析重入攻击_链圈子

第三步打开x[0], x[1], x[2] 的transfer() 拦截机制(第170-172 行),并且触发pX[0].borrow(),由于上述Lib.shellcode() 的作用下,最终会拿到21,000 BUSD,并且创造了不良资产。

Cream Finance何以损失1800万美元?区块链安全专家深入解析重入攻击_链圈子

第四步触发Liquidator 清算不良资产,获得CAKE。

Cream Finance何以损失1800万美元?区块链安全专家深入解析重入攻击_链圈子

清偿完闪电贷后,在测试环境中最终获利66 WBNB。虽然数额不大,但这个案例涉及到代币合约,清算合约等较复杂的漏洞利用过程,值得研究分享。

2021 年8 月30 日下午,就在这篇文章完稿之际,CREAM 项目传出了遭遇攻击损失 1,800 万美元 。笔者短暂分析攻击交易后发现这次攻击与上述DeFiPIE 遭遇的攻击手法极其类似,决定复现此案例并加入本文。

Cream Finance何以损失1800万美元?区块链安全专家深入解析重入攻击_链圈子

漏洞的原理其实不需要赘述,跟DeFiPIE 基本是一样的,攻击者通过AMP 代币自身的回调机制实现了「两个坛子一个盖」,用同一笔ETH 质押品借出了AMP 及ETH,最终通过另一个合约清算自己的不量债务获利。下面直接介绍攻击合约的各个模块以及最后的组装使用:

Cream Finance何以损失1800万美元?区块链安全专家深入解析重入攻击_链圈子

首先是注册callback function,跟前面UniswapV1 的情况类似,攻击者通过ERC-1820 合约注册一个tokensReceived() 函数,当有人往攻击合约发送AMP tokens 时,callback function 会被触发。

Cream Finance何以损失1800万美元?区块链安全专家深入解析重入攻击_链圈子

而callback function 本身就是一个针对crETH 合约的borrow() 调用,攻击者的预期是在crAMP.borrow() 的调用过程中利用同样的抵押品再借一笔ETH。

Cream Finance何以损失1800万美元?区块链安全专家深入解析重入攻击_链圈子

第三个模块是Liquidator 合约,与上述DeFiPIE 的Liquidator 类似,在上图Liquidator.trigger() 函数里,攻击者用AMP 清算了自身创造的不良资产获得crETH 抵押品(第60 行),随后将crETH 换成ETH(第61 行),并发回给owner,即攻击合约。

Cream Finance何以损失1800万美元?区块链安全专家深入解析重入攻击_链圈子

最后就是组装执行攻击了,上图是Exp.trigger() 函数,在第94 行先是一个UniswapV2 的闪电贷,借出了500 WETH,后面的uniswapV2Call() 函数才是真正的流程。

Cream Finance何以损失1800万美元?区块链安全专家深入解析重入攻击_链圈子

首先是一些准备工作,由于闪电贷借的是WETH 而crETH 需要使用ETH 才能铸造,因此在第105 行,先将WETH 换成ETH,接下来将换出的ETH 全数发给crETH 合约铸造出crETH cTokens。与前面DeFiPIE 攻击一样,需要调用一次Comptroller.enterMarkets() 将crETH 激活以便后续的操作。

Cream Finance何以损失1800万美元?区块链安全专家深入解析重入攻击_链圈子

第二步就是利用上面存入的500 ETH,借出AMP tokens,在crAMP.borrow() 的过程中crAMP 合约把AMP 转给攻击合约,由于前面ERC-1820 的机制,这次转帐会被拦截并另外借出355 ETH。

Cream Finance何以损失1800万美元?区块链安全专家深入解析重入攻击_链圈子

第三步通过Liquidator 合约清算债务,将部分质押品取回。从上图可以看到攻击者将前面借出的一半AMP 发给Liquidator,换回足够支付闪电贷的ETH,保留剩下的AMP。

Cream Finance何以损失1800万美元?区块链安全专家深入解析重入攻击_链圈子

最终将ETH 都换成WETH 支付闪电贷后,带走41 WETH + 9.74M AMP。

Cream Finance何以损失1800万美元?区块链安全专家深入解析重入攻击_链圈子

若以「币圈一天,人间一年」给区块链世界计时,重入攻击算是上古时期的物种了,开发者还需多从历史上发生过的案例中吸取经验,形成肌肉记忆,避免受到伤害。

原创文章,作者:惊蛰财经,如若转载,请注明出处:https://www.xmlm.net/jibi/31709.html