// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
contract MagicNum {
address public solver;
constructor() public {}
function setSolver(address _solver) public {
solver = _solver;
}
/*
____________/\\\_______/\\\\\\\\\_____
__________/\\\\\_____/\\\///////\\\___
________/\\\/\\\____\///______\//\\\__
______/\\\/\/\\\______________/\\\/___
____/\\\/__\/\\\___________/\\\//_____
__/\\\\\\\\\\\\\\\\_____/\\\//________
_\///////////\\\//____/\\\/___________
___________\/\\\_____/\\\\\\\\\\\\\\\_
___________\///_____\///////////////__
*/
}
To solve this level, you only need to provide the Ethernaut with a Solver,
a contract that responds to whatIsTheMeaningOfLife() with the right number.
Easy right? Well... there's a catch.
The solver's code needs to be really tiny.
Really reaaaaaallly tiny. Like freakin' really really itty-bitty tiny: 10 opcodes at most.
Hint: Perhaps its time to leave the comfort of the Solidity compiler momentarily,
and build this one by hand O_o. That's right: Raw EVM bytecode.
Good luck!
Opcode에 대한 설명이 너무 잘 되어 있어서 사진까지 그대로 가져왔다. 좋은 글을 써주셔서 감사합니다..! 이더리움에서 컨트랙트를 작성하고 컴파일하면 작성된 코드는 컴파일러(solc)에 의해 바이트코드로 변환된다. 그래야 EVM이 알아들을 수 있기 때문이다. 이 때 리믹스에서 contract detail을 볼 수 있는데, 크게 3가지로 나뉜다.
사진으로 볼 수 있듯이 Runtime Bytecode
는 전체 컨트랙트가 컴파일 돼서 변한 바이트코드를 나타낸다. 나중에 디플로이될 때 data field에 들어간다.(앞에 0x
가 붙어서 들어가게 되는데 이는 바로 뒤에 나올 데이터 혹은 숫자가 16진수로 표현됨을 의미한다.) Opcodes
는 operation code라고도 불리는데 바이트코드에 비해 조금 더 사람이 읽기 쉽게(?) 변환시킨 코드라고 보면 된다. 마지막으로 Assembly
는 opcodes를 사용해 컨트랙트 전체를 작성할 수 있도록 해준다.
조금 더 살펴보자면 Opcodes
는 프로그램의 지시문이나 명령 등을 사람이 읽을 수 있게 만들어 놓은 것인데, 예를 들면 PUSH1
은 1비트 크기의 데이터를 stack에 넣으라는 뜻이다.(참고로 EVM은 stack 기반의 구조를 가지고 있다.) Runtime Bytecode
에서의 6060604052
는 Opcodes
에서 PUSH1 0x60 PUSH1 0x40 MSTORE
로 바뀔 수 있고, 이를 해석하면 "stack에 96바이트의 메모리를 할당해서 64바이트의 시작 부분으로 pointer를 옮겨라"가 된다. 이 때 0x60
은 96을 의미하고, 0x40
은 64를 의미한다.
컨트랙트를 컴파일 했을 때 만들어지는 Opcodes는 크게 2가지 영역으로 나뉘어진다. 첫 번째는 Creation
으로 처음 컨트랙트가 생성되는 부분을 말하며 대표적으로 contructor가 포함되어있다. 두 번째는 runtime
으로 실질적으로 컨트랙트가 동작하도록 하는 function selector와 function bodies가 포함되어있다. 문제를 해결할 때도 2가지 부분으로 나뉜다. Creation 부분과 Runtime 부분으로 구성된 opcodes를 만들어야 한다.
먼저 Runtime 부분부터 만들어보자. 힌트에서 나온 42라는 숫자를 반환하는 opcodes를 만들어야 한다.
602a - 0x2a는 42를 뜻한다. stack에 42를 넣어주고
6080 - 0x80 위치 값을 stack에 넣어준다.
52 - 0x52는 MSTORE(p,v)를 뜻한다. 2개의 인자로 위치값(position)과 값(value)을 받는다.
6020 - 0x20은 32를 뜻한다. stack에 32를 넣어주고
6080 - 0x80 위치 값을 stack에 넣어준다.
f3 - RETURN(position, number of bytes)
MSTORE를 통해 0x80위치에 42값이 메모리에 저장된다. 이후 2개의 인자 값을 받는 RETURN을 통해 0x80 위치에 있는 32bytes 크기의 value를 반환한다.
이제 컨트랙트가 생성되는 부분을 만들어보자.
600a - 0x0a는 10을 뜻한다. 10bytes 크기로 Runtime Opcodes를 만들었으니 10을 stack에 넣어준다.
60?? - 현재 복사 대상의 위치를 stack에 넣어줘야 한다. 아직 정확한 위치를 모르니 일단 보류.
6000 - 붙여넣기 할 목적지 위치를 stack에 넣어준다.
39 - 0x39는 COPYCODE(t, f, s)를 뜻한다. 위 3개 인자를 받아서 실행된다.
600a - 0x0a는 10을 뜻한다. RETURN에 들어갈 크기값을 stack에 넣어준다.
6000 - 붙여넣기 한 곳의 위치값을 stack에 넣어준다.
f3 - RETURN(position, number of bytes)
COPYCODE(t, f, s)는 3개의 인자를 받는다.
t
: 붙여넣기 할 목적지의 위치값f
: 복사 대상의 현재 위치값s
: 복사 대상의 크기stack의 구조상 가장 나중에 쌓인 값을 가장 먼저 처리하기 때문에 Opcodes의 순서도 복사 대상의 크기가 제일 먼저 나오고 목적지의 위치값이 마지막에 나온다. 이후 RETURN을 통해 복사된 위치와 10bytes 크기의 value를 반환한다.
이제 앞에서 만든 2개의 opcodes를 합치면 된다. Creation의 opcodes는 60??600c600039600a6000f3
가 되고, Runtime의 opcodes는 602A60805260206080f3
가 된다. 이때, 복사 대상의 위치는 Creation 부분의 가장 마지막 부분이자 Runtime의 가장 처음 부분이 되므로 첫 부분부터 세어보면 12bytes가 된다.(2자리 = 1bytes, 총 24자리이므로 12bytes) 따라서 ??
부분은 12를 뜻하는 0x0c
가 된다. 이제 전부 합치면 다음과 같은 최종 opcodes가 나온다..!!
0x600a600c600039600a6000f3602A60805260206080f3
이제 만든 opcodes를 다음과 같이 sendTransaction
을 통해 deploy해주고
BYTECODE = "0x69602a60005260206000f3600052600a6016f3"
txn = web3.eth.sendTransaction({from: player, data: BYTECODE })
문제에서 요구했던 setSolver
에 인자값으로 주소를 넣어주면 끝!
contractAddress = txn.address
await contract.setSolver("contractAddress");
역대급으로 어려운 문제였다.. 푸는 게 아니라 이해하는데만 며칠 걸린 것 같다.. EVM의 본질적인 이해가 좀 더 필요하다..