Ethernaut 18. Magic Number

독수리박박·2024년 7월 26일
post-thumbnail

level 18. magic number
이번 문제는 10개 이해 opcode(evm code)를 사용해 숫자 42를 반환하는 컨트랙트를 배포해야 합니다.

Problem


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

contract MagicNum {
    address public solver;

    constructor() {}

    function setSolver(address _solver) public {
        solver = _solver;
    }

    /*
    ____________/\\\_______/\\\\\\\\\_____        
     __________/\\\\\_____/\\\///////\\\___       
      ________/\\\/\\\____\///______\//\\\__      
       ______/\\\/\/\\\______________/\\\/___     
        ____/\\\/__\/\\\___________/\\\//_____    
         __/\\\\\\\\\\\\\\\\_____/\\\//________   
          _\///////////\\\//____/\\\/___________  
           ___________\/\\\_____/\\\\\\\\\\\\\\\_ 
            ___________\///_____\///////////////__
    */
}

10 이하의 opcode로 구성된 컨트랙트를 배포하고 setSolver를 통해 등록해 문제를 해결하면 됩니다.

이 문제를 해결하기 위해서는 opcode와 컨트랙트 구조에 대한 이해가 필요합니다.

따라서 취약점 분석보다는 필요한 개념에 대해 설명하겠습니다.

Init code, Runtime code(Opcode)


Init code

Init code는 말 그대로 초기화, 즉 배포 코드 입니다. 컨트랙트가 배포 될때 실행되는 바이트 코드입니다.
다만 컨트랙트가 배포되고 난 이후에는 CA(contract)에 저장이 되지 않습니다. 저희가 컨트랙트를 호출할 때 실행되는 코드는 런타임 코드로 Init code는 컨트랙트의 실행과는 상관이 없습니다.

배포 코드의 목적은 런타임 코드를 EVM에 리턴 시켜주는 것 입니다.
Contract deploy tx는 아래와 같습니다.

{
  "to": null,
  "value": msg.value,
  "data": "<init_code><runtime_code><constructor_args (option)>"
}

생성자 인자와 함께 init code가 실행되고 초기화(생성자 함수 내부 로직)가 진행되며 EVM에 runtime code를 반환해 줍니다.

배포 코드 역시 opcode로 직접 구성할 수 있겠지만 굳이 안그래도 됩니다. constructor 함수 안에 10개 이하의 opcode로 이뤄진 runtime code를 넣고 return만 시켜주면 됩니다.

runtime code(Opcode)

이제 CA에 저장되며 호출되어 컨트랙트 내부 로직을 수행하는 runtime code를 직접 opcode로 구성해보겠습니다. etherscan Contract address를 치고 들어가서 볼 수 있는 컨트랙트 바이트 코드가 바로 이 runtime code 입니다.

evm codes
위 사이트에 들어가면 opcode들을 확인할 수 있고 PlayGround에서 실습도 가능합니다.

EVM은 스택 기반입니다. 따라서 어떤 데이터를 메모리나 스토리지에 올리거나 return 시켜줄 경우에는 우선 필요한 값들을 스택에 넣어두어야 합니다.

  1. 메모리에 리턴해줄 데이터 저장
    • push1 0x2a(42) == 60 2a
    • push0 offset(memory) 5f
    • mstore 52
      여기까지가 opcode로 602a5f52입니다. offset은 원하는 곳으로 지정하면 됩니다. 그리고 push0과 나머지 push들은 1가스가 차이나기 때문에 0을 push할 때에는 push0을 하는 것이 좋습니다. 미미한 값이긴 하지만...
  2. 메모리 값 리턴 시키기
    리턴에 필요한 값들을 스택에 넣어주어야 합니다.
    데이터 길이(32bytes) push1 32 == 60 20
    데이터 위치(0) push0 == 5f
    return == f3
    이렇게 6개의 opcode만 사용하고 42를 return 시켜줄 runtime code를 완성했습니다.

Solution

완성한 공격 코드는 아래와 같습니다.

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

interface IMagicNumber {
     function setSolver(address _solver) external;
}

contract Attack {
    constructor(address _target) {
        IMagicNumber tartget = IMagicNumber(_target);
        tartget.setSolver(address(this));

        assembly {
            mstore(0,0x602a5f5260205ff3)
            return (0x18, 0x08)
        }
    }
} 

inline assembly를 사용해 직접 메모리로 올리고 return 시켜주었습니다.

배포된 컨트랙트의 코드 영역에는 위와 같이 저희가 EVM에 리턴한 런타임 코드만 존재합니다.

0개의 댓글