Создание Blockchain CTF: Исследование уязвимостей смарт-контрактов — Часть 1

Автор публикации: Лукьянов Артур, младший исследователь, CyberOK

Мы в СайберОК в ходе пентестов очень любим “взламывать” разнообразные инновационные и необычные вещи. Смарт-контракты на блокчейне давно появились на наших радарах, так как они не только предлагают прозрачность, надежность и автоматизацию, но и легко могут стать объектом атак и уязвимостей. Поэтому, в рамках кибербитвы Standoff, мы решили объединить наш опыт по анализу защищенности и расследованию инцидентов в блокчейне и представить его в игровой форме — в виде открытой платформы для проведения соревнований Capture The Flag (CTF). Мы развернули собственную блокчейн-сеть с помощью ganache, чтобы дать участникам возможность взаимодействовать со смарт-контрактами в наиболее реалистичной атмосфере.

Задачи для CTF придумывали: Артур Лукьянов, Артем Нечаев, Антон Рябков, Александр Чванов, Александр Шведов, Илья Коллегов, Вячеслав Воробьев. Организовывал работу Алексей Москвин.

В этой статье я расскажу, как именно мы делали свой CTF для блокчейна и какие технологии могут помочь вам поиграться со смартконтрактами у себя дома.

Создание задачи (уязвимого смарт-контракта)

Перед началом создания задачи необходимо определить сценарий и контрактную логику, на основе которых будет разработан уязвимый смарт-контракт. Это позволит создать реалистичное окружение — ведь ломать контракт банка с функциями кредитования куда интереснее (и понятнее), чем абстрактного коня в вакууме. Важно учесть различные типы уязвимостей, такие как уязвимости рекурсии, переполнения, проверки прав доступа и другие. Это дает возможность создать задачу с разными уровнями сложности, чтобы каждый участник мог найти что-то интересное и подходящее для своего уровня навыков.

Например, вместо непонятной “хранилки флагов” можно создать сервис для “безопасного хранения заметок/информации”. Когда у сервиса есть легенда, участник CTF понимает, как пользователи могут использовать такое приложение. Так участник будет искать баги функционала и логики, а не играть в “угадайку” или заучивать уязвимые паттерны.

В качестве вдохновения можно брать уже существующие сервисы как референсы, например здесь.

При этом, важно не забывать про то, что это обучающая задача и не переусложнять её, превращая в полноценный аудит.

Подготовка тестового блокчейна с использованием Ganache

Для проведения Blockchain CTF необходимо иметь тестовую блокчейн-среду, которая позволит участникам разрабатывать и тестировать свои решения без риска потери реальных средств. В этой статье мы будем использовать Ganache — легковесную блокчейн-среду, которая позволяет запускать локальный блокчейн для разработки и тестирования смарт-контрактов. Ganache предоставляет удобный интерфейс для создания аккаунтов, имитации различных сценариев и взаимодействия с контрактами.

Я запускал Ganache на Arch Linux, но и для других систем установить его не составит труда: вот официальная инструкция.

Для начала установим ganache-cli:

yay -S ganache-cli

Далее нам понадобится RPC провайдер для ноды. URL RPC провайдера можно получить на alchemy.com или infura.io. Они имеют свои ограничения, но для запуска тестнета “для себя” их вполне хватает.

RPC провайдер необходим только если вы хотите работать с данными реального блокчейна — например, взаимодействовать с уже существующими смартконтрактами. Если ваши задачки не требуют такого, смело пропускайте этот шаг.

Теперь запустим ganache-cli:

ganache-cli -f <RPC provider URL>

Ganache выводит набор приватных ключей для аккаунтов с балансами по 100 ETH. Скопируем один из этих ключей, он понадобится нам позже.

Создание Blockchain CTF: Исследование уязвимостей смарт-контрактов — Часть 1

Разработка смартконтракта

Когда кейс для задачи уже придуман, дело за малым — реализовать уязвимый смартконтракт.

Неплохая Web IDE для разработки: http://remix.ethereum.org/

Для примера далее мы будем использовать следующий смартконтракт:

// SPDX-License-Identifier: MIT pragma solidity ^0.8.7; contract SecretNoteKeeper { string private secretNote; address private owner; constructor(string memory newNote) { secretNote = newNote; } function getSecret() public returns (string memory) { require(msg.sender == owner); return secretNote; } function changeOwner() public { owner = msg.sender; } }

Без знания языка программирования выбранного блокчейна реализовать смартконтракт будет проблематично, хотя ChatGPT, возможно, сможет помочь:

Создание Blockchain CTF: Исследование уязвимостей смарт-контрактов — Часть 1
Создание Blockchain CTF: Исследование уязвимостей смарт-контрактов — Часть 1
Создание Blockchain CTF: Исследование уязвимостей смарт-контрактов — Часть 1
Создание Blockchain CTF: Исследование уязвимостей смарт-контрактов — Часть 1

Неплохо, как отправная точка, но уязвимости всё же лучше добавлять руками.

Загрузка заданий на тестовый блокчейн

После создания уязвимого смарт-контракта необходимо загрузить задание на тестовый блокчейн с помощью Ganache. Это позволит участникам взаимодействовать с контрактом и исследовать его уязвимости.

Можно залить смартконтракт через тот же remix. Но лучше всё же создать скрипт деплоя — тогда повторная заливка и разворачивание стенда на сервере будет куда проще.

Для деплоя смартконтракта будем использовать hardhat: https://hardhat.org/

Создаём hardhat проект:

npx hardhat

Выбираем ‘create JS project’.

Далее, в папку contracts помещаем написанный нами контракт, назовём его SecretNoteKeeper.sol.Контракт-пример (Lock.sol), нужно удалить.

В hardhat.config.js надо указать правильную версию solidity:

Создание Blockchain CTF: Исследование уязвимостей смарт-контрактов — Часть 1

Установим ganache для hardhat:

npm install --save-dev @nomiclabs/hardhat-ganache

Добавим эту строчку в начало hardhat.config.js

require("@nomiclabs/hardhat-ganache");

Изменим scripts/deploy.js

// We require the Hardhat Runtime Environment explicitly here. This is optional // but useful for running the script in a standalone fashion through `node <script>`. // // You can also run a script with `npx hardhat run <script>`. If you do that, Hardhat // will compile your contracts, add the Hardhat Runtime Environment's members to the // global scope, and execute the script. const hre = require("hardhat"); async function main() { // Тут надо поменять SecretNoteKeeper на имя контракта - чтобы можно было создать инстанс const Task = await hre.ethers.getContractFactory("SecretNoteKeeper"); // А тут можно передать необходимые аргументы в конструктор const task = await Task.deploy('ctf{flag}'); await task.deployed(); console.log( `Task deployed to ${task.address}` ); } // We recommend this pattern to be able to use async/await everywhere // and properly handle errors. main().catch((error) => { console.error(error); process.exitCode = 1; });

И задеплоим на ganache:

npx hardhat run --network ganache scripts/deploy.js

(Дополнительно) Создание сайта для web3

Задеплоенного контракта уже достаточно для задачки, но будёт ещё круче оформить всё как Web3 сайт.

Создать базовую страничку для взаимодействия с контрактом не слишком сложно, но перед тем, как начать оформлять её, оцените, готовы ли вы поддерживать ещё и фронт? Браузерные кошельки (Metamask, Trust wallet, etc) могут менять свои API, да и добавление тестнета в них иногда может оказаться совсем не очевидным.

Создадим отдельную папку для фронтенда.

Будем использовать следующий HTML шаблон:

<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-Zenh87qX5JnK2Jl0vWa8Ck2rdkQ2Bzep5IDxbcnCeuOxjzrPF/et3URy9Bv1WTRi" crossorigin="anonymous"> </head> <body> <div class="container"> <h1>CTF task #01</h1> <button id="button-getsecret" class="btn btn-outline-secondary my-3">Get secret</button><br/> <button id="button-changeowner" class="btn btn-outline-secondary">Change owner</button> </div> </body> </html>
Создание Blockchain CTF: Исследование уязвимостей смарт-контрактов — Часть 1

Остаётся подключить кнопки к контракту. Например, так (для Metamask):

const testnetServerAddress = 'https://your-testnet-rpc-url'; // Replace with your testnet server address // Function to connect MetaMask async function connectMetaMask() { // Check if MetaMask is installed if (typeof window.ethereum !== 'undefined') { try { // Request MetaMask to connect await window.ethereum.request({ method: 'eth_requestAccounts' }); console.log('Connected to MetaMask'); } catch (error) { console.error(error); alert('Failed to connect to MetaMask'); } } else { alert('MetaMask is not installed'); } } // Function to invoke getSecret() function async function invokeGetSecret() { // Check if MetaMask is connected if (typeof window.ethereum !== 'undefined') { try { // Get the current selected account const accounts = await window.ethereum.request({ method: 'eth_accounts' }); // Get the contract instance const contract = new window.ethereum.Contract(contractAbi, contractAddress); // Call the getSecret() function const secret = await contract.methods.getSecret().call({ from: accounts[0] }); console.log('Secret:', secret); } catch (error) { console.error(error); alert('Failed to invoke getSecret()'); } } else { alert('Please connect to MetaMask'); } } // Function to invoke changeOwner() function async function invokeChangeOwner() { // Check if MetaMask is connected if (typeof window.ethereum !== 'undefined') { try { // Get the current selected account const accounts = await window.ethereum.request({ method: 'eth_accounts' }); // Get the contract instance const contract = new window.ethereum.Contract(contractAbi, contractAddress); // Call the changeOwner() function await contract.methods.changeOwner().send({ from: accounts[0] }); console.log('Owner changed successfully'); } catch (error) { console.error(error); alert('Failed to invoke changeOwner()'); } } else { alert('Please connect to MetaMask'); } } // Event listener for the 'Get Secret' button document.querySelector('#button-getsecret').addEventListener('click', invokeGetSecret); // Event listener for the 'Change Owner' button document.querySelector('#button-changeowner').addEventListener('click', invokeChangeOwner);

Игровой процесс

Task-based часть Blockchain CTF предлагает интересный игровой процесс для участников. Они получают задачу, которая представляет собой уязвимый смарт-контракт, и их задача состоит в том, чтобы найти и эксплуатировать уязвимость для получения доступа к защищенным ресурсам или выполнения определенного действия. Лучше подготовить несколько разных “испытаний” — так участники смогут проверить свои навыки в разных областях. Такой игровой процесс стимулирует участников к активному обучению и исследованию, а также позволяет им применить свои навыки на практике — “гугление” во время CTF не только развивает чуйку и эрудицию, но и совершенствует навыки.

Подсчет баллов и объявление победителей

После завершения Blockchain CTF процесса необходимо подсчитать баллы участников и объявить победителей. В зависимости от сложности и успешности выполнения задачи участники получают определенное количество баллов. Интересный вариант подсчёта баллов — выдача участнику монет на тестнете за выполнение заданий. Тогда побеждает самый “богатый” на конец соревнования участник.

Так, когда мы проводили CTF на Standoff 10, победителями соревнования стали: Сачивко Никита, Вячеслав Дмитриев, Греков Илья и Левчук Павел.

В следующей части статьи мы рассмотрим “Атаку на реальный смарт-контракт”, где мы расскажем, как использовали реальный контракт для CTF — реставрацию состояния смартконтракта и подготовку контракта для участников.

11
Начать дискуссию