Ethereum – платформа, основанная на Blockchain, для создания распределенных онлайн-приложений, работающих на базе смарт-контрактов. Реализация представляет собой единую децентрализованную виртуальную машину. Solidity — объектно-ориентированный, предметно-ориентированный язык программирования самовыполняющихся контрактов для платформы Ethereum. Смарт-контракт представляет собой небольшую программу, написанную на Solidity для автоматизации разного рода задач: сбор средств для стартапа, распределение активов или работу других онлайн-приложений (финансовых приложений, онлайн-казино, систем голосования и т.д.). Как и любые приложения, смарт-контракты могут содержать в себе уязвимости, позволяющие похитить средства.
Reentrancy
Данная уязвимость стала широко известна после краха проекта TheDAO в 2016. В ходе эксплуатации уязвимости злоумышленникам удалось вывести ~3.5 млн. ETH (порядка 53 млн. долларов на то время). Суть уязвимости заключается в том, что функция вывода vulnWithdraw содержит функцию msg.sender, уязвимую для повторного вызова.
contract Reentrancy {
function vulnWithdraw(uint _amount) {
require(balances[msg.sender] >= _amount);
msg.sender.call.value(_amount)();
balances[msg.sender] -= _amount; } }
Когда средства посылаются на адрес, находящийся по значению msg.sender, при помощи функции низкого уровня call(), контракт становится уязвим для повторного вызова и злоумышленник получает в 2 раза больше средств, чем положено. Действия повторяются, пока на атакуемом контракте совсем не останется средств.
Эксплойт:
import “Reentrancy.sol”
contract exploitReentrancy {
Reentrancy public reentrancy;
function withdrawFromReentrancy() {
reentrancy.withdrawSomeMoney(1000); }
function () payable {
reentrancy.withdrawSomeMoney(1000); } }
Как защититься:
- Не использовать функцию низкого уровня call. Если использование call неизбежно, то необходимо производить внутреннюю работу, например, проверку баланса
- Для отправки средств лучше использовать функции send и trance, так как они лишены уязвимости Reentrancy
Denial of Service
Отказ в обслуживании – множество уязвимостей, приводящих к неработоспособности смарт-контракта.
contract DoS {
function give_presents(address[] _winners) private {
uint len = _winners.length;
for (uint i=0; i<n; i++) {
require(_winners[i].send(10)); } } }
Если рассмотреть данный контракт с точки зрения того, что победителей может быть неограниченное количество (или просто очень много) и выигрыш выплачивается одновременно, то для выполнения всего цикла n-раз может понадобиться очень много gas, и если это значение превысит количество ресурсов gas в блоке, то транзакция не будет выполнена. Следовательно, весь выигрыш останется в сети в замороженном состоянии.
Как защититься:
- Делить все заведомо большие циклы на несколько более мелких, чтобы каждый из этих циклов исполнялся в разных транзакциях.
Access Control
Все функции в Solidity относятся к одному из четырех спецификаторов видимости: public, private, external и internal. Функция без объявления спецификатора автоматически причисляется к public, то есть ее можно вызвать отовсюду. С помощью уязвимости данного типа можно завладеть чужим контрактом или же наоборот, заставить пользователя авторизоваться в нужном нам контракте.
function initContract() public {
owner = msg.sender; }
Например, в вышеописанной функции не производится проверка того что владелец контракта уже вызван.
Как защититься:
- Внимательно относиться к объявлению спецификаторов видимости
Arithmetic Issues
Уязвимости целочисленного переполнения, как и в других языках, возникают из‐за ограниченного размера памяти, выделенного на переменную.
function withdraw(uint _amount) {
require(balances[msg.sender] — _amount > 0);
msg.sender.transfer(_amount);
balances[msg.sender] -= _amount; }
Вышеописанная функция из-за отсутствия проверки целостности нижней границы позволяет вывести неограниченное количество токенов.
Второй пример описывает ситуацию, когда результатом арифметической операции над двумя целыми беззнаковыми числами является беззнаковое число. То есть в нашем случае условие под if никогда не будет true и, следовательно, посты не будут удаляться.
function votes(uint postId, uint upvote, uint downvotes) {
if (upvote — downvote < 0) {
deletePost(postId) } }
Как защититься:
- Использовать библиотеку SafeMath от OpenZeppelin
Bad Randomness
Ethereum спроектирован на детерминированном алгоритме, поэтому получить случайность прямо внутри затруднительно.
function play() public payable {
require(msg.value >= 1 ether);
if (block.blockhash(blockNumber) % 2 == 0) {
msg.sender.transfer(this.balance); } }
В вышеописанном примере в качестве энтропии используется хеш блока, и он может быть предугадан.
Как защититься:
- Не использовать в качестве энтропии внутренние переменные контракта (даже private), хеш предыдущих или будущих блоков, переменные блока
Литература
- Документация “Decentralized Application Security Project (or DASP) Top 10 of 2018”, [Электронный ресурс, режим доступа: http://www.dasp.co ]
- Доклад компании Positive Technologies “Initial Coin Offering. Угрозы информационной безопасности”, 2018, 8 с. [Электронный ресурс, режим доступа: https://www.ptsecurity.com/upload/corporate/ru-ru/analytics/ICO-Threats-rus.pdf ]
- Официальная документация по языку Solidity от Ethereum Foundation [Электронный ресурс, режим доступа: https://solidity.readthedocs.io/en ]
- К. Даннен, Введение в Ethereum и Solidity, 2018, 90 c., «Самиздат»