​More than Re-entrancy : Revest Finance 被攻击事件分析

admin 2022年4月18日23:29:23评论33 views字数 7143阅读23分48秒阅读模式

2022年3月27日,以太坊上的staking DeFi项目 Revest Finance 遭到黑客攻击,损失约200万美元。BlockSecTeam团队第一时间介入分析,并在tweeter上向社区分享了我们的分析成果。事实上,就在我们通过tweeter向社区分享我们的分析成果时,我们发现了Revest Finance的TokenVault合约中还存在着一个critical zero-day vulnerability。利用该漏洞,攻击者可以用更加简单的方式盗取协议中的资产。于是我们立刻联系了Revest Finance项目方。在确定该漏洞已经被修复后,我们决定向社区分享这篇blog。

0. What's the Revest Finance FNFT

Revest Finance是针对DeFi领域中staking的解决方案,用户通过Revest Finance参与的任何DeFi的staking,都可以直接生成一个NFT,即 FNFT (Finance Non-Fungible Token) , 该NFT代表了这个staking仓位的当前以及未来价值。用户可以通过Revest Finance 提供的3个接口和项目进行交互。质押自己的数字资产,mint 出相应的 FNFT 。

mintTimeLock : 用户质押的数字资产在一段时间之后才能被解锁。mintValueLock : 用户质押的数字资产只有在升值或者贬值到预设数值才能被解锁。mintAddressLock : 用户质押的数字资产只能被预设的账户解锁。

Revest Finance 通过以下3个智能合约完成对用户存入的数字资产的锁定和解锁。

FNFTHandler : 继承自ERC-1155 token(openzepplin实现) 。每次执行lock操作时,fnftId会进行自增(fnftId 类似于ERC721中的tokenId)。FNFT在被创建时,用户需要指定它的totalSupply。当用户想要提走FNFT背后的underlying asset,需要burn掉相应比例的FNTF。LockManage : 记录FNFT被解锁(unlock)的条件。TokenVault : 接收和发送用户存入的underlying asset,并记录每一种FNFT的metadata 。例如fnftId =1的FNFT背后质押的资产类型。

因为此次攻击,黑客攻击的入口是mintAddressLock函数,那么我们以该函数为例,讲述FNFT的生命周期。

​More than Re-entrancy : Revest Finance 被攻击事件分析

图一


User A调用Revest的 mintAddressLock 函数

unlocker : User X -> 只有User X 可以解锁这笔资产recipients : [User A , User B , User C] quantities : [50 , 25 , 25] -> mint 数量为100 (sum (quantities)) , User A , User B , User C 各拥有50 , 25 ,25 枚 。asset : WETH -> mint 出的 FNFT 以 WETH 为抵押品。depositAmount : 1e18 -> 每一枚 FNFT 背后的抵押品数量为 1枚WETH ( WETH decimal 为18 )

假设当前系统中没有其他FNFT, User A 通过mintAddressLock 与系统进行交互,FNFTHandler返回的fnftId = 1 

LockManger 为其添加相应的记录

fnftId : 1unlocker : User X 

Token Vault 为其添加相应的记录

fnftId : 1asset : WETHdepoistAmount : 1e18

接着Token Valut 要从 User A 这里转走 100 * 1e18 数量的WETH 。

最后系统分别给 User A , User B , User C mint 50 , 25 ,25 枚 01-FNFT 。

通过mintAddressLock 函数铸造FNFT 就完成了。

​More than Re-entrancy : Revest Finance 被攻击事件分析

图二


当User X 解锁 01-FNFT 后,用户B 便可以通过withdrawFNFT提走underlying asset 。如图二所示,User B 想要提取自己手中持有的25个 01-FNFT 质押的数字资产。

协议首先检查01-FNTF是否已经 unlock ,如果已经unlock, 那么协议会burn掉User B的25个01-FNFT,并给他转25*1e18数量的 WETH 。此时01-FNFT 的 totalSupply 为 75 。

Revest 合约还提供了另外一个接口,叫做depositAdditionalToFNFT,以便让用户为一个已经存在的 FNFT 添加更多的underlying asset 。下面我们用2张图描述它的“正常”用法。

​More than Re-entrancy : Revest Finance 被攻击事件分析

图三


这里有三种情况 

一.quantity == 01-FNFT.totalSupply() 如图三所示

以图二中的场景为上下文,User A 要为 01-FNFT 添加更多的抵押物。

quantity = 75 -> 为75个 01-FNFT 追加质押 。amount = 0.5*1e18 -> 每一枚 01-FNFT 追加0.5*1e18 数量的WETH 。

于是User A 需要向 Token Vault 转入 37.5*1e18 WETH (75 * 0.5*1e18) Token Vault 修改系统记账,将depositAmount修改为 1.5*1e18。现在每一枚01-FNFT 承载的资产为1.5*1e18 WETH 。

