Ethernaut 08 Vault
Ethernaut 08 VaultEthernaut 08 VaultEthernaut 08 Vault 문제 풀이Ethernaut 08 Vault writeup
Contents
문제
지문
Unlock the vault to pass the level!
코드
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Vault {
bool public locked;
bytes32 private password;
constructor(bytes32 _password) {
locked = true;
password = _password;
}
function unlock(bytes32 _password) public {
if (password == _password) {
locked = false;
}
}
}배경지식
컨트랙트의 상태변수는 EVM storage에 저장된다. storage는 32바이트 단위의 슬롯으로 나뉘고, 일반적인 값 타입 상태변수는 선언 순서에 따라 slot 0부터 배치된다.
Solidity는 작은 타입들을 같은 슬롯에 packing할 수 있다. 예를 들어 bool은 1바이트만 필요하다. 하지만 bytes32는 슬롯 하나를 전부 차지하는 32바이트 타입이므로, 앞의 bool이 slot 0에 들어가면 다음 bytes32는 slot 1에 저장된다.
외부에서 특정 슬롯을 직접 읽을 때는 web3.eth.getStorageAt을 사용한다.
await web3.eth.getStorageAt(contractAddress, slotNumber)private는 컨트랙트 코드 레벨에서의 접근 제한이다. 즉 다른 컨트랙트가 password() 같은 getter를 호출하거나 상속 컨트랙트에서 직접 접근하지 못하게 막는다.
하지만 블록체인에 기록된 storage 자체를 숨기는 기능은 아니다. 모든 노드는 컨트랙트 storage를 가지고 있고, RPC를 통해 특정 슬롯의 raw value를 조회할 수 있다. 그래서 비밀값을 상태변수에 그대로 저장하면 private를 붙여도 실제로는 숨겨지지 않는다.
문제 코드 분석
storage 구조는 다음과 같다.
contract Vault {
bool public locked;
bytes32 private password;locked는 첫 번째 상태변수이므로 slot 0에 저장된다. password는 두 번째 상태변수이고 타입이 bytes32라서 slot 1에 저장된다.
private은 자동 getter만 막을 뿐이고, password 값은 컨트랙트 storage에 평문으로 들어간다. 따라서 slot 1을 직접 읽으면 값이 그대로 나온다.
잠금 해제 조건은 다음과 같다.
function unlock(bytes32 _password) public {
if (password == _password) {
locked = false;
}
}unlock은 입력받은 _password가 storage에 저장된 password와 같으면 locked를 false로 바꾼다. 별도의 권한 체크는 없다.
따라서 slot 1에서 password 값을 읽어 그대로 unlock에 넘기면 된다.
풀이
password는 외부 함수로 직접 읽을 수 없을 뿐, storage slot 1에 그대로 저장되어 있다.
slot 1을 조회해서 bytes32 값을 얻고, 그 값을 unlock의 인자로 넣으면 locked가 false가 된다.
익스플로잇
const password = await web3.eth.getStorageAt(
"0x045fA1eF4d2cc0Ec669B1Ac0CD2f7bCd036EF43A",
1
);
await contract.unlock(password);
Challenge
Description
Unlock the vault to pass the level!
Code
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Vault {
bool public locked;
bytes32 private password;
constructor(bytes32 _password) {
locked = true;
password = _password;
}
function unlock(bytes32 _password) public {
if (password == _password) {
locked = false;
}
}
}Background
A contract's state variables are stored in EVM storage. Storage is divided into 32-byte slots, and ordinary value-type state variables are laid out starting from slot 0 in declaration order.
Solidity can pack smaller types into the same slot. For example, a bool needs only 1 byte. However, bytes32 is a 32-byte type that occupies an entire slot, so if the preceding bool goes into slot 0, the following bytes32 is stored in slot 1.
To read a specific slot from outside, use web3.eth.getStorageAt.
await web3.eth.getStorageAt(contractAddress, slotNumber)private is an access restriction at the contract code level. It prevents other contracts from calling a getter like password() and prevents derived contracts from accessing the variable directly.
It does not hide the storage recorded on the blockchain. Every node holds the contract's storage, and the raw value of any slot can be queried over RPC. So a secret stored as-is in a state variable is readable even when marked private.
Code analysis
Start with the storage layout.
contract Vault {
bool public locked;
bytes32 private password;locked is the first state variable, so it is stored in slot 0. password is the second state variable and is of type bytes32, so it is stored in slot 1.
private only suppresses the automatic getter; the value of password sits in the contract's storage in plaintext, so reading slot 1 directly returns it.
The unlock condition:
function unlock(bytes32 _password) public {
if (password == _password) {
locked = false;
}
}unlock sets locked to false if the supplied _password matches the password in storage. There is no separate access control.
So the attack is to read the password value from slot 1 and pass it straight to unlock.
Solution
password just can't be read through an external function; it is still stored as-is in storage slot 1.
Query slot 1 to get the bytes32 value, pass it as the argument to unlock, and locked becomes false.
Exploit
const password = await web3.eth.getStorageAt(
"0x045fA1eF4d2cc0Ec669B1Ac0CD2f7bCd036EF43A",
1
);
await contract.unlock(password);