// 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 make_contact() public {
contact = true;
}
function record(bytes32 _content) contacted public {
codex.push(_content);
}
function retract() contacted public {
codex.length--;
}
function revise(uint i, bytes32 _content) contacted public {
codex[i] = _content;
}
}
우리의 mission은 owner를 탈취하는 것이다.
흐음.. 그런데 여기에 owner를 가질 수 있는 함수가 없다.
Ownable-0.5.sol을 보면 _transferOwnership() 함수를 이용하여 _owner를 탈취할 수 있을 것 같다.
그런데 이 함수들은 전부 owner일 때만 가능하다.
이 문제는 Storage 저장 방식에 관한 문제이다. 코를 잘 보면 Secure coding이 부족한 부분이 있다. 바로 retract() 함수이다.
보통 Secure coding을 위해 +=, -= 을 .add, .sub로 설정하고, push, pop을 사용하면 안전한 함수이다. 그런데 retract() 함수는 codex.length--로 underflow가 일어난다.
Storage에 저장되는 조건이 4가지 있다.
Storage slot의 첫 번째 항목은 하위 순서로 정렬되어 저장된다.
값 유형은 저장하는 데 필요한 만큼의 바이트만 사용한다.
값 유형이 Storage slot의 나머지 부분에 맞지 않으면 다음 Storage slot에 저장된다.
구조체 및 배열은 항상 새 slot에 시작하고 채워진다.
그리고 동적배열 같은 경우는 크기가 정해지지 않아서 다른 변수들 사이에 저장될 수가 없다. 그래서 외부 저장 장소에 동적 배열을 저장한다. 외부 저장 장소이지만 2 ** 256 범위 안에 있으며 계산은 keccak256으로 한다.
즉 새로운 slot p에 저장된다하면 keccak256(p)의 위치에 저장이 된다.
문제에서 같이 한 번 보자. 해당 컨트랙트의 함수를 호출하려면 contact가 true가 되어야 한다.
그러면 make_contact()함수를 호출한 다음 나머지 함수 호출이 가능하다.
이제 해당 Contract의 Storage를 보자
slot0은 contact랑 owner가 같이 있다. 그리고 다음 slot부터 codex이 들어가는데 그림으로 Storage를 살펴보자
Storage를 보면 2 ** 256개의 저장공간이 있고 저장공간 하나당 32byte가 들어간다. codex은 동적배열이니까 keccak256(1)을 한 위치에 codex[0]이 들어간다. 실제로 한 번 확인해 볼까??
확실히 keccak(1)에 내가 입력한 값이 들어가 있다.
input의 시작은 keccak256(1)부터 시작이다. 그리고 owner index에 접근하기 위해 ((2**256)-1) - keccak256(1)을 해주고 +1을 하면 0번 index에 접근할 수 있다. 또 Contract의 모든 변수에 액세스 하기 위해여 codex의 길이를 -1 시켜 underflow를 일으킨다.
index가 ((2**256)-1) - keccak256(1)이고 player32는 player의 address를 32byte로 늘린 거다. revise()를 하면 owner를 탈취할 수 있다.