Pwnstar

Ethernaut Level 1(Fallback) 본문

Wargame/Ethernaut

Ethernaut Level 1(Fallback)

포너블처돌이 2023. 1. 31. 23:17

코드를 보고 컨트랙트의 소유권을 획득하거나 balance를 0으로 만들면 된다고 한다.

  • ABI와 상호작용할 때 이더를 어떻게 보내는 지
  • ABI 밖에서 이더를 어떻게 보내는지
  • wei와 ether를 전환
  • Fallback 메서드

다음과 같은 것들을 알면 문제 풀이에 도움이 될 것이라고 한다.

여기서 ABI는 컨트랙트 내의 함수를 호출하거나 컨트랙트로부터 데이터를 얻는 방법이다.

solidity 코드가 있으니 보고 분석을 해 보자.

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

contract Fallback {

  mapping(address => uint) public contributions;
  address public owner;

  constructor() {
    owner = msg.sender;
    contributions[msg.sender] = 1000 * (1 ether);
  }

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

  function contribute() public payable {
    require(msg.value < 0.001 ether);
    contributions[msg.sender] += msg.value;
    if(contributions[msg.sender] > contributions[owner]) {
      owner = msg.sender;
    }
  }

  function getContribution() public view returns (uint) {
    return contributions[msg.sender];
  }

  function withdraw() public onlyOwner {
    payable(owner).transfer(address(this).balance);
  }

  receive() external payable {
    require(msg.value > 0 && contributions[msg.sender] > 0);
    owner = msg.sender;
  }
}

  mapping(address => uint) public contributions;
  address public owner;

우선 변수.

mapping은 키 ⇒ 값 쌍에 대한 해시 조회 테이블이다

contributions 변수의 key type은 address이고 value type은 uint이다.

owner 변수는 address 형의 변수로 20바이트의 이더리움 주소를 값으로 가진다.

constructor() {
  owner = msg.sender;
  contributions[msg.sender] = 1000 * (1 ether);
}

생성자. 변수의 값을 초기화하는 역할

owner 변수는 msg.sender(컨트랙트 호출을 시작한 주소, 만약 EOA 트랜잭션이 컨트랙트를 직접 호출했다면 트랜잭션에 서명한 주소가 되지만 그렇지 않다면 컨트랙트 주소가 된다.)

현재 owner는 level address로 초기화되어있다. 이걸 내 주소로 바꾸거나 해야하는 것으로 보인다.

처음 msg.sender(owner)의 값은 1000 ether로 초기화되어있다.

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

함수 변경자이다. 함수 선언에 modifier라는 이름을 추가해서 해당 함수가 호출될 때의 조건을 설정해줄 수 있다.

  function contribute() public payable {
    require(msg.value < 0.001 ether);
    contributions[msg.sender] += msg.value;
    if(contributions[msg.sender] > contributions[owner]) {
      owner = msg.sender;
    }
  }

contribute 함수에는 payable이라는 구문이 붙어있는데, 이는 가상화폐로 접근하기 위한 키워드이다. ether를 전송하는 컨트랙트 작성을 위해 사용한다.

require은 입력값이 설정한 조건의 기댓값에 맞는 지 확인할 때 사용한다.

msg.value 즉 이 호출과 함께 전송된 이더의 값이 0.001 ether보다 작을 때에만 이 함수가 실행된다.

contributions[msg.sender]에 전송된 이더의 값을 더해주고

만약 호출자의 contributions의 값(contributions[msg.sender])이 컨트랙트 소유자의 contributions의 값(contributions[owner])보다 크다면 호출자가 owner가 될 수 있다.

그런데 호출자의 contributions의 값이 생성자를 통해 1000 ether로 초기화되어있으니 이 값보다 커야할 것이다.

  function getContribution() public view returns (uint) {
    return contributions[msg.sender];
  }

호출자의 contributions 값을 반환해주는 함수이다. 그런데 이 함수를 호출해주면 이해를 할 수 없는 값이 나온다. 이건 내가 이해를 잘 못하는 건지, 좀 더 공부가 필요할 것 같다.

  function withdraw() public onlyOwner {
    payable(owner).transfer(address(this).balance);
  }

