智能合约审计-拒绝服务漏洞及案例分析

什么是拒绝服务

拒绝服务(Denial of Service),简称Dos,简而言之拒绝服务就是限制合法用户永久或在一段时间内无法使用智能合约。

拒绝服务漏洞的分类

意外恢复拒绝服务

攻击者使用智能合约进行此类攻击。 要解释意外恢复如何导致 DoS,请考虑以下Auction合约允许投标人出价的智能合约,一旦有新的最高出价者,它就会将金额退还给旧的投标人。

Auction.sol:

contract Auction {
    address frontRunner;
    uint256 highestBid;

    function bid() public payable {
        require(msg.value > highestBid, "Need to be higher than highest bid");
        // Refund the old leader, if it fails then revert   
        require(payable(frontRunner).send(highestBid), "Failed to send Ether");

        frontRunner = msg.sender;
        highestBid = msg.value;
    }
}

Attack.sol:

import "./Auction.sol";   
contract Attacker{
    Auction auction;

    constructor(Auction _auctionaddr){
        auction = Auction(_auctionaddr);
    }

    function attack (){
        auction.bid{value: msg.value}();
    }

}

合同很简单,如下所述:

Auction.sol Auction合约定义了一个名为 bid()的函数,它在其中检查以下条件: a.如果出价金额是大于 highestBid,就将先前的投标金额退还给先前的投标人 b.如果两个条件都满足,则它会用新值更新最高出价和最高出价者 ( frontRunner )。

Attack.sol a.Attacker合约获取合约的部署地址,并在Auction构造函数中对其进行初始化,以便攻击者可以访问Auction合约的功能。 b.该函数attack()调用合约函数Auction.bid()进行出价。

过程分析 漏洞利用是如何发生的?让我们分步来看。 假设有用户开始出价。 1.用户 1出价购买“3”以太币,因此将成为领跑者。 2.用户 2出价购买“5”以太币,因此将接管领跑者的角色,用户 1将获得退款。 3.用户 3出价购买“6”以太币,因此将成为新的领跑者,用户 2将获得退款。 4.攻击者调用合约attack() 函数并出价,比如说,'7' 以太币。攻击者合约将成为新的领跑者,而用户3将被退还。 5.现在,如果任何其他用户调用bid()函数,向攻击者合约的退款将失败。这是因为Attacker合约没有实现接收以太币的receive()或者 fallback 函数。由于这个原因,任何Solidity以太传输函数,例如call(), send()或者transfer()都会导致异常或由于语句而导致意外恢复require(),从而停止执行。

在这种情况下,攻击者合约将一直是无可争议的国王或出价最高者,从而利用该系统。

区块gas限制拒绝服务

每个区块都有可以消耗的气体量上限,因此可以进行计算量上限。这是区块气体限制。如果消耗的 gas 超过此限制,交易将失败,即交易的 gas limit 高于可用的 Max Limit,导致交易失败。 如果这样的交易失败,尤其是当你处于循环中将金额退还给所有者时,它会停止执行,导致退款完全被阻止,资金永远被卡住。 一个可以达到 区块gas限制的 for 循环示例如下:

address[] private refundAddresses;
mapping (address => uint) public refunds;
function refundAll() external onlyOwner {
   // unknown length iteration based on how many addresses participated
    for(uint i; i < refundAddresses.length; i++) {
   // doubly bad, now a single failure on send will hold up all funds
       require(refundAddresses[i].send(refunds[refundAddresses[i]]))
    }
}

这可能是无意的,或者假设一个攻击者决定创建大量地址,每个地址都从智能合约中获得少量资金。 如果操作正确,交易可以被永久阻止,甚至可能阻止其他交易。

对于主网,gas限制为8000000,对于Ropsten网络,gas限制为5500000,对于本地Ganache网络(在 Truffle 中),默认区块gas限制为 90000。

