LOADING...
LOADING...
LOADING...
当前位置: 玩币族首页 > 新闻观点 > [BlockSecDeFi攻击系列之六]终而复始:Uniswap重入事件

[BlockSecDeFi攻击系列之六]终而复始:Uniswap重入事件

2021-08-27 BlockSec 来源:区块链网络

去中心化金融(DeFi)作为区块链生态当红项目形态,其安全尤为重要。从去年至今,发生了几十起安全事件

BlockSec作为长期关注DeFi安全的研究团队(https://blocksecteam.com),独立发现了多起DeFi安全事件,研究成果发布在顶级安全会议中(包括USENIX Security, CCS和Blackhat)。在接下来的一段时间里,我们将系统性分析DeFi安全事件,剖析安全事件背后的根本原因

往期回顾:

(1) [BlockSec DeFi攻击分析系列之一] 我为自己代言:ChainSwap攻击事件分析

(2)?[BlockSec DeFi攻击分析系列之二] 倾囊相送:Sushiswap手续费被盗

(3)?[BlockSec DeFi攻击分析系列之三] 偷天换日:深度剖析Akropolis攻击事件

(4)?[BlockSec DeFi攻击分析系列之四] 表里不一:Sanshu lnu 的 Memestake 合约遭袭事件分析

(5) [BlockSec DeFi攻击系列之五] 以假乱真:DODO V2 众筹池遭袭事件分析

如果能重来,你会做什么?

本期简述了一个意外发现时空暗道的毛贼,如何戏弄守护在秘宝洞口的独角兽,将财宝窃于囊中的魔幻故事

阅读建议:

如果您初识 Defi,又有耐心的话,可以从头开始阅读,酌情跳过废话

如果您对AMM、ERC777、Uniswap等非常了解,可以直接从0x1中Uniswap重入部分开始

文章较长,看不下去,记得点个关注再走喔~

正文

时间:2020-4-18. 8:58.?#9893295

【注】imBTC 是 tokenLon 发行的与 BTC 价值 1:1 锚定的 ERC777 标准代币

相关代币的价值情况:

imBTC ($7029.38) : ETH ($178.81) = 39.31

0x0. 背景介绍

0.0 AMM

AMM类型的交易所解决的痛点是:区块链上代币的有效交换

俗话说的好:「哪里有痛点,哪里就有钱赚」。有很多人愿意掏钱(手续费)来使用代币交换这个服务

AMM一方面用这些手续费吸引玩家向资金池投钱,资金池有了钱就可以通过AMM实现代币交换;另一方面,由于套利者的存在,池子代币交换的价格与市场价格一致

这样,提供流动性的玩家赚到了手续费,套利者赚到了差价,用户得到了代币有效交换这一服务。三个角色缺一不可,构成这一系统。一拍即合,各自欢喜

其中AMM有几种性质,最广为人知的就是:交易池中底层代币(Underlying Token)的储备量满足一定的不变式,比如Uniswap的恒定乘积 (reserve0 * reserve1 = k)

但其实还有很多隐藏的性质?(伏笔1),想知道吗?哎,我就不说,想知道就自己继续看下去!

0.1 ERC777

提出时间:2017-11-20

ERC777是对ERC20的"升级"

它会在代币转移 (balance加减) 之前回调TokensToSend函数,转移之后回调TokensReceived函数

TokensToSend函数由转移代币的持有者 (可以是合约) 实现,TokensReceived由转移代币的接收者实现,这给了用户很大的自由,但也带来了一些问题,比如本次的攻击

0x1. 攻击分析

1.0?经典重入攻击

我们先不急着去看攻击过程,先复习下最简单的重入攻击(例如: The DAO,LendfMe等事件)

在这些"经典"攻击中,攻击者通过重入可以不断的使合约对其转账,直到退出"递归"时才更新一次的状态,他可能转账了1000个 Token (50个 * 20次),但是 balance 却只减少了50

如果攻击者可以在 transfer 的过程中重新调用 withdraw 函数,就可以实现重入。主要原因在于:合约中转账等操作先于余额状态的更新

? 总结

简单来说,重入攻击就是打断施法,重点在于:

①?在哪里打断施法

②?打断以后又做了些什么可以影响后续的结果

重入攻击有一个重要的特征,就是:先转账,后更新状态

对于上面这种传统的重入攻击,打断的便是「转账+记账」这一组合技,做的事情就是不断重新转账,以影响后续的记账结果

1.1 重入Uniswap

(前方重点!)

那 Uniswap 如何重入呢?我们知道,Uniswap 是一个去中心化交易所(DEX),用户可以在上面交换代币

【补充】UniswapV1 只实现了ETH和任意 token 之间的交换,对于 token 与 token 的交换,可以借助ETH中转来实现

这并不像传统重入攻击的「转账+记账」模式。那它可以在哪里打断施法,又可以做哪些事情影响后续的结果呢?

代码分析

Uniswap 交易对合约中的交换函数(例如: ethToToken, TokenToToken...),原理基本一致,即保证交易池(交易对合约,后简称交易池)内两种币数量的乘积恒定(不考虑 Fee 的情况下[注1]),这些函数会先调用 getInputPrice 方法获取可以购买的另一种代币数量:

对应的公式为:

这里公式表示:池中原来储备量为?ether : token?,现在alice手里有?token(put)?个 token,ether(get)?代表她能从池中买到多少个ETH

我们现在直接挑其中一个开锤,比如 tokenToETH (这个函数的功能是用 token 换 ETH):

我们可以看到这个函数先将 ETH 转给用户,再调用 transferFrom 收取用户的代币(代码第8行和第10行)

我们是否可以打断这两笔转账呢? 对于普通的 ERC20 代币,确实是没有办法打断转账的过程,但是还记得吗?我们提到的 ERC777 代币,这种复杂的代币,恰恰提供了这样的暗道,使不怀好意之人,有了可乘之机

现在想法很简单了,如果 Uniswap 存在一个 ETH-ERC777 的池,我们就可以利用 ERC-777 的回调功能,在 transferFrom 的过程中,重入这个函数,继续发送 (send) 一笔 ETH 给自己

这时可能有聪明的读者要问了:「即使重入后又转了一笔 ETH 给自己,后面"递归"返回后,不是还要为每轮重入所购买的 ETH 付相应的 token 吗?」没错,是这样的,如果只是简单的重入这个函数,只是把一次购买(token → ETH),变成了多次购买,毛都赚不到

更聪明的读者可能现在已经想起来,之前我们提到的?Uniswap 的计价公式,由 ERC-777 的特点,我们可以知道重入是发生在 ETH 之后,token 余额变更之前,这就意味着,在重入过程中计价公式的变量状态其实是不一致的(ETH 的 reserve 更新了,但是 token 的 reserve 还未更新),攻击者正是利用这一点,每次薅一点羊毛,直到把人家羊给薅秃了:

从公式中可以看到,本来在一次 swap 后,token 和 ETH 的状态会同时变化(以维持乘积恒定),但是由于重入发生在发送?ETH?和更新 token 余额之间,直接被打断施法了,从而造成了悲剧

很简单的道理:如果正常的两次调用,第二次是 token↑ 使得 etherget?↓,但是由于重入后状态没有更新(token 没变),所以相比"正常情况"下可以获得更多的 ETH

【注1】公式相关推导过程(基本原理就是:保证交易池中两种代币一直满足恒定的乘积

【注2】可能读到这里, 你还是感觉哪里不对, 这是正常的, 如果有兴趣, 你可以思考这样几个问题:

1) 这样一定能获利吗, 需要满足什么条件吗?

2) 攻击者获利是最优的吗, 还可以怎样优化?