此时User C 调用withdrawFNFT ,burn掉他持有的25枚 01-FNFT ,他可以拿走25*(1.5*1e18) = 37.5*1e18 WETH 。

于是,此时01-FNFT 的totalSupply为50 。

​More than Re-entrancy : Revest Finance 被攻击事件分析

图四


二.quantity < 01-FNFT.totalSupply() 如图四所示 

以图三中的场景为上下文,User A 继续为01-FNFT添加更多的抵押物。

quantity = 10 -> 为10枚 01-FNFT 追加质押。amount = 0.5*1e18 -> 为10枚 01-FNFT 每一枚追加0.5*1e18 WETH

由于quantity < 01-FNFT.totalSupply()
于是,User A 向协议支付 5*1e18 WETH 系统将会burn掉 10枚 01-FNFT ,mint 出10枚 02-FNFT ,并将burn掉的10枚01-FNFT承载的资产和User A 新转入的资产,注入到02-FNFT中。于是就有

fnftId : 2asset : WETHdepositAmount : 2.0*1e18 (1.5*1e18 + 0.5*1e18) 

此时 

01-FNFT.totalSupply : 40 01-FNFT.depositAmount : 1.5*1e18 (逻辑上应该如此,见后文: the New Zero-day Vulnerability)02-FNFT.totalSupply : 10 02-FNFT.depositAmount : 2.0*1e18

三.quantity > 01-FNFT.totalSupply()

这种情况,交易会revert。

1. What't the Re-entrancy vulnerability

在理解了mintAddressLock 函数 和 depositAdditionalToFNFT 函数的基本工作流程后,来看一下攻击者使用的重入手法。 假定the lastest fnftId = 1(不影响理解)

​More than Re-entrancy : Revest Finance 被攻击事件分析

图五 


如图五所示
第一步:
攻击者调用 mintAddressLock 函数 

depositAmount = 0 quantities = [2] 

mint 出了 2枚 01-FNFT , 由于攻击者将 depositAmount 设置为0 ,因此他没有转入任何数字资产。相当于01-FNFT 背后承载的underlying asset 为0 。

第二步:攻击者再次调用 mintAddressLock 函数 

depositAmount = 0quantities = [360000] 准备mint 36w 枚02-FNFT depositAmount为0 。

在mint 的最后一步,攻击者利用ERC-1155 的call-back 机制重入了 depositAdditionalToFNFT 函数 。(详见下面给出的 _doSafeTransferAcceptanceCheck函数)


function _mint( address to, uint256 id, uint256 amount, bytes memory data) internal virtual { require(to != address(0), "ERC1155: mint to the zero address");
address operator = _msgSender(); uint256[] memory ids = _asSingletonArray(id); uint256[] memory amounts = _asSingletonArray(amount);
_beforeTokenTransfer(operator, address(0), to, ids, amounts, data);
_balances[id][to] += amount; emit TransferSingle(operator, address(0), to, id, amount);
_afterTokenTransfer(operator, address(0), to, ids, amounts, data);
_doSafeTransferAcceptanceCheck(operator, address(0), to, id, amount, data); }

function _doSafeTransferAcceptanceCheck( address operator, address from, address to, uint256 id, uint256 amount, bytes memory data) private { if (to.isContract()) { try IERC1155Receiver(to).onERC1155Received(operator, from, id, amount, data) returns (bytes4 response) { if (response != IERC1155Receiver.onERC1155Received.selector) { revert("ERC1155: ERC1155Receiver rejected tokens"); } } catch Error(string memory reason) { revert(reason); } catch { revert("ERC1155: transfer to non ERC1155Receiver implementer"); } } }

​More than Re-entrancy : Revest Finance 被攻击事件分析

图六


在depositAdditionalToFNFT 中 , 攻击者传入 

quantity = 1

amount = 1*1e18fnftId = 1 

因为 quantity < fntfId.totalSupply(),因此协议会burn掉攻击者1枚01-FNFT, 铸造1枚02-FNFT。(02-FNFT在协议中已经存在,但是fnftId 更新延迟)然后修改fnftId =2 的depositAmount 为 amount。相信你已经发现,这一步,攻击者通过重入将 fnftId = 2 的 depositAmount 从0修改为1.0*1e18 , 仅仅花费1*1e18 RENA 就获得了 (360000 +1 ) * 1*1e18 RENA 的系统记账。

​More than Re-entrancy : Revest Finance 被攻击事件分析

图七


最后攻击者调用withdrawNFNFT函数,burn掉 360,001枚02-FNFT,取走了360,001*1e18 RENA 。

建议修复方法

