To solve this level, you only need to provide the Ethernaut with a Solver, a contract that responds to whatIsTheMeaningOfLife() with the right 32 byte 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 bytes 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!
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract MagicNum {
address public solver;
constructor() {}
function setSolver(address _solver) public {
solver = _solver;
}
/*
____________/\\\_______/\\\\\\\\\_____
__________/\\\\\_____/\\\///////\\\___
________/\\\/\\\____\///______\//\\\__
______/\\\/\/\\\______________/\\\/___
____/\\\/__\/\\\___________/\\\//_____
__/\\\\\\\\\\\\\\\\_____/\\\//________
_\///////////\\\//____/\\\/___________
___________\/\\\_____/\\\\\\\\\\\\\\\_
___________\///_____\///////////////__
*/
}
문제부터 대충 해석하자면은....
저 solver주소에 있는 컨트랙트에서 whatIsTheMeaningOfLife()를 호출할건데 리턴값이 32바이트 숫자여야한다.
일단 코드에서 볼 수 있듯이 42라는 숫자가 있는데 뭐 해외에서 유명한 밈인가보다 여튼 42를 32바이트로 리턴하라는 뜻이다.
그런데 봐야할건 코드가 매우매우 작은 길이여야 한다는 것이다.
즉, 컨트랙트의 코드 길이가 10bytes이하여야 한다.
그냥 solidity로 배포하면은 코드길이가 당연히 10bytes이상이 되어버리기에
EVM bytecode를 사용해야한다.
EVM bytecode란 어셈블리어랑 비슷하게 솔리디티에서 사용 가능하다.
opcode랑 각 value등을 맞춰서 하면 된다.
일단 컨트랙트는 초기화 바이트코드+런타임 바이트코드 이렇게 되어있다.
초기화 바이트코드는 constructor에 해당하는 부분으로 컨트랙트를 초기화하는 바이트 코드이고
런타임바이트코드는 컨트랙트에서 호출가능한 함수들의 로직으로 구성되어있다.
즉, 여기서 10bytes이내라는 거는 런타임 바이트코드가 10bytes이내로 되어야 한다는 것이다.
일단 초기화 바이트코드부터 먼저 짜주겠다.
초기화 바이트코드
# Initialization Opcodes
600a // s: push1 0x0a
600c // f: push1 0x0c
6000 // t: push1 0x00
39 // CODECOPY
600a // s: push1 0x0a
6000 // p: push1 0x00
f3 // return to EVM
# to Hex
초기화에선 런타임 코드를 옮겨준 후 리턴해야하는데 코드를 옮길때는 CODECOPY를 사용할 수 있다.
1. 600a (length)
60은 PUSH1의 opcode이고 0a는 10이다.
즉, 런타임바이트코드가 길이가 10이라는걸 뜻한다.
2.600c (offset)
초기화 바이트코드는 현재 계산하면 0x0c만큼이다.
즉, 런타임바이트코드가 0x0c위치 뒤에 위치한다는 걸 뜻한다.
3. 6000 (destOffset)
메모리 시작 위치를 push한다.
여기선 0x00에서 메모리가 시작한다는걸 뜻한다.
4. 39 (CODECOPY opcode)
런타임코드를 초기화 코드의 0x0c위치에서 메모리의 0x00위치로 복사한다는 것이다.
즉 런타임코드를 옮기는거다.
그다음 이 메모리에 옮긴 값을 리턴해야 한다.
Return의 opcode는 다음과 같다.
5. 600a (length)
10만큼의 길이를 반환한다는 것이다.
즉, 런타임바이트코드를 반환하는 뜻이다.
6. 6000 (offset)
아까 0x00에 위치시켰으니 0x00부터 리턴한다.
7. f3 (opcode)
return opcode이다.
이렇게 하면 초기화 바이트코드는 끝이다.
런타임 바이트코드
실질적으로 whatIsTheMeaningOfLife()함수를 실행시켰을때 32바이트 42를 반환하는 코드이다.
최대한 꾹꾹담아 10bytes로 만들어야 한다.
# Runtime Opcodes
6042 // v: push1 0x2A
6080 // p: push1 0x80
52 // opcode mstore
6020 // s: push1 0x20
6080 // p: push1 0x80
f3 // return
# to Hex
리턴할려면 이 역시 메모리에 값을 둬야하는데 여기선 코드가 아닌 숫자로 mstore을 사용해 메모리에 값을 쓸 수 있다.
1. 6042 (value)
0x2A 즉, 42를 반환한다.
2. 6080 (offset)
0x80위치에 두는 것이다.
뭐 32바이트 값을 리턴해줘야 하기에 위치가 32배수면 상관없다.
3. 52 (opcode)
mstore opcode이다.
이 다음부턴 다시 return이다.
4. 6020 (length)
0x20 즉, 32바이트만큼 리턴한다.
5. 6080 (offset)
0x80위치에 두었으니 해당 위치부터 값을 리턴한다.
6. f3 (opcode)
return opcode이다.
이제 초기화 바이트코드와 런타임바이트코드를 합치면 아래와 같이 나온다.
600a600c600039600a6000f3602A60805260206080f3
위 바이트코드는 어떠한 함수가 호출되어도 42값을 32바이트 크기로 리턴한다.
바이트코드를 기준으로 contract생성은 assembly의 create를 사용하면 된다.
이때, 바이트코드는 앞에 hex를 붙여야 한다.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "forge-std/console.sol";
import "forge-std/Script.sol";
import "../src/magic_number.sol";
contract Attack {
MagicNum public target;
address public solverAddr;
constructor(MagicNum _target){
target = _target;
bytes memory bytecode = hex"600a600c600039600a6000f3602A60805260206080f3";
address addr;
assembly {
addr := create(0, add(bytecode, 0x20), mload(bytecode))
}
console.log(addr);
solverAddr= addr;
}
function attack() external{
target.setSolver(solverAddr);
}
}
contract MagicNumberSolve is Script {
MagicNum public target;
function setUp() external{
address payable targetAddress = payable(0xD91253B08a328BfB58da9e4720b205D9d29805A7);
target = MagicNum(targetAddress);
}
function run() external {
vm.startBroadcast(vm.envUint("user_private_key"));
Attack attacker = new Attack(target);
attacker.attack();
vm.stopBroadcast();
}
}
이렇게 컨트랙트를 배포하고 이더스캔을 보면은 성공적으로 10바이트의 런타임 바이트코드가 배포된걸 확인 가능하다.
'Web3 > The Ethernaut' 카테고리의 다른 글
[The Ethernaut] Alien Codex (0) | 2025.02.03 |
---|---|
[The Ethernaut] Recovery (0) | 2025.02.02 |
[The Ethernaut] Preservation (0) | 2025.02.02 |
[The Ethernaut] Naught Coin (0) | 2025.02.02 |
[The Ethernaut] Gatekeepr Two (0) | 2025.01.31 |