区块链安全

任意外部调用类型漏洞

漏洞分析

事情要近期从 Visor.finance 又一次被黑说起:

尝试来还原一下攻击手法。

首先来看攻击交易: 

https://etherscan.io/tx/0x69272d8c84d67d1da2f6425b339192fa472898dce936f24818fda415c1c1ff3f

直接从区块链浏览器上面似乎看不出什么。只能看出是可以看到从0地址两次转出来了大量vVISR代币,因为0地址不会主动转账或者授权给配额,所以应该是 mint 出来的。

然后还可以看出:

攻击者地址:0x10c509aa9ab291c76c45414e7cdbd375e1d5ace8

恶意漏洞利用合约:0x10c509aa9ab291c76c45414e7cdbd375e1d5ace8(未开源代码)

使用 blocksec 的交易分析工具去分析:https://versatile.blocksecteam.com/tx/eth/0x69272d8c84d67d1da2f6425b339192fa472898dce936f24818fda415c1c1ff3f

主要是使用 Invocation flow 功能来查看调用关系,自己可以对其中的地址和函数根据区块链浏览器和查函数指纹网站进行查询找到标签,然后使用这个分析工具的 Customize account map 功能,替换为自定义的标签,就可以提高 Invocation flow 的可读性。

下面是我给出的输入:

{
    "0x0000000000000000000000000000000000000000": "0x0000...0000",
    "0xf938424f7210f31df2aee3011291b658f872e91e": "Visor Finance: VISR Token",
    "0x8efab89b497b887cdaa2fb08ff71e4b3827774b2": "Visor Finance Exploiter",
    "0x10c509aa9ab291c76c45414e7cdbd375e1d5ace8": "Exploit_Contract",
    "0x3a84ad5d16adbe566baa6b3dafe39db3d5e261e5": "vVISR",
    "0xc9f27a50f82571c1c8423a42970613b8dbda14ef": "RewardsHypervisor" 
}

对应的 Invocation flow 就清晰多了:

可以看到:

  1. 攻击合约(Exploit_Contract)的0x4a0b0c38 匿名函数内调用了 Visor.Finance 的 RewardsHypervisor 合约,call 的是 deposit 函数,把攻击合约的地址和攻击者自己的地址传参进去了;
  2. 后面居然又回调了到攻击合约的 ownerdelegatedTransferERC20 函数;
  3. 在delegatedTransferERC20函数里又重入了 Visor.Finance 的 RewardsHypervisor 合约,调用了 deposit 函数。

这么一看,很明显 RewardsHypervisor 合约的 deposit 函数内存在任意的外部调用。在这里就被恶意合约调用了。

所以来看看 RewardsHypervisor 合约的开源代码:https://etherscan.io/address/0xc9f27a50f82571c1c8423a42970613b8dbda14ef#code

这段代码的作用是向这个合约存入(deposit)visr,mint 成 vvisr 流动性代币。

  1. 入参为:参数1,声称存入的 Visr 的数量,这些 Visr 是需要直接转到此合约的。参数2:visr的来源地址。参数3:将 vvisr 发送去的地址。
  2. 返回值是:mint 出得 vvisr 的数量。这个函数是 external 的,被外部调用的。
  3. 46-48 行的判断是:声称存入的 Visr 数量必须大于0,不能将 mint 的流动性代币发给0地址或者此合约(Hypervisor)地址。
  4. 50行,创建一个状态变量用于接 mint 出的 vvisr 数量的数据,初始值为存入的 visr 的值。
  5. 51-54行:若 vvisr 这个 erc20 代币的总供应量不为0,那么计算此合约(Hypervisor)地址中 visr 的余额。这个合约本身持 visr 币,计算这个合约中的 visr 的数量是为了根据比例计算要 mint 的 vvisr 的数量。
  6. 53 行,要 mint 的 vvisr 的数量shares = (visrDeposit/visrBalance)*vvisr总量,很容易理解,是根据比例的。
  7. 56 行,如果发送 Visr 的地址是合约,那么要求这个合约实现了 IVisor 接口,合约的 owner 是当前的 msg.sender,然后从 from 地址发送入参1 声称的金额的 visr 到当前合约地址。如果调用失败会 revert。
  8. IVisor 接口如下非常简单,也就是要求合约可以转账,有 owner。
  9. 如果发送 Visr 的地址是 eoa,那么要求先有授权给了当前 Hypervisor 合约,然后从 from 合约转入参1 声称的金额的 visr 到当前合约地址。
  10. 根据算出来的 shares mint 这么多 vvisr。然后转给 to 的地址。 

这个函数的问题有哪些呢,主要问题1在第56-59行的逻辑中,若传入 visr 的地址是合约,那么检测传入 visr 的合约地址的 owner 是 msg.sender,然后调用 from 的 delegatedTransferERC20函数。本意是检测确实是 msg.sender 授权的转账。

这里的主要问题就在于完全没有验证是否真的转入了visr。

因为from是用户自己实现的合约,所以用户可以自己实现 delegatedTransferErc20函数,里面的内容随便写,可以完全不实现转账。然后呢也满足 require。最后,因为 Hypervisor 合约本身有钱,会按照根据入参1,也就是声称要存入的钱计算出来的 shares 来 mint vvisr 发给用户。

