ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Ethernaut 풀이] 이더리움을 해킹해보자 - 2.Fallout
    BlockChain/Technology 2021. 8. 11. 02:19

    오늘의 풀어볼 문제는 Fallout 입니다.

     

    이번 주제는 Rubixi라는 회사에서 일어났었던

    이더리움 생태계의 실화를 바탕으로 만들어진 내용입니다.

    공격자는 아주 간단한 방법으로 onwership을 주장하고, 일부 자금을 훔칠 수 있었습니다.

    ≪ Ethernaut 풀이 시리즈 

     

    [Ethernaut 풀이] 이더리움을 해킹해보자 - 0.Hello Ethernaut

    [Ethernaut 풀이] 이더리움을 해킹해보자 - 1.Fall back


    1. 목표 확인

    • 컨트랙트의 소유권을 주장하기

    이번 level은 목표도 하나이고, 하는 방법은 간단합니다. 그냥 ownership만 가져오면 됩니다.

    Get new instance 버튼을 눌러 인스턴스를 생성하고 시작합니다.

     

    2. 문제풀이

     

    이번에 주어진 컨트랙트의 코드입니다.

    // SPDX-License-Identifier: MIT
    pragma solidity ^0.6.0;
    
    import '@openzeppelin/contracts/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];
      }
    }

     

    ① Ownership 가져오기

     

    현재 owner는 level 컨트랙트가 owner로 설정되어 있습니다.

     

     

    ownership을 가져오려면 owner를 변경하는 부분을 찾으면 됩니다. 코드를 쭉 살펴보니 owner는 constrcutor부분에만 있습니다. 

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

    이 말인 즉슨 owner는 컨트랙트를 처음 배포할 때 정해졌으므로 그 뒤로 바꿀 수 없다는 말과 같습니다.

    그런데 owner를 바꾸는 것이 이번 미션의 목표입니다. 어떻게 하면 될까요? 

    힌트에는 Solidity Remix IDE가 나와 있습니다.

     

    Solidity Remix IDE는 솔리디티 언어를 사용해서 이더리움 스마트 컨트랙트를 컴파일/테스트/디버깅/배포하는 등 통합 개발 환경을 제공하는 툴입니다. 아래 사이트에서 별도의 설치없이 이용하실 수 있습니다.

    https://remix.ethereum.org/

     

    Remix - Ethereum IDE

     

    remix.ethereum.org

     

     

    ② Remix 이용하기

     

    Remix의 장점은 컴파일을 할 때 오류를 검사해주고 알려줍니다. Remix에서 Rinkeby 네트워크를 이용하기 위해서는 ethernaut에 연결할 때와 동일하게 메타마스크를 연결합니다. 

     

    컴파일 하기 전 준비 사항입니다.

    • 홈 탭에서 새로운 파일을 하나 만들어주고 fallout.sol이라고 이름을 지었습니다. 
    • Run 탭의 Environment를 Inject Web3를 선택하여 메타마스크를 연결합니다.
    • Compile 탭에서 Auto compile 옵션을 선택합니다.
    • fallout.sol에 복사한 소스를 붙여넣습니다. 

    이대로 배포를 해보려 하면 컴파일 에러가 납니다.

     

    4번째 줄 import부분에서 에러가 발생하고 있다.

    해당 에러가 나는 이유는 붙여넣기한 소스의 경우 truffle framework를 사용해서 작성된 코드여서 local의 Ownable.sol 파일을 가지고 왔지만 Remix에선 해당 파일을 불러올 수가 없기 때문입니다. openzeppeline에서 직접 코드를 가져와 붙여 넣어줘야 합니다.

    import 코드를 지워주고 아래 코드를 붙여 넣어줍니다.

    import 'https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v3.3.0/contracts/access/Ownable.sol';

     

    한가지 더 SafeMath라는 Library를 사용하고 있으므로 아래 코드도 붙여 넣어줍니다.

    import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v3.3.0/contracts/math/SafeMath.sol";

     

    이제 Run 탭에 가서 Deploy 버튼을 눌러 배포를 합니다.

    잠깐 배포를 하기 전에 우리는 직접 컨트랙트를 배포하는 것이 아니라 level 컨트랙트가 배포해 놓은 컨트랙트를 불러서 사용해야 됩니다. 콘솔에서 Instance address를 확인하고 Remix의 Load contract form Address 영역에 Instance의 주소를 입력하고 At address 버튼을 눌러 불러옵니다.

     

    불러온 컨트랙트를 살펴봅니다. 이상한 점이 있습니다.

     

     

    배포되었던 컨트랙트의 코드를 보면 Fallout 함수는 생성자였습니다. 지금은 construct()라는 생성자 문법을 쓰고 있지만 예전에는 컨트랙트의 이름과 함수의 이름이 같을 경우 생성자 역할을 하였습니다.

    하지만 remix로 불러와서 확인해본 결과 Fallout은 호출을 할 수 있었고, 자세히 들여다보니

    Fallout이 아니라 Fal1out 입니다. 오타가 났습니다.

     

    이 경우 오타가 난 Fal1out 함수를 호출하면 그대로 ownermsg.sender로 변경이 됩니다.

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

    이로써 이번 level의 목표가 달성되었습니다.

     

    목표 달성 ! 컨트랙트의 소유권을 주장하기

     

    이번 level은 하는 방법은 간단했지만 실제로 Rubixi에서 일어났던 사건입니다. 회사는 이름을 'Dynamic Pyramid'에서 'Rubixi'로 변경했지만 어떻게든 계약의 생성자 메서드 이름을 바꾸지 않았습니다.

    contract Rubixi {
      address private owner;
      function DynamicPyramid() { owner = msg.sender; }
      function collectAllFees() { owner.transfer(this.balance) }
      ...

     

    이를 통해 공격자는 이전 생성자를 호출하고 컨트랙트의 소유권을 주장하고 일부 자금을 훔칠 수 있었습니다.

    smartcontractland에서 큰 실수가 일어났고, 이런 취약점은 곧 손실로 이어졌습니다. 지금은 다행히 construct가 나와서 이와 같은 일이 반복될 일이 없지만 컨트랙트가 모두에게 공개되는 블록체인의 특성상 항상 조심하고 주의해야할 점입니다. 수고하셨습니다 ! 다음 level에서 만나요 ~

    댓글

Designed by Tistory.