function mint(    address account,     uint id,     uint amount,     bytes memory data) external override onlyRevestController {    require(amount > 0, "Invalid amount");    // 避免 mint 已经存在的 fnft     require(supply[id] == 0, "Repeated mint for the same FNFT");    supply[id] += amount;    // 将fnftId 自增移到外部调用前面    fnftsCreated += 1;    _mint(account, id, amount, data);}

2. the New Zero-day Vulnerability

在blockSecTeam团队分析Revest Finance 的代码时,handleMultipleDeposits 函数引起了我们的注意。


function handleMultipleDeposits( uint fnftId, uint newFNFTId, uint amount) external override onlyRevestController { require(amount >= fnfts[fnftId].depositAmount, 'E003'); IRevest.FNFTConfig storage config = fnfts[fnftId]; config.depositAmount = amount; mapFNFTToToken(fnftId, config); if(newFNFTId != 0) { mapFNFTToToken(newFNFTId, config); }}

当用户调用 depositAdditionToNFT 函数追加抵押物时,该函数会改变 FNFT 的 depositAmount 。从代码中我们可以发现,当newFNFTId != 0 时,该函数既改变了 fnftId 对应的 FNFT 的depositAmount 也改变了 newFNFTId 对应的 depositAmount 。 

按照常理,当 newFNFTId !=0 时,系统应该只记录 newNFTId 对应的 depositAmount 。不应该改变 fnftId 对应的depositAmount 。 

我们认为这是一个非常严重的逻辑bug ,利用该漏洞,攻击者可以很轻松提走系统中的数字资产。下面用3张图描述模拟攻击的原理。
假定 the latest fnftId = 1 

​More than Re-entrancy : Revest Finance 被攻击事件分析

图八


首先攻击者调用 mintAddressLock 函数,mint出360000个01-FNFT 。攻击者将 amount 设置为0 因此他不必转入任何资产到Revest Finance 协议中。
mint 结束后,攻击者拥有360000 枚 depositAmount =0 的 01-FNFT 。

​More than Re-entrancy : Revest Finance 被攻击事件分析

图九


然后攻击者调用 depositAdditionalToFNFT 函数,参数如下

fnftId = 1amount = 1 * 1e18quantity = 1

协议转走攻击者 amount * quantity 数量的代币 ,即 1 * 1e18RENA
协议会burn掉攻击者1枚01-FNFT , 并为其铸造一枚02-FNFT(假定 latest fnftId = 2)
按照 handleMultipleDeposits 函数中的逻辑, fnftId = 2 的资产,其 depositAmount 会被设置为 1.0*1e18。
但是 fnftId = 1的资产,其depositAmount 也会被设置为 1.0*1e18 ,而这个值本应该为0!



​More than Re-entrancy : Revest Finance 被攻击事件分析

图十


第三步,攻击者直接提款,将手中所有的 01-FNFT 提现。不考虑gas费,他将净赚 359,999 * 1e18 数量的 REAN 代币。 

很显然,使用这种方式进行攻击,比真实的重入攻击更加简单直接。

建议修复方法

针对该漏洞,blockSecTeam团队给出了相应的patch方法。


function handleMultipleDeposits( uint fnftId, uint newFNFTId, uint amount) external override onlyRevestController { require(amount >= fnfts[fnftId].depositAmount, 'E003'); IRevest.FNFTConfig memory config = fnfts[fnftId]; config.depositAmount = amount; if(newFNFTId != 0) { mapFNFTToToken(newFNFTId, config); // 当 newFNFTId 为 0 时才更新 fnftId 对应的depositAmount } else { mapFNFTToToken(fnftId, config); }}

3. 项目方的修复方式

由于TokenVault and FNFTHandler 两个漏洞合约存储了许多关键的状态,无法在短时间内重新部署它们, 为了快速恢复使用, Revest Finance 官方重新部署了Revest 合约 (https://etherscan.io/address/0x36c2732f1b2ed69cf17133ab01f2876b614a2f27#code)的精简版本。该版本关闭了大部分复杂的功能,以避免被进一步攻击。项目方将在未来迁移状态并重新部署修复过的合约。

4. 总结

提升DeFi项目的安全性不是一件容易的事情。除了代码审计,我们认为社区应该采取更加主动的方式,例如项目监控预警、甚至是攻击阻断 使得DeFi社区更加安全。(https://mp.weixin.qq.com/s/o41Da2PJtu7LEcam9eyCeQ).

参考文献

*[1]: https://blocksecteam.medium.com/revest-finance-vulnerabilities-more-than-re-entrancy-1609957b742f

​More than Re-entrancy : Revest Finance 被攻击事件分析


原文始发于微信公众号(BlockSec Team):​More than Re-entrancy : Revest Finance 被攻击事件分析

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年4月18日23:29:23
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   ​More than Re-entrancy : Revest Finance 被攻击事件分析http://cn-sec.com/archives/921780.html

发表评论

匿名网友 填写信息