在深入分析部分, 小编对这些问题做了一些简单的尝试, 如果有兴趣, 不妨继续看下去

(这已经是小编第4次复盘这次攻击, 但还觉得很多问题没有真正的搞清楚, 所以如果你没看懂, 那也没什么大不了的)

? 总结

总结来说,这次的攻击是由于:

① UniswapV1不兼容ERC777代币 → ② 从而导致合约代码可重入 → ③ 从而导致恒定乘积中变量状态不一致 → ④ 从而导致交易池资金被薅走

1.2 Real World

原理大概就是这样,管你听没听懂,继续看就完了,下面我们来看看real world中攻击者到底做了什么?

其实说到现在,更更聪明的读者,都可以跑去自己攻击了(友情提示:小心警察叔叔找上门哦

我们随便找一笔攻击者的Tx:0x32c83905db61047834f29385ff8ce8cb6f3d24f97e24e6101d8301619efee96e

可以看到攻击主要分为两个部分:

首先是一堆的自毁合约,看起来比较迷惑,但是查看这些自毁合约的调用者(GST[注1])就可以知道这是为了节省攻击的Gas(与攻击本身关系不大)

攻击过程:

step 1: 使用 1 ETH 向 Uniswap(imBTC) 换取 imBTC

step 2: 将换得的 imBTC 分两次(一次一半),向 Uniswap(imBTC) 换回 ETH(其中第二笔是重入所得),通过简单的计算我们可以知道:0.611341052127704463 + 0.472375805535296596 = 1.0837168576630012 > 1?通过这种方式来薅羊毛

step 3: 最后将收益从攻击合约转给攻击者自己(1.0837168576630012 - 1 =?0.08371685766300119(一笔攻击的获利))

【注1】GST (GasToken):是一个旨在节省Gas的代币,我们知道Ethereum有一个特性就是销毁合约时会返回大量的Gas,所以GST的原理就是:在Gas Price便宜的时候,用户可以通过这个合约生成一系列子合约,来"存储Gas"(同时Mint出相应的GST代币,代币用户存储了多少单位的Gas),当需要时再用GST调用合约销毁当时创建的子合约换取相应的Gas

[GST2]:?0x0000000000b3f879cb30fe243b4dfee438691c04?(https://gastoken.io/)

1.3 Misc

这次的攻击事件,攻击者"或许"不是第一个发现漏洞的人

Uniswap 交易对合约中的重入漏洞,早在?2019年1月12日?ConsenSys 的审计报告中就被提及,而且在?#14 commit?中提到:合约中可能存在多种方式的重入攻击(包括利用 ERC-777 标准代币),并给出了简单的攻击过程

审计报告中提出:对于 UniswapV1 交易对合约中的 exchange 类型函数,无论 transfer 是发生在 token 余额状态变更前,还是 token 余额状态变更后,如果 transfer函数 可以重入,都会造成损失,并给出了后一种情况的简单攻击过程模拟

【补充】利用 ERC-777 重入属于前一种,重入发生在状态变化前(还记得上面我们提到的,ERC-777 代币转移的过程吗,_callTokensToSend?是发生在?_move?之前的 |?回收伏笔1),审计报告中还指出,相比第二种情况,利用 ERC-777 来攻击会更简单

"If token balances are updated after the reentrancy (e.g. ERC-777), the algorithm is even easier and requires fewer funds to steal liquidity pool."

https://github.com/ConsenSys/Uniswap-audit-report-2018-12#31-liquidity-pool-can-be-stolen-in-some-tokens-eg-erc-777-29

0x2. 附录

a. 攻击者是否获利最大化,如何获利更多?

这是一个比较困难的问题

从直觉上来看:攻击者每笔攻击交易重入的次数越多,使用的Ether数额越大,获利就越多,但是还要考虑实际交易对中真实的情况

因此小编只是做一些简单的尝试与统计:

优化的维度有:初始时攻击者投入的Ether数量,投入Token占比,重入的深度、攻击次数

这些都可以在数学上求解,但是小编懒(bu)得(hui)搞,有兴趣的大佬可以尝试

实验条件:区块号 #9893295, 工具 brownie

实验1:获利与投入ETH数量及投入Token占比的关系

实验参数:使用ETH的数量 [1, 3, 5, ... ,19],投入Token的占比 [1/20, 3/20, 5/20, ... 19/20]

【注】这里的token占比指的是:还记得凹函数这一性质吗,前半段下降快于后半段,这里实验的是前半段与后半段的比例对获利的影响,其中占比指的是前半段占全部的比例

结论:投入ETH的数量越大,获利越大,并且增长的幅度也会有所加大。投入token的占比在0.5时接近最大值

实验2:获利与攻击次数的关系

实验参数:分别使用100 ETH / 累计 ETH两种方式,尝试增加攻击次数

我们知道随着攻击次数的增加,池中状态会一直向曲线的左侧移动,也就是说随着攻击次数的增加,获利会逐渐增大

上面两图是两种不同的方式,上图每次使用固定的100ETH进行攻击,下图初始用100ETH攻击,后续每次使用的ETH会累积上之前的获利。很显然累积上获利使池子更快的被掏空(40 次 / 175次)

结论:随着攻击次数的增加,获利会以指数趋势增加

实验3:重入次数与获利的关系

实验参数:重入次数取 [2, 40],使用100 ETH

结论:随着重入次数的增加,理论上获利是会更多的,但是增长的幅度逐渐趋于平缓

【注】重入次数与token占比是关联的,比如重入2次,token占比为0.5 ...

同时还需要考虑gas limit等条件,所以攻击者选择重入2次,token占比0.5,还是有道理的

b.?本次事件涉及的攻击Tx有哪些 (时间范围)?

通过使用我们的内部工具与数据集,得到结果如下:

对于Attacker (0x60f3fdb85b2f7) 来说,攻击Txs涉及的区块范围为:9893295 - 9894249?共954块

c.?攻击机会何时开始存在?

攻击者是否发现的足够早(攻击者之前是否存在攻击机会)?

UniswapV1的 imBTC 池在 #9059910 被创建出来,攻击开始于 #9893295

d.?本次事件后续结局如何?

通过使用我们的内部工具与数据集,得到结果如下:

在?#9894379?块 (2020-4-18 12:49:50):0xb9e29984fe506 向 imBTC 合约发送一笔Tx (0x7ce097c5149),调用其 pause 方法[注1]关停合约(禁止转账等)

【注1】pause的实现方式很简单,利用一个全局标志变量 _pause,对每个转账函数加一个modifier来修饰,当这个标志为true时,revert掉

在?#9895526?块 (2020-4-18 16:57:55):0xb9e29984fe506 向 imBTC 合约发送一笔Tx (0xced24b64665b9),调用 unpause 方法,解冻 imBTC 合约,恢复正常交易

0x3. 安全建议

道路千万条,安全第一条,这里小编给出一些安全建议,各位大佬权当参考:

1.? 对于重要函数(修改一些重要Storage变量),建议使用一些防止重入的方法,如lock(比如Openzeppelin中提供的ReentrancyGuard等方法)

2. 合约代码尽量满足:Checks-Effects-Interaction 模型

3. 项目上线前应做好审计工作,并不断迭代修改。审计方和项目方,是相互促进的关系。像本次事件中,审计中指出的错误,时隔一年被攻击,岂不是很尴尬

4. 应提前考虑好兼容问题,保证合约代码的完备性。比如 通缩/通胀 代币、ERC777代币等比较特殊的代币模型,都应尽可能的考虑与规避风险

0x4. 参考

imBTC Uniswap Pool Drained for ~$300k in ETH:?https://defirate.com/imbtc-uniswap-hack/

Openzeppelin PoC:https://github.com/OpenZeppelin/exploit-uniswap#exploit-details

https://medium.com/imtoken/about-recent-uniswap-and-lendf-me-reentrancy-attacks-7cebe834cb3

详解 Uniswap 的 ERC777 重入风险:https://paper.seebug.org/1182/

https://medium.com/imtoken/about-recent-uniswap-and-lendf-me-reentrancy-attacks-7cebe834cb3

查看更多

—-

编译者/作者:BlockSec

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

LOADING...
LOADING...