Введение
Смарт-контракты, самоисполняющиеся программы на блокчейне, обеспечивают автоматизацию и децентрализацию множества процессов, от финансовых транзакций до управления цифровыми активами. Однако их безопасность критически зависит от правильной реализации механизмов контроля доступа. Уязвимости контроля доступа (Access Control Vulnerabilities, SC01:2025) возникают, когда смарт-контракт неадекватно ограничивает доступ к своим функциям или данным, позволяя злоумышленникам выполнять несанкционированные действия. Такие уязвимости могут привести к краже средств, манипуляции данными или полной компрометации контракта.
В этой статье мы подробно разберем, что такое уязвимости контроля доступа, рассмотрим реальные примеры атак, проанализируем уязвимый код и предложим рекомендации по устранению подобных проблем. Также мы дополним информацию актуальными данными из открытых источников.
Что такое уязвимости контроля доступа?
Уязвимости контроля доступа возникают, когда смарт-контракт не обеспечивает должной проверки прав пользователей перед выполнением критически важных функций. Это может быть связано с:
- Отсутствием модификаторов доступа: Функции, которые должны быть доступны только определенным ролям (например, владельцу контракта), остаются публичными.
- Неправильной логикой проверки: Ошибки в условиях, проверяющих права пользователя.
- Некорректным управлением ролями: Назначение избыточных привилегий или ошибки в управлении ролями.
- Недостаточной защитой от внешних вызовов: Непроверенные вызовы внешних контрактов, которые могут быть скомпрометированы.
Эти уязвимости позволяют злоумышленникам выполнять действия, для которых они не авторизованы, такие как сжигание токенов, перевод средств, изменение владельца контракта или приостановка его работы.
Воздействие
Последствия уязвимостей контроля доступа могут быть катастрофическими:
- Финансовые потери: Злоумышленники могут вывести средства или токены из контракта.
- Нарушение целостности: Данные контракта могут быть изменены несанкционированно.
- Потеря доверия: Уязвимости подрывают репутацию проекта и блокчейн-экосистемы в целом.
- Дестабилизация рынка: Например, массовая эмиссия или сжигание токенов может повлиять на их рыночную стоимость.
Реальные примеры атак
- HospoWise Hack (2022)
В 2022 году проект HospoWise стал жертвой атаки из-за уязвимости контроля доступа. В контракте функция burn была объявлена публичной, что позволило любому пользователю сжигать токены Hospo. Злоумышленник использовал эту возможность для сжигания большого количества токенов на Uniswap, что вызвало искусственную инфляцию и позволило ему обменять оставшиеся токены на ETH, истощив пул ликвидности.
Уязвимый код (HospoWise):
function burn(address account, uint256 amount) public {
_burn(account, amount);
}
Проблема: Отсутствие модификатора onlyOwner или других проверок доступа позволило любому пользователю вызвать функцию burn.
Решение: Добавление модификатора onlyOwner из библиотеки OpenZeppelin:
import "@openzeppelin/contracts/access/Ownable.sol";
contract HospoWise is Ownable {
function burn(address account, uint256 amount) public onlyOwner {
_burn(account, amount);
}
}
SolidityScan Автоматически выявил данную уязвимость
- LAND NFT Hack (2023)
14 мая 2023 года проект $LAND потерял 149,616 $BUSD из-за уязвимости контроля доступа. Злоумышленник использовал контракт, который позволял нескольким адресам минтить NFT. Один из таких адресов не был должным образом ограничен, что позволило атакующему сминтить 200 NFT, обменять их на токены $XQJ и затем конвертировать в $BUSD.
Обзор взлома смарт-контракта:
Расшифровка уязвимости смарт-контракта:
Проект $land позволял нескольким адресам майнеров выполнять mint NFT, и один из таких адресов был идентифицирован как 0x2e599883715d2f92468fa5ae3f9aab4e930e3ac7.
Атакующий воспользовался уязвимостью и взаимодействовал с контрактом 0x2e599883715d2f92468fa5ae3f9aab4e930e3ac7, чтобы заминтить 200 NFT.
Затем атакующий вызвал функцию 0x2c672a34 контракта по адресу 0xeab03ad7ea0ac5afb272b592bef88cf93ed190c5. С помощью этой функции он обменял накопленные NFT на значительное количество токенов $XQJ — по курсу 200 $XQJ за 1 NFT.
Позже эксплойтер совершил swap, в ходе которого обменял 28,601 $XQJ на сумму в 149,616 $BUSD.
Атакующий продолжал выполнять mint NFT до тех пор, пока не был достигнут лимит выпуска NFT.
Проблема: Отсутствие ограничений на минтинг NFT для определенных адресов.
Решение: Введение строгого контроля доступа, например, с использованием ролевого доступа (RBAC) или проверки onlyOwner.
- The DAO Hack (2016)
Хотя The DAO Hack связан с реentrancy, уязвимость контроля доступа также сыграла роль. Злоумышленник смог многократно вызывать функцию вывода средств из-за отсутствия ограничений на рекурсивные вызовы, что позволило вывести значительную часть средств контракта.
Урок: Контроль доступа должен учитывать не только прямые вызовы, но и возможные сценарии, такие как реentrancy.
Первый Пример уязвимого контракта
Рассмотрим пример смарт-контракта, демонстрирующего уязвимость контроля доступа:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract VulnerablePayroll {
address public payrollWallet;
mapping(address => uint256) public salaries;
// Функция для обновления адреса кошелька для зарплат
function updatePayrollWallet(address newWallet) public {
payrollWallet = newWallet;
}
// Функция для выплаты зарплаты
function paySalary(address employee, uint256 amount) public {
require(payrollWallet != address(0), "Wallet not set");
salaries[employee] += amount;
}
}
Анализ уязвимости
Проблема: Функция updatePayrollWallet является публичной и не имеет никаких проверок доступа. Любой пользователь может изменить адрес payrollWallet, перенаправив выплаты на свой кошелек.
Последствия: Злоумышленник может подменить кошелек и получить все выплаты, предназначенные сотрудникам.
Риски: Финансовые потери и нарушение работы системы.
Исправленный контракт
Используем библиотеку OpenZeppelin Ownable для ограничения доступа:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/access/Ownable.sol";
contract SecurePayroll is Ownable {
address public payrollWallet;
mapping(address => uint256) public salaries;
// Функция для обновления адреса кошелька, доступная только владельцу
function updatePayrollWallet(address newWallet) public onlyOwner {
require(newWallet != address(0), "Invalid wallet address");
payrollWallet = newWallet;
}
// Функция для выплаты зарплаты, доступная только владельцу
function paySalary(address employee, uint256 amount) public onlyOwner {
require(payrollWallet != address(0), "Wallet not set");
salaries[employee] += amount;
}
}
Что изменилось?
- Добавлен модификатор onlyOwner: Только владелец контракта может вызывать функции updatePayrollWallet и paySalary.
- Проверка валидности адреса: Убедились, что новый кошелек не является нулевым адресом.
- Наследование от Ownable: Использование проверенной библиотеки OpenZeppelin для управления правами владельца.
Второй пример уязвимости
Примером может служить контракт, который позволяет пользователям вносить и снимать эфир (основную валюту блокчейна Ethereum). В нём может быть публичная функция withdraw, которая позволяет пользователям снимать эфир. Если контракт не проверяет уровень разрешений пользователя перед выполнением этой функции, злоумышленник может вывести эфир без разрешения пользователя. Поскольку контракт не контролирует должным образом доступ к своим данным и функциям, это и есть уязвимость контроля доступа.
Пример смарт-контракта Solidity с уязвимостью контроля доступа:
Копировать
Редактировать
pragma solidity ^0.8.0;
contract Bank {
mapping(address => uint) public userBalances;
// Функция для внесения средств на счёт
function deposit() public payable {
userBalances[msg.sender] += msg.value;
}
// Функция для снятия средств — уязвимость: нет проверки прав
function withdraw(uint amount) public {
payable(msg.sender).transfer(amount);
}
}
В этом примере контракт Bank позволяет пользователям вносить и снимать эфир со своих счетов. Контракт использует отображение userBalances для хранения баланса каждого пользователя. Однако этот контракт уязвим для атаки на контроль доступа, потому что функция withdraw не проверяет уровень разрешений пользователя перед выполнением. Злоумышленник может вызвать функцию withdraw и вывести эфир из контракта без разрешения пользователя.
Исправление уязвимости
Чтобы устранить эту уязвимость, контракт должен проверять уровень разрешений пользователя перед выполнением функции withdraw. Например, контракт может определить переменную, хранящую адрес владельца, и затем сравнивать её с msg.sender в функции withdraw:
Копировать
Редактировать
pragma solidity ^0.8.0;
contract Bank {
mapping(address => uint) public userBalances;
address public owner;
// Устанавливаем владельца контракта при его создании
constructor() {
owner = msg.sender;
}
// Функция для внесения средств
function deposit() public payable {
userBalances[msg.sender] += msg.value;
}
// Безопасная функция снятия средств — проверка прав владельца
function withdraw(uint amount) public {
require(msg.sender == owner, "Not authorized");
payable(msg.sender).transfer(amount);
}
}
В этой обновлённой версии контракта переменная owner хранит адрес владельца контракта, и функция withdraw проверяет эту переменную перед выполнением перевода. Это предотвращает возможность вызова функции withdraw злоумышленниками и вывода ими эфира без разрешения пользователя.
Типы уязвимостей контроля доступа
- Отсутствие проверок модификаторов: Критические функции остаются публичными, как в случае с burn в HospoWise.
- Неправильные имена модификаторов: Ошибки в написании модификаторов (например, onlyowner вместо onlyOwner) могут привести к обходу проверок.
- Избыточные привилегии: Назначение ролей с чрезмерными правами, что нарушает принцип наименьших привилегий.
- Непроверенные внешние вызовы: Вызовы внешних контрактов без проверки их безопасности.
- Ошибки в логике RBAC: Неправильное управление ролями, например, возможность любому пользователю назначить себе роль администратора.
Рекомендации по устранению
- Используйте проверенные библиотеки:
- Библиотека OpenZeppelin предоставляет готовые решения, такие как Ownable и AccessControl, для реализации контроля доступа.
- Пример использования AccessControl для ролевого доступа:
import "@openzeppelin/contracts/access/AccessControl.sol";
contract RoleBasedContract is AccessControl {
bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE");
constructor() {
_setupRole(DEFAULT_ADMIN_ROLE, msg.sender);
_setupRole(ADMIN_ROLE, msg.sender);
}
function restrictedFunction() public onlyRole(ADMIN_ROLE) {
// Логика функции
}
}
- Принцип наименьших привилегий:
- Назначайте пользователям только те права, которые необходимы для выполнения их задач.
- Ограничивайте доступ к критическим функциям, таким как минтинг токенов или изменение владельца.
- Регулярные аудиты:
- Проводите аудит кода с помощью профессиональных аудиторов, таких как CredShields, или инструментов автоматического анализа, таких как SolidityScan.
- Тестируйте контракт на все возможные сценарии использования.
- Тестирование и формальная верификация:
- Пишите тесты, покрывающие все сценарии доступа, включая попытки несанкционированного вызова функций.
- Используйте формальную верификацию для математического доказательства корректности логики контроля доступа.
- Минимизация внешних вызовов:
- Избегайте ненужных вызовов внешних контрактов, чтобы уменьшить поверхность атаки.
- Если внешние вызовы неизбежны, проверяйте их безопасность и доступ.
- Ограничение повторных вызовов:
- Защищайте контракт от reentrancy с помощью модификаторов, таких как nonReentrant из OpenZeppelin.
Инструменты для обнаружения уязвимостей
- SolidityScan: Автоматически обнаруживает отсутствие модификаторов контроля доступа и другие уязвимости. Поддерживает более 130 видов проверок.
- Slither: Статический анализатор для Solidity, который помогает находить проблемы в коде, включая ошибки контроля доступа.
- Mythril: Инструмент для анализа безопасности смарт-контрактов, выявляющий уязвимости, такие как privilege escalation.
- OpenZeppelin Defender: Платформа для мониторинга и управления безопасностью смарт-контрактов.
Заключение
Уязвимости контроля доступа остаются одной из самых серьезных угроз для безопасности смарт-контрактов. Реальные инциденты, такие как HospoWise и LAND NFT Hack, демонстрируют, насколько разрушительными могут быть последствия таких уязвимостей. Использование проверенных библиотек, таких как OpenZeppelin, применение принципа наименьших привилегий, регулярные аудиты и тестирование помогут минимизировать риски. Разработчики должны уделять особое внимание проектированию и проверке механизмов контроля доступа, чтобы обеспечить безопасность пользователей и целостность своих проектов в децентрализованной экосистеме.