智能合约经典漏洞案例,xSurge 重入漏洞+套利 综合运用

智能合约经典漏洞案例,xSurge 重入漏洞+套利 综合运用

1. 事件介绍

xSurge 被攻击事件发生在 2021-08-16 日,距离今天已经近 1 年了,为什么还会选择这个事件进行分析?主要是这个攻击过程很有意思,有以下的几点思考

  • 使用 nonReentrant 防止重入,又在代码中又不遵循检查-生效-交互模式(checks-effects-interactions)时,要怎么利用?
  • 该漏洞利用的代码是重入漏洞的典型代码,但利用过程却不是重入。应该是”最像重入漏洞的套利漏洞”。
  • 随着 ERC20 合约的规范化,ERC20 的合约漏洞越来越少。xSurge 会不会是 ERC20 合约最后的漏洞?
  • 价格计算机制既然可以从数学上证明存在漏洞,能不能用数学的方法来挖掘此类的漏洞?
  • 该攻击过程中与闪电贷的完美结合。所有的攻击成本只有 gas 费,漏洞利用的大资金从闪电贷中获得,攻击获利后归还闪电贷的本息。闪电贷使攻击的收益损失比大大大大地提高。

xSurge 是一个基于 bsc 链的 Defi 的生态系统,其代币为 xSurgeToken,用户可以通过持有 xSurgeToken 获得高收益回报,同时可以将 xSurgeToken 用于其 Defi 生态中的。xSurgeToken 遵循 ERC20 协议,初始供应量为 10 亿枚,随着用户的对 xSurgeToken 的转入转出,xSurgeToken 的价格会动态进行调整。
在 2021-08-16 日,黑客通过 xSurgeToken 合约代码中的漏洞,窃取了 xSurgeToken 合约中的 12161 个 BNB。具体来说黑客采用闪电贷出的 10000 WBNB 做为初始资金,通过代码漏洞套利,淘空了 xSurgeToken 合约的 BNB 余额,同时套利使 xSurgeToken 的价格下降了 7 千多倍。
本文将重点分析和复现 xSurge 的攻击过程。

2. 漏洞分析

漏洞可以被利用主要在于两点:

  1. sell 函数中先转了 xSurge 代币,后进行了余额的减操作,导致余额会在短暂时间内留存,这笔余额被黑客用来再次购买了 xSurge。
  2. xSurge 的价格计算过程中,在有更多的买入时,会导致 xSurge 的价格变低,xSurge 的价格变低,又会导致可以购买更多的 xSurge,类似于滚雪球,越滚越大,最终把 xSurge 合约掏空。同时,叠加黑客利用闪电贷,贷到 10000 个 WBNB 进行攻击,这么大一笔钱对 xSurge 价格影响更大,从而导致循环调用 8 次就可以掏空 xSurge 合约。

对这前两点进行分析说明

2.1 漏洞代码分析

漏洞代码链接如下:
https://bscscan.com/address/0xe1e1aa58983f6b8ee8e4ecd206cea6578f036c21#code
漏洞涉及到三个函数,:

  1. sell():卖出 xSurge,把卖出得到的 BNB 发送给出售者(也就是函数调用者)
  2. purchase():买入指定数量的 xSurge。该函数为 internal 类型,只能由下面的 recive()函数调用。
  3. receive():receive 不是普通函数,是 solidity 的回调函数。当 xSurge 合约收到 BNB 时,该函数会自动执行,在该函数中调用了 purchase()函数。

漏洞点在于:sell()函数中调用了 call 函数后才进行 balance 的减法操作,攻击者通过 call 函数获得代码控制权后,在 balance 还没减掉的情况下,攻击者调用 purchase 方法进行购买,达到了类似套利的效果。
众所周知,“更高成本带来更高的收益”,黑客看到这种机会,也期望着有更多的投入来套取更大的收益,于是,攻击者为了扩大这种攻击效果,使用了闪电贷,从 Pancake 中贷出来 10000 个 BNB 进行攻击,在一个区块中经过 8 次的“套利”,获得了 12161 个 BNB。

在代码中,看到攻击利用的关键点

/** Purchases SURGE Tokens and Deposits Them in Sender's Address*/
// 利用关键点3:这里调用了mint时,balance还是没有减之前的,所以mint出来的肯定会多一些。
    function purchase(address buyer, uint256 bnbAmount) internal returns (bool) {
        // make sure we don't buy more than the bnb in this contract
        require(bnbAmount <= address(this).balance, 'purchase not included in balance');
        // previous amount of BNB before we received any
        uint256 prevBNBAmount = (address(this).balance).sub(bnbAmount);
        // if this is the first purchase, use current balance
        prevBNBAmount = prevBNBAmount == 0 ? address(this).balance : prevBNBAmount;
        // find the number of tokens we should mint to keep up with the current price
        uint256 nShouldPurchase = hyperInflatePrice ? _totalSupply.mul(bnbAmount).div(address(this).balance) : _totalSupply.mul(bnbAmount).div(prevBNBAmount);
        // apply our spread to tokens to inflate price relative to total supply
        uint256 tokensToSend = nShouldPurchase.mul(spreadDivisor).div(10**2);
        // revert if under 1
        if (tokensToSend < 1) {
            revert('Must Buy More Than One Surge');
        }

        // mint the tokens we need to the buyer
        mint(buyer, tokensToSend);  **// 到这里就成功了。**
        emit Transfer(address(this), buyer, tokensToSend);
        return true;
    }

    /** Sells SURGE Tokens And Deposits the BNB into Seller's Address */
    function sell(uint256 tokenAmount) public nonReentrant returns (bool) {

        address seller = msg.sender;

        // make sure seller has this balance
        require(_balances[seller] >= tokenAmount, 'cannot sell above token amount');

        // calculate the sell fee from this transaction
        uint256 tokensToSwap = tokenAmount.mul(sellFee).div(10**2);

        // how much BNB are these tokens worth?
        uint256 amountBNB = tokensToSwap.mul(calculatePrice());

        // 利用关键点1:漏洞地方,在call中程序的控制权被转移到攻击者手里。但是在攻击balance的数量却还没有少
        (bool successful,) = payable(seller).call{value: amountBNB, gas: 40000}("");
        if (successful) {
            // subtract full amount from sender,在这里才开始减掉balance的数量
            _balances[seller] = _balances[seller].sub(tokenAmount, 'sender does not have this amount to sell');
            // if successful, remove tokens from supply
            _totalSupply = _totalSupply.sub(tokenAmount);
        } else {
            revert();
        }
        emit Transfer(seller, address(this), tokenAmount);
        return true;
    }

// 利用关键点2:攻击合约得到程序控制权后,直接向xsurge合约进行转账,从而触发xsurge合约的receive函数的调用。这里会调用purchase函数
receive() external p
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值