코드가 한줄이지만 익숙치 않아서 차근차근 분석해보자.

  • this는 호출이 이루어진 컨트랙트의 인스턴스를 나타낸다.
  • address(this)는 호출이 이루어진 컨트랙트의 인스턴스의 주소를 나타낸다.
  • balance는 특정 주소의 잔액을 나타낸다.
  • address.transfer(value)는 value 값을 address로 전송시킨다.
  • payable(address)는 이 주소가 payable하게 만들어준다.

즉, owner의 주소에 컨트랙트 인스턴스의 주소가 가진 이더의 잔액을 전부 전송하는 코드로 볼 수 있다.

이 함수는 앞서 언급한 modifier가 붙은 함수이다. 호출자와 owner가 동일할 때에만 실행할 수 있으며 owner가 아닌 호출자가 함수를 호출할 경우

이렇게 caller is not the owner라는 경고 메세지를 띄워준다.

  receive() external payable {
    require(msg.value > 0 && contributions[msg.sender] > 0);
    owner = msg.sender;
  }

external이라는 구문이 붙은 함수이고, 명시적으로 this가 앞에 붙어야만 컨트랙트 내에서 호출할 수 있다.

require로 조건이 붙는데, 전송된 이더의 값이 0보다 크고 호출자의 contributions의 값이 0보다 클 경우 호출자가 owner가 된다.

이렇게 간단한? 코드 분석이 끝났는데, 호출자(나)를 owner로 만드는 방법은 contribute 함수놔 receive 함수를 실행하는 방법이다.

그런데 contribute함수로 contributions[owner]를 넘게 만들려면 0.001보다 적은 양의 ether를 보내서 1000 ether를 넘겨야 하는데, 이러면 100000번 이상 실행시켜야 하니 receive 함수를 실행시키는 방법으로 해보자.

receive 함수는 외부에서 이더를 받을 때 작동하는 함수이다. 문제 도움말 중 ABI 밖에서 이더를 보내는 방법을 알면 도움이 될 것이라고 했는데 이 의미일 것으로 생각된다.

근데 이제 진짜 문제가 외부에서 보내는 방법을 도저히 모르겠다.

구글에서 검색을 해 보다가 콘솔로 이더를 보내는 방법을 찾아서 시도해 보았다.

sendTransaction() 이라는 함수를 이용해서 이더를 보낼 수 있는데,

contract에도 contract.contribute에도 sendTransaction함수를 호출할 수 있는데, 둘 다 시도해본 결과 contract.sendTransaction() 을 사용하여 contract의 owner를 변경하는데 성공했다.

이유는 아마 receive가 외부에서 이더를 받을 때 실행되는 함수인데, contribute() 함수는 내부 함수이기에 receive가 실행되지 않았을 것이라고 생각된다.

ownership을 획득했으니 이제 balance를 0으로 만들어보자.

이건 withdraw함수를 사용하면 될 것 같다.

withdraw 함수를 사용하여 instance의 balance를 0으로 만든 것을 확인할 수 있고,

submit을 해보면 문제 풀이에 성공했다고 알려준다.


라이트업을 전혀 안보고 문제를 풀려니 참 오래 걸리는 것 같다.

💡
계속 공부를 하면서 이해도를 높여가는 중이지만 중간중간 틀린 내용이 있다면 보시는 분들이 계시다면 짚어주신다면 감사히 피드백을 받아 적용하도록 하겠습니다.


Uploaded by N2T

'Wargame > Ethernaut' 카테고리의 다른 글

Ethernaut Level 4(Telephone)  (0) 2023.03.12
Ethernaut Level 3(Coin Flip)  (1) 2023.03.09
Ethernaut Level 2(Fal1out)  (0) 2023.02.21
Ethernaut Level 0(Hello Ethernaut)  (0) 2023.01.27
Ethernaut 문제풀이를 위한 환경설정  (0) 2023.01.27
Comments