Block Stuffing 即使您的合约不包含无限循环,攻击者也可以通过以足够高的 gas 价格放置计算密集型交易来防止其他交易被包含在区块链中几个区块。 为此,攻击者可以发出多个交易,这些交易将消耗整个 gas 限制,并在下一个区块被开采时立即包含足够高的 gas 价格。没有 gas 价格可以保证包含在区块中,但价格越高,机会就越大。 如果攻击成功,则该块中不会包含其他交易。有时,攻击者的目标是在特定时间之前阻止对特定合约的交易 这种攻击技术被用于赌博应用程序——Fomo3D。 Block Stuffing 攻击可用于任何需要在特定时间段内采取行动的合约。然而,与任何攻击一样,它只有在预期回报超过其成本时才有利可图。这种攻击的成本与需要填充的块数成正比。如果通过阻止其他参与者的行动可以获得大笔支出,那么您的合约很可能会成为此类攻击的目标。

真实案例分析

总结了这么多,但是你会发现,真实的漏洞往往更加复杂,接下来对一个真实Balancer 协议dos漏洞进行分析。

AMM 和 Balancer 简介 自动做市商 (AMM) 是智能合约可实现众包流动资金池 (LP) 的自动管理,为去中心化交易所 (DEX) 提供可交易的代币。AMM 是 DeFi 中的一个重要原语,因为任何人都可以将自己的代币存入 AMM LP 中,从而获得一部分交易费用和 LP 代币作为回报。

AMM 不依赖人类来管理交易,交易直接由算法控制。AMM 用两种资产的流动资金池代替订单簿市场中的买卖订单,这两种资产的价值相对于彼此。当一种资产换成另一种资产时,两种资产的相对价格发生变化,并且重新计算两种资产的新市场利率。

大多数 AMM 使用常数均值做市商方程 (也称为常数乘积方程)来定义流动性池的行为方式:

Balance of token A * Balance of token B = Constant

例如,在一个有 2,000,000 个代币 A 和 1,000 个代币 B 的池中,恒定乘积为 2,000,000,000。然后,这种逆相关关系决定了资产在任何时刻的交易价格,因为当第二个代币的价格下跌时,一个代币的价格上涨:

Market price for token A = Balance of token B / Balance of token A

价格曲线通常被用作可视化这种关系的一种方式:

为了找到给定池中资产的交换价格(即,当您交换一定数量的代币 B时收到的代币 A的数量),我们计算池中保持恒定乘积所需的每个代币的余额交换后不变:

从这次交换中收到的代币 A的数量传达了“买入价”,它是根据上面的关系计算的:

Enter Balancer

Balancer 于 2018 年推出,在以太坊区块链和其他 EVM 兼容系统上引入了可配置流动性协议。借助 Balancer,用户能够以任意比例创建最多包含八种不同 ERC-20 代币的流动资金池。这些池可以被认为是自动重新平衡投资组合,为交易者提供所谓的去中心化指数基金。

Balancer 将多个池路由在一起,允许从一个池中的一个代币轻松交易到另一个池中的任何其他代币。这些多资产池使用所谓的加权数学来工作,旨在允许任何资产之间的掉期,无论它们是否具有价格相关性。Balancer 的价格方程是常量乘积方程的推广,考虑了多代币和非均分的权重,因此价格由矿池的余额和权重决定。

Balancer 的核心智能合约称为“The Vault”,它控制和持有每个 Balancer 池中的所有代币。Vault 的架构将代币核算和管理与矿池逻辑分开,简化了矿池的合约。在此架构中,Balancer 的协议交换费用是在名为合约ProtocolFeesCollector.sol上定义的矿池收取的交换费用的百分比。这些费用包括闪贷费用,它们通过 Balancer 的 DAO 进行治理进行调整。

这次漏洞使用 Vault 合约余额发放闪贷可用于将代币从 Vault 转移到ProtocolFeesCollector(就像它们是常规协议费用一样),导致 DoS 场景,因为 Vault 突然在交换期间缺少要转移的代币。

Balancer 的闪电贷

