티스토리 뷰

🦅 이전글

2023.10.03 - [Solidity] - [Solidity] 재진입 공격 (Reentrancy Attack)


 

🚫 재진입 공격 예방 기법

1. Checks Effects Interactions 패턴

 Checks Effects Interactions 패턴은 컨트랙트 내부에서 외부 주소를 호출할 때 적용할 수 있는 패턴입니다. 외부 함수를 호출하거나 이더를 다른 계정으로 보낼 때, 이를 호출하는 컨트랙트는 제어 흐름을 외부 개체에게 넘겨주게 됩니다. 이러한 이유로 재진입 공격도 가능한 것입니다. 그렇기 때문에 외부 주소를 호출하기 전에 충분한 검토가 필요합니다. 먼저 가능한 모든 조건들을 검사(checks)하고, 컨트랙트 내부에서 실행가능한 모든 것들을 적용(effects)한 뒤에 마지막으로 외부 주소를 호출(interactions) 해야 합니다.

 

 이하는 Checks Effects Interactions 패턴을 적용하여 수정한 withdraw 함수입니다.

function withdraw() public {
	uint currentBalance  = balances[msg.sender];

	if (currentBalance == 0) { // checks
		revert();
	}

	balances[msg.sender] = 0; // effects

	(bool success,) = payable(msg.sender).call{value: currentBalance}(""); // interactions
	if (!success) {
		balances[msg.sender] = currentBalance;
		revert();
	}
}

 실행 순서는 다음과 같습니다.

 

  1. BadBank 컨트랙트에 3 이더가 들어있는 상태에서 Attack 컨트랙트에서 1 이더를 가지고 attack 함수를 호출합니다.
  2. 먼저 1 이더를 deposit 함수로 저장합니다. Attack 컨트랙트의 잔액은 1 이더입니다.
  3. withdraw 함수를 호출합니다.
  4. Attack 컨트랙트의 잔액(1 이더)을 currentBalance에 저장하고 currentBalance가 0인지 확인합니다.
  5. Attack 컨트랙트의 잔액을 0으로 만듭니다.
  6. call 함수를 호출하여 Attack 컨트랙트에게 currentBalance만큼의 이더를 전송합니다.
  7. Attack 컨트랙트의 receive 함수가 실행됩니다. BadBank 컨트랙트의 잔액이 3 이더이므로 withdraw 함수로 재진입합니다.
  8. Attack 컨트랙트의 잔액(0 이더)을 currentBalance에 저장하고 currentBalance가 0인지 확인합니다.
  9. currentBalance가 0이기 때문에 revert됩니다.
  10. call -> receive -> withdraw 순으로 호출된 순서에서 revert된 결과가 withdraw -> receive -> call로 되돌아감에 따라 call 함수의 호출 결과가 false를 반환합니다.
  11. 조건문에서 false를 받음에 따라 함수 호출이 revert되고 초기 상태도 되돌아가면서 공격은 실패하게 됩니다. 

 

2. Mutex 사용하기

 mutex는 여러 개체가 동일한 자원에 동시에 접근하는 것을 제한하기 위한 매커니즘으로, 솔리디티에서는 이미 실행중인 함수에 재진입하는 것을 제한하기 위해 사용할 수 있습니다.

 

 이하는 Mutex 추상 컨트랙트입니다. 함수수정자(modifier)를 사용하여 함수를 잠그고 만약 재진입하는 경우 revert를 실행하여 함수 실행을 취소하고 초기 상태로 되돌려 놓습니다.

abstract contract Mutex {
    bool internal locked;

    modifier lock() {
        if (locked) {
            revert();
        }

        locked = true;
        _;
        locked = false;
    }
}

 Mutex 추상 컨트랙트는 다음과 같이 상속을 통해서 사용할 수 있습니다. 상속으로 Mutex의 lock 함수수정자를 가져와, withdraw 함수에 적용하였습니다.

contract BadBank is Mutex {
    mapping (address => uint) balances;

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

    function withdraw() public lock {
        (bool success,) = payable(msg.sender).call{value: balances[msg.sender]}("");
        if (!success) {
            revert();
        }
        balances[msg.sender] = 0;
    }

    function getBalance() public view returns (uint) {
        return address(this).balance;
    }
}

 이 경우에 실행 순서는 다음과 같습니다.

 

  1. BadBank 컨트랙트에 3 이더가 들어있는 상태에서 Attack 컨트랙트에서 1 이더를 가지고 attack 함수를 호출합니다.
  2. 먼저 1 이더를 deposit 함수로 저장합니다. Attack 컨트랙트의 잔액은 1 이더입니다.
  3. withdraw 함수를 호출합니다.
  4. lock 함수수정자가 먼저 실행되어 locked 값을 true로 변경합니다. 그리고 withdraw 함수가 실행됩니다.
  5. call 함수를 호출하여 Attack 컨트랙트에게 Attack 컨트랙트의 잔액(1 이더)을 전송합니다.
  6. Attack 컨트랙트의 receive 함수가 실행됩니다. BadBank 컨트랙트의 잔액이 3 이더이므로 withdraw 함수로 재진입합니다.
  7. lock 함수수정자가 실행됩니다. locked의 값이 true이므로 revert됩니다.
  8.  revert된 결과가 lock -> receive -> call로 되돌아감에 따라 call 함수의 호출 결과가 false를 반환합니다.
  9. 조건문에서 false를 받음에 따라 함수 호출이 revert되고 초기 상태도 되돌아가면서 공격은 실패하게 됩니다.


🙏 마치며

 재진입 공격을 예방하는 기법에 대해 알아보았습니다. 스마트 컨트랙트는 실제 돈이 오고가기 때문에 무심코 지나칠 수 있는 작은 코드 조각이 어마어마한 재앙을 불러일으킬 수 있다는 사실! 개인적인 생각으로는 가스비가 조금 더 나오더라도 두 가지 방법을 모두 사용하는 것도 나쁘지 않을 것 같습니다. 보안적인 요소들은 돈을 쓴 만큼 또 제 값을 하니까요!


🐱‍👤 소스 코드

 

ReentrancyAttack

ReentrancyAttack. GitHub Gist: instantly share code, notes, and snippets.

gist.github.com


📖 참고자료

 

Checks Effects Interactions

A compilation of patterns and best practices for the smart contract programming language Solidity

fravoll.github.io

 

Solidity by Example

 

solidity-by-example.org

글에서 수정이 필요한 부분이나 설명이 부족한 부분이 있다면 댓글로 남겨주세요!
최근에 올라온 글
최근에 달린 댓글
«   2025/01   »
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 31
Total
Today
Yesterday
글 보관함