Мы в CyberOK в ходе пентестов очень любим “взламывать” разнообразные инновационные и необычные вещи. Смарт-контракты на блокчейне давно появились на наших радарах, так как они не только предлагают прозрачность, надежность и автоматизацию, но и легко могут стать объектом атак и уязвимостей. В рамках кибербитвы Standoff 10 мы решили объединить наш опыт по анализу защищенности и расследованию инцидентов в блокчейне и представить его в игровой форме — в виде открытой платформы для проведения соревнований Capture The Flag (CTF). Мы развернули собственную блокчейн-сеть с помощью ganache, чтобы дать участникам возможность взаимодействовать со смарт-контрактами в наиболее реалистичной атмосфере.
Эта статья – подробный туториал о том, как сделать такой же 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. Скопируем один из этих ключей, он понадобится нам позже.
Разработка смарт-контракта
Когда кейс для задачи уже придуман, дело за малым — реализовать уязвимый смарт-контракт.
Неплохая 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, возможно, сможет помочь:
Диалог с ChatGPT
Неплохо, как отправная точка, но уязвимости всё же лучше добавлять руками.
Загрузка заданий на тестовый блокчейн
После создания уязвимого смарт-контракта необходимо загрузить задание на тестовый блокчейн с помощью Ganache. Это позволит участникам взаимодействовать с контрактом и исследовать его уязвимости.
Можно залить смарт-контракт через тот же remix. Но лучше всё же создать скрипт деплоя — тогда повторная заливка и разворачивание стенда на сервере будет куда проще.
Для деплоя смарт-контракта будем использовать hardhat: https://hardhat.org/
Создаём hardhat проект:
npx hardhat
Выбираем ‘create JS project’.
Далее, в папку contracts помещаем написанный нами контракт, назовём его SecretNoteKeeper.sol
Контракт-пример (Lock.sol), нужно удалить.
В hardhat.config.js надо указать правильную версию solidity:
Пример конфигурации
Установим ganache для hardhat:
npm install --save-dev @nomiclabs/hardhat-ganache
Добавим эту строчку в начало hardhat.config.js:
require("@nomiclabs/hardhat-ganache");
Изменим scripts/deploy.js:
script/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>
Результат
Остаётся подключить кнопки к контракту. Например, так (для Metamask):
JS для подключения
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 — реставрацию состояния смарт-контракта и подготовку контракта для участников.