闪电贷是一种特殊的智能合约操作,允许在没有抵押品的情况下借入任何可用数量的资产(最高为总流动性)。只要在一次大宗交易中返还流动性和利息,这是可能的。如果借款人无法偿还贷款,则整个交易将被简单地还原。闪贷的一个常见用途是套利算法,通过在第一个 DEX 上交易资产来寻求利润,然后在其他 DEX 上取回资产加上收益。 在 Balancer 的背景下,交易者可以借用 The Vault 上可用的任何数量的代币以用于闪贷。这些类型交易的逻辑在FlashLoans.sol合约中指定,更具体地说在flashLoan函数中:

1.该函数从简单的初始化开始,确保代币数组的长度与金额数组匹配,并将零地址分配给previousToken变量(以确保代币已排序且唯一):

2.代币数组的第一个循环用于记录每个代币的预余额,计算贷款费用,并将每个数量的代币转移给接收者。注意接收器上的回调函数,receiveFlashLoan,就在循环之后:

  • 第二个循环检查每个代币的余额是否等于或大于它们之前的余额,然后将任何余额盈余作为费用添加到receivedFeeAmount变量中:

4.这笔费用将转至ProtocolFeesCollector:

_payFeeAmount(token, receivedFeeAmount);

通过Fees.sol下面描述的功能:

您是否发现闪贷功能的实现方式存在问题?如果没有,请不要担心;我们非常接近了解此漏洞!我们只需要最后一条信息:双重入口点 ERC-20 代币。

代理和双重入口点 ERC-20 代币

当 ERC-20 代币以代理模式运行时,用户在执行代币交易时直接与代理合约交互。然后代理连接到实现底层逻辑的目标合约。

双入口点 ERC-20 令牌依赖于用户和合约都可以直接与目标合约交互的架构。由于可以绕过代理,因此这些令牌有两个入口点。

在 Balancer 池中交易的双入口点令牌的示例是 Synthetix 令牌,例如SNX和sBTC。就上下文而言,Synthetix是一种衍生流动性协议,它为“合成物”(合成资产)的创建提供支持,这些合成物可以以去中心化和无需许可的方式进行交易。

漏洞分析

我们讲解了几个重要的概念,现在我们准备深入研究在我们上面描述的闪贷方法中发现的这个漏洞,它是Balancer 的 Vault 合约的一部分。 如果攻击者试图为同一代币的两个入口点执行 Balancer 闪贷怎么办?在这种情况下,闪贷还款(第二个循环)将被解释为第二个入口点的代币余额过剩,并作为费用发送到 The Vault! 这是此漏洞利用的分步过程: 1.在漏洞利用脚本中,我们为选定的双入口点令牌定义两个入口点,例如 sBTC,检索目标合约和代理合约:

2.然后我们制定一个 Exploit 合约:

a.从IVault.sol接口实现闪贷功能,

b. receiveFlashLoan从 The Vault 的IFlashLoanRecipient.sol接口实现方法:

  • 回到我们的脚本,我们部署这个合约,利用 flashloan 方法,第一个代币作为第一个入口点和金库的全部余额,第二个代币作为零余额的第二个入口点:

  • 如果我们再看一下 flashloan 函数,在第一个循环中,我们会看到第一个入口点(目标)的先前余额被跟踪为全部余额(并转出)。第二个入口点(代理)的余额被跟踪为 0:

  • 在第二个循环中,检查第一个入口点(目标)的先前余额仍然是完整余额。该漏洞存在于检查第二个入口点(代理)期间。它之前的余额为零,其后余额为全额,导致receivedFeeAmount收到等于全额的差额!

  • Vault 的_payFeeAmount方法然后会将全部 sBTC 发送到ProtocolFeesCollector合约,完全耗尽 Vault!池的最终状态将导致合法交易(例如交换和池退出)失败,从而导致 DoS 情况!

漏洞修复

由于 Balancer 治理控制着对ProtocolFeeCollector合约的访问,因此攻击者在将资金发送到该合约后将无法取回资金。但是,正如我们在上面了解到的那样,利用此类漏洞可能会导致对池资源的 DoS 攻击。 收到漏洞通知后,Balancer Labs 团队迅速在受影响的合约中部署了缓解措施,将代币移至 ProtocolFeeCollector 合约,然后移回已恢复的 Vault,恢复正常操作。

全部评论(0)