所以问题的关键在于:并未验证是否有转入钱!

uniswap v2 的一些交易对合约就不存在此问题,为什么呢?因为1. 交易对合约本身没钱;2. 是计算得到的到底转入了多少钱,每次保存合约余额,然后用新的余额-历史余额的状态变量,得到转入的钱。

然后还存在问题2,else 里面的逻辑,如果from不是合约而是Eoa类型,就调用visr.safeTransferFrom函数,但是由于传入的from参数和to参数可控,意味着可以用别人授权给RewardsHypervisor合约的visr充值进去,再mint vvisr给自己。

也就是 from 写授权给了 RewardsHypervisor 合约的地址,to 写自己作为收帐地址,就可以白嫖 vvisr。因为如果授权给了 spender 一定的配额,那么一定可以转配额数额的 ether 给 spender。

收到 vvisr 之后,可以调用 withdraw 函数,换回 visr :

漏洞复现

hardhat 进行攻击复现:

首先来说下配置文件:

url 是从 moralis.io 获取的,这个网上可以免费获得区块链历史存档数据。

然后找到要fork的区块,攻击发生在13849007,那么就fork到它上一个区块13849006。

再来看攻击合约:

上面说过,攻击合约的 delegatedTrandferERC20 函数里面只要不实现本义的给 RewardsHypervisor 合约转账就行了,在这里索性什么都不用做。owner 要实现一会儿等于 msg.sender 就行了,其实也就是 tx.orgin。在 Invocation flow 中我们看到攻击者在这个函数里面实现了重入 deposit 函数的逻辑,但是我们这里测试的话就只是简单的返回,do nothing。

然后 IvVISR.sol,是一个接口:

IRewardsHypervisor.sol 同样是一个接口。

这两个文件分别是漏洞合约和 vVISR 的接口,目的是使用接口来强转 address 类型,这样使用起来更方便。比如 IvVISR(0x0123456789abcdef0123).balanceof(‘test.eth’)。如果不用接口,当然可以,因为合约都在链上,但是调用起来比较麻烦,比如调用一下漏洞合约的 deposit 函数,还得 sha3 计算函数签名等。

然后再来看攻击代码。

attack.js 是针对漏洞1的,就非常简单:

尝试 hack,去除掉一些 gas 之后,果然获得了巨额财富:

攻击者在 delegatedTransferERC20 里面实现了重入 deposit 的逻辑,最大的功能是可以耗尽池子。

然后再来看看漏洞2,所以我们首先要找到给 Visor.Finance 的 RewardsHypervisor 合约授权过的 eoa。


以下为错误尝试:

从 deposit 过的 eoa 中随机找了一个 https://etherscan.io/tx/0x0f27686101cbe22c79f6c313251e4680866a752d123215d9838374168e3344bd,但是目前这个 eoa 已经没有 visr 了:

可以很清楚的看到这个eos是在区块高度 13845429  被害的。所以我们同样回要到上一个区块 13845428。

从被害交易的 log 中可以看到金额:

注意上面的金额是 visr 的金额,下面的是 vvisr 的数量。

但是我尝试之后提醒我 transfer amount exceeds balance。我的思路是:这是一个 deposit 函数调用,既然发起的是一个 eoa,那么为了 safetransfrom,必定是先 approve 了的。

但其实 0x078…414ac 是拿 0x224… 转给 RewardsHypervisor  的,0x224 是一个合约,点开看是 NFT。这是正常的业务逻辑:币放在 NFT 里面,然后验证 msg.sender 是不是 NFT 的 owner。如果是,就把 nft 里面的 visr转出来充值。

所以哪怕一个交易是 eoa 发起的,但是也可能通过 internal 交易,实际上是合约调用合约。就像上面的情况。

这个 deposit from 地址,不等于 tx.origin 也就是发起地址。所以发起地址并未授权。


所以还是寻找 approval 的事件:

https://etherscan.io/tx/0xcf076b83a99f9841f2c262add72d73a2041fa633173fbe4295e29c95ec2d2207

但是这个eoa在这个交易中把 vvsr 花掉了:https://etherscan.io/tx/0xa62079f9fb5084b18c2be6403fdc05726d3ef68b9e6ab6976922b18c7adeee01

于是退回到这个上一个区块高度:13841698

攻击成功:

这里还得注意一件事,就是虽然 allowance 还有,但是 eoa 的 balance 不够了。这也体现了攻击者重入调用的另一重考虑。

目前是否修复呢?拿两天前的授权的 eoa 测试:

https://etherscan.io/tx/0xce1c7b6192d0d5cb77239cf9872bdfd2749ea3d2b82db4b19af3bca6bcbb3079

查询配额:

发现已修复:

漏洞检测

结合 Slither 这类白盒工具对此类型外部可控地址参数、且可重入到原合约类型的漏洞进行检测。

参考引用

参考自 @Rivaill 大佬的文章 https://mp.weixin.qq.com/s/BmJFtgOELP_Sd3LYhq5dlw,致谢