只读型重入漏洞介绍及案例分析
Eocene Research
2023-05-10 18:32
订阅此专栏
收藏此文章
最出名的一次重入漏洞利用是 2016 年的 The DAO 攻击事件,这次攻击事件导致了以太坊的分叉。


撰文:Eocene Research


概述


在众多的智能合约安全问题中,重入漏洞一直是危害性最大,发生频率较高的漏洞。该漏洞是由于合约在处理函数调用时没有适当地管理状态变量的更改顺序而引起的。最出名的一次重入漏洞利用是 2016 年的 The DAO 攻击事件,这次攻击事件导致了以太坊的分叉。目前由于开发人员安全意识的提高,多数项目都使用了重入锁防止重入漏洞,减少安全风险。但是在 DeFi 领域中,尽管合约在某些关键函数使用了重入锁,却也仍然有可能遭受到只读型重入漏洞的攻击。


只读型重入漏洞介绍


只读型重入漏洞是重入漏洞的一种特殊情况,发生漏洞的位置是智能合约中 view 函数,由于该类型的函数并不对合约的状态变量进行修改,因此大多数情况下该类型的函数并不会使用重入锁进行修饰。当受害合约调用存在漏洞合约的 view 函数时,由于此时 view 函数中使用到的状态变量还未更新,导致受害合约通过 view 函数获取的数据也是未更新的,如果此时受害合约依赖 view 函数返回的值,则可能会出现异常的状况,如计算抵押物价格异常、奖励计算错误。


案例分析


下面将对两个著名的 DeFi 协议 Curve 和 Balancer 中存在的只读型重入漏洞进行分析。Curve 协议专注于加密货币之间的稳定币交易,例如 USDC、DAI、USDT 等。Balancer 协议则旨在让用户创建自己的资产组合,并可以将其作为流动性池子供其他用户交易。这两个协议中的只读型重入漏洞都由 ChainSecurity 发现,非常敬佩他们在 WEB3 安全做出的贡献。


Curve


Curve 漏洞分析


Curve 是一个去中心化交易所,用户可以向流动性池中提供流动性资产,从而获得相应份额的 LP 代币,该代币代表着流动性池中的份额。当用户移除流动性时,合约会燃烧掉 LP 代币,并发送相应的流动性资产给用户。同时合约提供了一个函数 get_virtual_price 用于计算 LP 代币的虚拟价值,其计算公式是使用 D 除以 LP 代币的总供应量 ,D 可以简单理解为流动性代币的总值,函数实现如下:



用户在添加流动性后,如果想移除流动性,可以调用合约的 remove_liquidity 函数:



该函数首先获取 LP 代币的总供应量,然后计算用户能取出多少的流动性资产,并将流动性池的资产余额中减去相应的数量,再将其发送给用户,如果是 ETH,则调用底层的 call 进行发送,如果是 ERC20 代币则使用其 transfer 函数进行发送,最后再将 LP 代币进行燃烧销毁。


这里当流动性资产是 ETH 时,会使用 call 发送 ETH,进入到 msg.sender 的 fallback 函数的逻辑中,似乎存在可以利用的点,但是合约中的关键函数如 add_liquidity、exchange、remove_liquidity 等都使用了重入锁,无法进行重入。回到 remove_liquidity 函数中,由于状态的更新不是同步完成的,进入到 msg.sender 的 fallback 函数时,还未进行 LP 代币的燃烧,但是流动性池中的余额已经扣掉,因此 balance 会变小,而此时 TotalSupply 还未变化。查看合约中使用到这两个状态变量的函数,可以发现受影响的是 get_virtual_price 函数,且该函数是 view 类型的函数,并未使用到重入锁,如果在重入中调用了该函数,最后获取的结果和正常值相比会变小,即 LP 代币的价格降低了。


如果外部合约依赖 Curve 中 get_virtual_price 函数返回的结果来进行逻辑处理的话,则有可能会受到该只读型重入漏洞的影响。


攻击案例分析


2023 年 2 月 9 日,DeFi 协议 dForcenet 遭到了黑客攻击,被攻击的根本原因是其预言机使用了 Curve 流动性池的 get_virtual_price 函数的返回值。


一笔攻击交易:https://arbiscan.io/tx/0x5db5c2400ab56db697b3cc9aa02a05deab658e1438ce2f8692ca009cc45171dd


攻击流程分析:


首先通过闪电贷贷出大量的 WETH,然后调用 Curve ETH/wstETH 池的 add_liquidity 添加流动性,获得 wstETHCRV:



然后将部分 wstETHCRV 转移到另一个攻击合约,并且在 wstETHCRV-gauge 合约中质押借出 wstETHCRV-gauge 和 USX :



再调用 Curve ETH/wstETH 池的 remove_liquidity 函数移除流动性,移除流动性时进入到攻击合约的 fallback 函数的逻辑中,由于 dForcenet 的预言机使用了 Curve ETH/wstETH 池的 get_virtual_price 函数,在此时的重入的状态下,由于 池中 ETH 的余额已经减小,但是 wstETHCRV 的总供应量还未改变,因此获取到的虚拟价格会变小,因此攻击者在 fallback 中清算了另一个攻击合约以及一个用户的借款:



攻击者最后再将 wstETHCRV-gauge 经过一些列操作兑换成了 WETH,归还闪电贷后,获利 1236 个 ETH 以及 71 万个 USX 代币。


其中 dForcenet 中预言机的实现:




可以看到 getPrice 中调用了 Curve 流动性的 get_virtual_price 函数,并进行了乘法处理。


Balancer


Balancer 漏洞分析


用户可以通过 Balancer 协议创建一系列资产的流动性池,并通过其他用户的交易获取手续费。用户可以通过 Balancer: Vault 合约的 joinPool 函数向创建好的流动性池中添加流动性,也可以通过 exitPool 函数撤销流动性。这两个函数都通过调用内置函数 _joinOrExit 来实现,只是传入的操作类型不同。_joinOrExit 函数的实现如下:



函数首先对传入的参数校验,然后获取对应的流动性池中的代币的余额。接下来会调用 _callPoolBalanceChange 函数计算余额,并进行资产转移,支付费用:



函数会根据操作类型,调用流动性池的 onJoinPool 或 onExitPool 函数,然后进行转账操作。当操作为移除流动性时,会调用 _processExitPoolTransfers 函数将资产转移给用户,具体的实现如下:



函数中会调用 _sendAsset 进行资产的转移,而在 _sendAsset 函数中,如果代币是 WETH,则会将 WETH 转换为 ETH,然后调用 sendValue 进行发送,而 sendValue 使用的是 call 发送 ETH,因此如果 recipient 是合约并且实现了 fallback 函数的话,则会进行到其 fallback 函数的逻辑中。


在执行了 _callPoolBalanceChange 函数的逻辑后,接下来会根据池子的类型调用对应的修改余额的函数对余额进行修改:



到这里函数的逻辑就结束了。可以发现存在的一个问题,函数是先进行转账,然后再进行余额的修改,如果资产中存在 ETH 的话则会触发接收者的 fallback 函数,此时转账已经完成,但是合约内置的余额还未更新。因此寻找合约中可利用的点,发现合约中的关键函数都使用了重入锁进行防护,无法利用。


虽然在 write 函数中没有发现利用点,但是发现存在 view 类型的函数 getPoolTokens 使用了合约内置的余额,并且该函数因为不会进行状态变量的修改,没有被重入锁进行修饰。


因此如果存在外部合约调用了 Balancer: Vault 合约的 getPoolTokens 函数,并使用其返回值进行逻辑处理的话,则可能会受到该只读型重入漏洞的影响。


攻击案例分析


2023 年 4 月 4 日,Arbitrum 上的借贷项目 Sentiment 遭到攻击,损失 100 万美元。遭到攻击的根本原因是其预言机使用了 Balancer: Vault 合约的 getPoolTokens 函数返回的数据,此处存在已知的只读型重入漏洞。


攻击交易:https://arbiscan.io/tx/0xa9ff2b587e2741575daf893864710a5cbb44bb64ccdc487a100fa20741e0f74d


攻击流程分析:


攻击者通过闪电贷贷出 606 个 WBTC,10,050 个 WETH 和 18,000,000 个 USDC,然后通过 Sentiment 创建了一个账户,并向其中存入了 50 个 WETH,再通过创建的账户将这 50 个 WETH 存入到 Balancer: Vault 中:



然后通过攻击合约调用 Balancer: Vault 合约的 joinPool 函数向流动性池提供流动性,转入了 606 个 WBTC,10,000 个 WETH,18,000,000 个 USDC,再调用 exitPool 移除流动性,发送 ETH 时触发攻击合约的 fallback 函数:



在 fallback 函数中,攻击者调用 borrow 函数进行借款,其中会使用 RiskEngine.isAccountHealthy 进行账户健康度检查,其中预言机的 getPrice 函数由 WeightedBalancerLPOracle.getPrice 获取,而该函数又会使用到 Balancer: Vault 合约的 getPoolTokens 返回的数据,此时由于还在攻击合约的 fallback 的逻辑中, 流动性池内的 WBTC、WETH、USDC 的数量还未更新,因此此时预言机的价格相比之前的放大了 16 倍,攻击者能通过 50 个 WETH 借出更多的资产。



然后攻击者多次调用 borrow 函数 和 exec 函数借出其他资产,归还闪电贷后获利 0.5 个 WBTC、30 个 WETH,538,399 个 USDC,360,000 个 USDT:




查看 WeightedBalancerLPOracle 合约的 getPrice 函数:



可以看到该函数通过 Balancer.vault 合约的 getPoolTokens 函数获取到对应的流动性池的代币余额后,在后续的逻辑中会对代币余额进行乘法操作,未更新的代币余额会导致此处结果被放大。


总结


本文简单介绍了只读型重入漏洞的概念,并且分析了两个顶级的 DeFi 中存在的只读型重入漏洞,以及对应的真实发生的攻击案例。在开发 DeFi 项目时应当考虑到其他项目可能存在的集成的情形,并通过多种安全措施减少重入漏洞带来的危害,必要时 view 类型的函数也可以加入重入锁,增强安全性。

【免责声明】市场有风险,投资需谨慎。本文不构成投资建议,用户应考虑本文中的任何意见、观点或结论是否符合其特定状况。据此投资,责任自负。

Eocene Research
数据请求中
查看更多

推荐专栏

数据请求中
在 App 打开